1. @property
1.1. 介绍
对于类对象,在绑定属性的时候,如果直接把属性暴露出去,虽然使用起来简单,但是没有办法检查设置的参数;此时可以通过实现一些方法来完成对属性值的校验,但是此时使用起来会发现比较复杂。Python内置的@property装饰器就是负责把一个方法变成属性调用的。
1.2. 基本用途
- 对属性值进行校验(通常是对属性进行赋值操作的时候进行一些参数值校验)
- 定义只读属性。
- 定义只写属性(比如设置password为A,然后程序内部通过加密将password加密为B然后保存到password_hash,此时设置password为只写属性,即无法获取password的明文)
下面以一个定义只写属性的例子来说明:
1.2.1. 定义只写属性
一个比较常用的场景就是密码加密,比如下面的例子(需要了解werkzeug中的生成密码和校验密码的函数):
- 设置密码,此时会调用User类中的password方法,从而将密码加密,并保存到password_hash中,而password是一个只写属性,不过可以读取加密之后的密文。
- 如果尝试读取password,此时会抛出一个错误。
- 此时可以读取加密之后的密文password_hash。
- 可以使用verify_password函数来验证密码。
# Werkzeug=2.1.2
# python=3.8
from werkzeug.security import generate_password_hash, check_password_hash
class User:
@property
def password(self):
raise AttributeError('password is not readable attribute')
@password.setter
def password(self, password):
# generate_password_hash会根据输入的参数将其加密,得到一个加密之后的hash值。
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
# check_password_hash会对比加密之后的hash和加密之前的密码,从而判断密码是否正确。
return check_password_hash(self.password_hash, password)
if __name__ == '__main__':
user = User()
user.password = "123"
print(user.password) # AttributeError: password is not readable attribute
print(user.password_hash) # pbkdf2:sha256:260000$UDSvk3c1TsaXCqdX$85eb85fe7258e7d7519ccd504ae36fab91e7419e3a4f63233a0fe7fb91703834
print(user.verify_password("123")) # True
1.3. 注意事项
1.3.1. 注意属性的方法名和实例变量不要重名
如下,此时调用p.first_name的时候就会报错,报错的原因是执行p.first_name时,首先会转化为方法调用,然后执行Person类中的first_name()方法,在该方法中,执行return self.first_name的时候,又被看做是self的属性,从而再次转化为方法调用,从而造成递归,最终导致栈溢出,报错RecursionError。(递归栈溢出Python3.7.3 中默认递归深度为1000,若超过此递归深度将抛出异常)
# python=3.8
class Person:
# birth = "abc"
@property
def first_name(self):
return self.first_name
if __name__ == '__main__':
p = Person()
print(p.first_name) # RecursionError: maximum recursion depth exceeded
1.4. 将方法转化为属性的3种方式
1.4.1. 使用@property装饰器
使用@property装饰器将方法作为属性来访问。支持setter和deleter。注意,需要首先使用@property将方法定义为属性,之后才可以定义该属性的setter和deleter装饰器。
比如下面的例子:
- 注意,初始化__init__方法中没有直接对_first_name初始化,而是对first_name初始化,但是对first_name初始化的时候,此时相当于对first_name进行赋值操作,即会调用@first_name.setter函数,从而对_first_name进行了赋值。
- 这里使用property的主要作用是设置属性的时候进行类型检查
class Person:
def __init__(self, first_name):
self.first_name = first_name
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError("Expected a string")
self._first_name = value
@first_name.deleter
def first_name(self):
raise AttributeError("Can't delete attribute")
if __name__ == '__main__':
p = Person("Tom") # 初始化的时候,相当于执行了对first_name属性的赋值操作,从而会自动执行setter函数
print(p.first_name) # Tom
1.4.2. 将已经存在的方法定义为property-方式1
除了上述提到的@property的方法,还可以使用property类将已经存在方法绑定到类的成员属性上,property类函数的说明如下所示:
class property([fget[, fset[, fdel[, doc]]]])
参数
fget -- 获取属性值的函数
fset -- 设置属性值的函数
fdel -- 删除属性值函数
doc -- 属性描述信息
返回值
返回新式类属性。
以具体的例子说明:
- 可以定义3个方法,并传递给property中,property的三个参数值分别为fget、fset和fdel,从而将这3个方法(get_first_name、set_first_name、del_first_name)附加到类属性first_name中。
- 当调用print(p.first_name)时,此时不会去字典表__dict__中查找key为first_name的值,而是会调用Person.first_name.fget,即相当于执行了get_first_name()方法。比较Person.first_name.fget的id和Person类中的get_first_name函数的id,可以发现是一样的。
class Person():
def __init__(self, first_name):
self.set_first_name(first_name)
def get_first_name(self):
return self._first_name
def set_first_name(self, value):
if not isinstance(value, str):
raise TypeError("Expected a string")
self._first_name = value
def del_first_name(self):
raise AttributeError("Can't delete attribute")
# 将已有的方法绑定到property上。property的参数分别是fget fset和fdel。
first_name = property(get_first_name, set_first_name, del_first_name)
if __name__ == "__main__":
p = Person("Tom")
p.first_name = "Mary"
print(p.first_name) # Mary
# del p.first_name # AttributeError: Can't delete attribute
print(id(Person.first_name.fget)==id(Person.get_first_name)) # True,
1.4.3. 将已经存在的方法定义为property-方式2
除了上述提及的方法,还可以使用如下的方式:
- 首先,初始化一个空的property对象,之后调用getter、setter和deleter分别对齐进行赋值。
# python=3.8
class Person():
def __init__(self, first_name):
self.set_first_name(first_name)
def get_first_name(self):
return self._first_name
def set_first_name(self, value):
if not isinstance(value, str):
raise TypeError("Expected a string")
self._first_name = value
def del_first_name(self):
raise AttributeError("Can't delete attribute")
# 定义一个空的property
first_name = property()
# 分别增加getter、setter和deleter属性。
first_name = first_name.getter(get_first_name)
first_name = first_name.setter(set_first_name)
first_name = first_name.deleter(del_first_name)
if __name__ == "__main__":
p = Person("Tom")
p.first_name = "Mary"
print(p.first_name) # Mary
# del p.first_name # AttributeError: Can't delete attribute
print(id(Person.first_name.fget)==id(Person.get_first_name)) # True,