网站首页 > 技术文章 正文
点赞、收藏、加关注,下次找我不迷路
自己写了一个类,里面的属性和方法不想被随便修改或访问,该怎么办呢?别着急,今天咱们就来好好聊聊 Python 里的访问限制技巧。
一、为啥需要访问限制?
咱先打个比方,你开了一家银行,里面有很多重要的信息,比如客户的存款金额、密码等。这些信息肯定不能让所有人都随便查看和修改,得有一定的保护措施。在 Python 里,类就像是这家银行,里面的属性和方法就是银行的信息和操作,我们需要通过访问限制来确保这些 “信息” 的安全性和完整性。
比如说,我们有一个学生类,里面有学生的姓名和成绩。成绩肯定是一个敏感信息,我们不希望外部代码随意修改成绩,或者不小心写错了成绩。这时候,访问限制就派上用场了,它可以让我们控制哪些属性和方法可以被外部访问,哪些只能在类内部使用。
二、Python 访问限制的三种 “防护盾”
在 Python 中,主要通过属性和方法命名的约定来实现访问限制,虽然不像 Java 等语言有严格的 public、private、protected 关键字,但通过一些命名规则,也能达到类似的效果。常见的有公开属性、保护属性和私有属性这三种,咱们一个一个来详细说。
(一)公开属性:大大方方随便看(默认情况)
在 Python 中,默认情况下,类的属性和方法都是公开的,也就是说,外部代码可以直接访问和修改。比如我们定义一个简单的类:
class Student:
def __init__(self, name, age):
self.name = name # 公开属性
self.age = age # 公开属性
def say_hello(self): # 公开方法
print(f"Hello, my name is {self.name}, I'm {self.age} years old.")
创建实例后,就可以直接访问和修改这些属性:
stu = Student("小明", 18)
print(stu.name) # 输出:小明
stu.age = 20 # 直接修改年龄
print(stu.age) # 输出:20
stu.say_hello() # 调用公开方法,输出:Hello, my name is 小明, I'm 20 years old.
这就相当于银行的大厅,任何人都可以进来看看,虽然方便,但也缺乏保护。在实际开发中,如果属性可以被随意修改,可能会导致数据不一致等问题。比如成绩属性,如果外部代码不小心写成了负数,就会出现错误。所以,我们需要更严格的访问限制。
(二)保护属性:加个 “小提醒”(单下划线开头)
保护属性是在属性名前加一个单下划线,比如_attr。这其实是一种约定俗成的方式,告诉其他开发者,这个属性是 “受保护的”,虽然外部代码仍然可以访问和修改,但最好不要这么做,应该在类内部或子类中使用。
比如我们修改一下学生类,把年龄设为保护属性:
class Student:
def __init__(self, name, age):
self.name = name
self._age = age # 保护属性,单下划线开头
def say_age(self):
print(f"My age is {self._age}")
外部代码仍然可以访问和修改_age:
stu = Student("小红", 19)
print(stu._age) # 输出:19
stu._age = 21 # 还是能修改
print(stu._age) # 输出:21
但是,作为有素质的开发者,我们应该遵守这个约定,不要轻易修改保护属性。就像银行的员工通道,虽然外人也能进去,但这是给员工用的,外人最好别进去捣乱。保护属性主要用于子类继承时,子类可以访问父类的保护属性,进行扩展和重写。
(三)私有属性:藏得严严实实(双下划线开头)
私有属性是在属性名前加两个下划线,比如__attr。这时候,Python 会对这个属性进行名称改写,将其变为_类名__attr,这样外部代码就不能直接通过原来的属性名访问了,从而实现了更严格的访问限制。
咱们再改改学生类,把成绩设为私有属性:
class Student:
def __init__(self, name, score):
self.name = name
self.__score = score # 私有属性,双下划线开头
def get_score(self): # 获取成绩的方法
return self.__score
def set_score(self, score): # 设置成绩的方法,添加验证
if 0 <= score <= 100:
self.__score = score
else:
print("成绩必须在0到100之间!")
这时候,如果外部代码直接访问__score,就会报错:
stu = Student("小刚", 85)
print(stu.__score) # 报错:AttributeError: 'Student' object has no attribute '__score'
那怎么访问私有属性呢?可以通过类提供的公共方法,比如上面的get_score()和set_score()。在设置成绩的方法中,我们还可以添加验证逻辑,确保成绩在合理范围内,这就保证了数据的有效性。
不过,虽然私有属性不能直接通过原来的名称访问,但通过名称改写后的名称还是可以访问的,比如stu._Student__score:
print(stu._Student__score) # 输出:85
stu._Student__score = 120 # 虽然能修改,但这是不推荐的做法
print(stu.get_score()) # 输出:120,不过我们设置的验证逻辑没起作用,所以千万别这么干!
这就相当于银行的保险库,虽然有办法强行打开,但这是违反规定的,会带来安全隐患。所以,我们一定要通过类提供的公共方法来操作私有属性,保证数据的安全。
三、三种属性对比,一目了然
为了让大家更清楚地理解这三种属性的区别,咱们做个表格来总结一下:
类型 | 命名规则 | 访问权限 | 示例代码 | 注意事项 |
公开属性 | 普通命名 | 外部可直接访问和修改 | self.name | 无特殊限制,使用方便但缺乏保护 |
保护属性 | 单下划线开头 | 外部可访问修改,但约定不建议这么做 | self._age | 子类可继承访问,用于内部或子类使用 |
私有属性 | 双下划线开头 | 外部不能直接访问,需通过公共方法 | self.__score | Python 会进行名称改写,可通过_类名__attr 访问,但不推荐 |
通过这个表格,是不是一下子就清楚了?以后看到不同命名的属性,就知道该怎么处理啦。
四、@property 装饰器,让属性访问更优雅
上面我们通过定义get_score()和set_score()方法来操作私有属性,虽然实现了访问限制,但调用的时候需要像方法一样加括号,比如stu.get_score()。有没有更优雅的方式,让我们可以像访问公开属性一样访问私有属性呢?这时候,@property装饰器就派上用场了。
(一)啥是 @property?把方法变成属性来用
@property装饰器可以将一个方法转换为属性,这样我们就可以像访问普通属性一样访问这个方法,不需要加括号。同时,我们还可以通过@属性名.setter装饰器来定义设置属性的方法,实现对属性的读写控制。
还是以学生类的成绩为例,我们用@property来改写:
class Student:
def __init__(self, name, score):
self.name = name
self.__score = score # 私有属性
@property
def score(self): # getter方法,转换为属性
return self.__score
@score.setter
def score(self, score): # setter方法,设置属性
if 0 <= score <= 100:
self.__score = score
else:
print("成绩必须在0到100之间!")
现在,访问和设置成绩就像操作公开属性一样方便:
stu = Student("小芳", 90)
print(stu.score) # 像属性一样访问,输出:90
stu.score = 85 # 像属性一样设置,合法
print(stu.score) # 输出:85
stu.score = 150 # 不合法,输出提示信息
print(stu.score) # 成绩不变,还是85
这样是不是更简洁、更符合我们的使用习惯呢?而且,通过@property,我们可以在获取和设置属性时添加各种逻辑,比如验证、计算等,让代码更灵活。
(二)只读属性:只定义 getter,不定义 setter
如果我们希望某个属性只能读取,不能设置,比如一些计算出来的属性,只需要定义@property装饰的 getter 方法,不定义 setter 方法就可以了。
比如,我们计算学生的等级,根据成绩来划分:
class Student:
def __init__(self, name, score):
self.name = name
self.__score = score
@property
def score(self):
return self.__score
@score.setter
def score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
print("成绩必须在0到100之间!")
@property
def grade(self): # 只读属性,根据成绩计算等级
if self.score >= 90:
return "A"
elif self.score >= 80:
return "B"
elif self.score >= 60:
return "C"
else:
return "D"
这时候,grade属性只能读取,不能设置:
stu = Student("小强", 75)
print(stu.grade) # 输出:B
stu.grade = "A" # 报错:AttributeError: can't set attribute 'grade'
这样就保证了等级属性的只读性,避免外部代码错误地修改。
五、slots,限制实例属性的种类
有时候,我们希望限制一个类的实例只能拥有特定的属性,不能随意添加其他属性,这时候可以使用__slots__魔法方法。__slots__定义了一个类允许的属性列表,实例只能拥有列表中的属性,这样可以节省内存,提高效率,同时也能防止误添加属性。
(一)怎么用?定义__slots__元组
比如,我们定义一个学生类,只允许有name和age属性,不允许添加其他属性:
class Student:
__slots__ = ("name", "age") # 定义允许的属性
def __init__(self, name, age):
self.name = name
self.age = age
创建实例后,只能设置name和age属性,添加其他属性会报错:
stu = Student("小伟", 20)
stu.name = "小辉" # 合法
stu.age = 21 # 合法
stu.score = 90 # 报错:AttributeError: 'Student' object has no attribute 'score'
(二)注意:__slots__对继承的影响
如果一个子类没有定义__slots__,那么它会继承父类的__slots__,同时还可以添加自己的属性。但如果子类定义了__slots__,那么它的实例只能拥有父类__slots__和自己__slots__中的属性,除非在子类的__slots__中包含'__dict__',这样才能允许实例有动态属性。
比如:
class GraduateStudent(Student):
__slots__ = ("major",) # 子类定义自己的slots
def __init__(self, name, age, major):
super().__init__(name, age)
self.major = major
这时候,GraduateStudent的实例只能有name、age、major属性,不能添加其他属性。如果想让子类实例可以有动态属性,需要在__slots__中加入'__dict__':
class GraduateStudent(Student):
__slots__ = ("major", "__dict__") # 允许动态属性
六、整个记忆口诀
为了让大家更容易记住这些访问限制的技巧,咱们来编个口诀:
单下划线是提醒,保护属性别乱改;
双下划线真隐藏,名称改写来帮忙;
@property 装饰器,属性访问变优雅;
slots 来限制,属性种类跑不了。
七、隐藏在背后的问题
(一)私有属性真的完全不能被外部访问吗?
不是的,通过名称改写后的名称_类名__attr还是可以访问的,但这是不推荐的做法,会破坏封装性,所以一定要通过类提供的公共方法来操作。
(二)子类能继承父类的私有属性吗?
子类不能直接访问父类的私有属性,因为私有属性在父类中被名称改写了,子类中的同名属性不会冲突。如果父类有公共方法访问私有属性,子类可以调用这些方法。
(三)什么时候用保护属性,什么时候用私有属性?
如果希望子类可以访问和修改该属性,用保护属性(单下划线);如果不希望任何外部代码(包括子类)直接访问,必须通过父类的公共方法操作,用私有属性(双下划线)。
(四)__slots__和__dict__有什么关系?
默认情况下,Python 实例有一个__dict__属性,用于存储动态添加的属性,这会占用一定的内存。使用__slots__后,实例不再有__dict__,只能有__slots__中定义的属性,从而节省内存。如果需要动态添加属性,需要在__slots__中包含'__dict__',但这样会部分失去__slots__节省内存的优势。
以后在写类的时候,根据不同的需求选择合适的访问限制方式,比如敏感属性用私有属性加@property,需要限制实例属性种类时用__slots__。多练习、多实践,你会越来越熟练的。
猜你喜欢
- 2025-07-27 UV 迄今最快、最好的 Python 包管理器
- 2025-07-27 Python“三步”即可爬取,毋庸置疑
- 2025-07-27 第二章:Python 运算符与表达式(python语言中运算符号)
- 2025-07-27 掌握Python比较运算符:核心要点与实战解析
- 2025-07-27 Python教程(九):While循环与真实示例
- 2025-07-27 Python字符串对齐神技!4种方法让你的输出瞬间专业10倍
- 2025-07-27 Python运算符与表达式(python运算符含义)
- 2025-07-27 一图看懂 Python 2 / Python 3 编码 | CSDN 博文精选
- 2025-07-27 让 Python 代码飙升330倍:从入门到精通的四种性能优化实践
- 2025-05-03 如何在 Python 中创建一个不可变的字典 - Adam Johnson
- 1517℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 596℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 521℃MySQL service启动脚本浅析(r12笔记第59天)
- 489℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 489℃启用MySQL查询缓存(mysql8.0查询缓存)
- 477℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 456℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 454℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- windowsscripthost (69)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)