markdown
# Python 中的 @property:像访问属性一样调用方法
在写类的时候,我们经常会遇到一个问题:
对象的属性如果可以被随便修改,就可能出现一些不合理的数据。
比如一个人的年龄:
```python
class Person:
def __init__(self, age):
self.age = age
然后创建对象:
python
person1 = Person(-18)
print(person1.age)
这时候程序是可以正常运行的,但是很明显不合理。
因为年龄不可能是负数。
再比如学生成绩:
python
class Student:
def __init__(self, score):
self.score = score
创建对象:
python
student1 = Student(100)
print(student1.score)
一开始这样写没什么问题。
但是如果外界可以随便修改:
python
student1.score = -100
那就会出现不合理的数据。
所以问题来了:
Python 有没有像 Java 一样,可以把属性保护起来,然后只暴露指定的接口让外界调用呢?
答案是有的。
在 Python 中,我们可以使用 @property 装饰器来控制属性的访问。
一、为什么需要属性访问控制
如果一个属性可以被外界随便赋值,那么数据就可能变得不安全。
比如:
python
person1 = Person(-18)
这个对象虽然创建成功了,但是 -18 这个年龄明显是不合理的。
我们希望在给 age 赋值的时候,能够先判断一下:
python
if age < 0 or age > 120:
raise ValueError("不合理的年龄")
如果年龄不合理,就直接抛出异常。
二、使用 @property 定义属性
完整代码如下:
python
class Person:
def __init__(self, age):
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, age):
if age < 0 or age > 120:
raise ValueError('不合理的年龄')
self._age = age
person1 = Person(18)
print(person1.age)
这段代码中,最重要的部分就是:
python
@property
def age(self):
return self._age
@property 的作用是:
把一个方法变成属性一样来访问。
也就是说,虽然 age 本质上是一个方法:
python
def age(self):
return self._age
但是我们调用的时候不需要写成:
python
person1.age()
而是可以直接写:
python
person1.age
看起来就像是在访问普通属性一样。
三、使用 setter 控制赋值
下面这段代码用来控制给 age 赋值:
python
@age.setter
def age(self, age):
if age < 0 or age > 120:
raise ValueError('不合理的年龄')
self._age = age
当我们执行:
python
self.age = age
或者:
python
person1.age = 18
实际上都会自动调用这个 setter 方法。
所以在赋值之前,程序会先判断年龄是否合法:
python
if age < 0 or age > 120:
raise ValueError('不合理的年龄')
如果年龄小于 0,或者大于 120,就会抛出异常。
如果年龄合理,才会真正赋值:
python
self._age = age
四、为什么使用 _age
在代码中,真正保存年龄的属性是:
python
self._age
而不是:
python
self.age
这是因为 self.age 已经被 @property 和 @age.setter 接管了。
如果在 setter 里面继续写:
python
self.age = age
就会再次调用 setter,导致无限递归。
所以我们用:
python
self._age = age
来真正保存数据。
这里的 _age 前面有一个下划线,表示这是一个内部使用的属性,不建议外界直接访问。
五、创建对象时发生了什么
当我们创建对象:
python
person1 = Person(18)
会先执行构造方法:
python
def __init__(self, age):
self.age = age
这里的:
python
self.age = age
并不是简单地创建一个普通属性。
因为类里面已经定义了:
python
@age.setter
def age(self, age):
所以这句代码会自动调用 setter 方法。
也就是说,创建对象的时候,年龄就已经被检查了一遍。
如果写成:
python
person1 = Person(-18)
就会抛出异常:
python
ValueError: 不合理的年龄
这样就避免了创建出不合理的对象。
六、读取属性时发生了什么
当我们执行:
python
print(person1.age)
这里的 .age 其实不是直接访问属性,而是在调用这个方法:
python
@property
def age(self):
return self._age
只不过 @property 让它看起来像普通属性一样。
所以:
python
person1.age
本质上相当于调用了 getter 方法。
七、@property 的好处
使用 @property 有几个好处:
- 可以像访问普通属性一样访问方法
- 可以在赋值时增加数据校验
- 可以保护对象内部数据
- 可以让代码更加安全
- 对外使用简单,对内逻辑可控
比如外界仍然可以这样写:
python
person1.age
也可以这样赋值:
python
person1.age = 20
但是赋值的时候,程序会自动帮我们检查数据是否合法。
八、总结
@property 装饰器可以把方法伪装成属性来访问。
它通常和 setter 一起使用:
python
@property
def age(self):
return self._age
@age.setter
def age(self, age):
if age < 0 or age > 120:
raise ValueError('不合理的年龄')
self._age = age
这样做的好处是:
既保留了属性访问的简洁写法,又可以在内部控制数据是否合法。
简单来说:
python
@property
负责读取属性。
python
@属性名.setter
负责设置属性。
在这个例子中,我们通过 @property 和 @age.setter,让 age 属性不能被随便设置成不合理的值。
这就是 Python 中属性访问控制的一种常见写法。