Python的描述符
目录
1.引出描述符
点击跳转此案例 属性方法应用-限制值 可知,@property装饰器 和 @xxx.setter装饰器 可以将对属性的调用或赋值转换成为对方法的调用,完美解决了需求且没有降低代码的可读性。此时这个被@property装饰的方法即为property属性。
1.1 @property是什么
@property是python的一种装饰器,用于修饰方法,一般与getter、setter、deleter方法搭配使用,装饰只读方法、修改方法和删除方法用于访问、修改或删除属性。而property是python中的内置函数,底层用C语言实现(源代码在Objects/descrobject.c中),python中的语法参数如下:
property(fget=None, fset=None, fdel=None, doc=None)
fget –获取属性值的函数 fset –设置属性值的函数 fdel –删除属性值函数 doc –属性描述信息
1.2 property怎么使用
1.2.1 类属性方式
类属性方式即在类中创建一个为property实例对象的类属性,如下所示:
class C(object):
def __init__(self):
self._x = None
def getx(self):
print('getx called')
return self._x
def setx(self, value):
print('setx called')
self._x = value
def delx(self):
print('delx called')
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
c = C()
print(c.x.__doc__) # 获取第四个参数中设置的值:输出:I'm the 'x' property.
c.x = 20 # 输出:setx called
c.x # 输出:getx called
del c.x # 输出:delx called
c 是 C类对象的实例对象,可通过c.x.__doc__
获取属性描述信息,c.x = 20
将触发 setx,c.x
触发 getx, del c.x
触发 delx。
1.2.2 装饰器方式
装饰器方式即在类的实例方法上应用@property,并搭配getter、setter 和 deleter方法修饰修改和删除方法。不过,经典类中只有@property 修饰方法,因此通过装饰器方式创建的property属性只读,但可以通过上述使用property函数创建类属性的方式可获得完整功能;新式类中以上装饰器均有。
注意:
以下通过新式类举例说明使用方法:
class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.getter
def x(self):
print("Getting value")
return self._value
@x.setter
def x(self, value):
print("Setting value")
self._x = value
@x.deleter
def x(self):
print("deletting value")
del self._x
c = C()
c.x = 20 # 输出:Setting value
c.x # 输出:Getting value
del c.x # 输出:deletting value
@property装饰器定义了一个名为x的属性,使用@x.getter修饰器标记getter方法,使用@x.setter修饰器标记setter方法,使用@x.deleter修饰器标记deleter方法。这样就可以像访问普通属性一样来访问、修改和删除x属性。当访问x属性时,会自动调用getter方法;当修改x属性时,会自动调用setter方法;当删除x属性时,会自动调用deleter方法。
1.3 property应用场景
1.3.1 计算属性(Lazy evaluation 或懒加载)
如果某个属性的值需要通过对象的状态计算获得,且计算过程较为复杂或资源消耗较大,可以使用 property 延迟计算,只有在实际访问该属性时才进行计算。通过这种方式,可以避免每次访问时都进行昂贵的计算。示例如下:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
print("Calculating area...")
return 3.14 * self._radius ** 2
c = Circle(5)
print(c.area) # 计算并返回圆的面积
1.3.2 只读属性
如果某个属性值在初始化后不能修改,可以将其转换为只读属性。通过在类中定义 property 装饰器而不实现 setter 方法,可以确保属性值的只读性。示例如下:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
p = Person("Alice")
print(p.name) # 输出 Alice
p.name = "Bob" # 会抛出 AttributeError
1.3.3 计算后缓存(缓存属性)
当某个属性的值在多次访问过程中不会改变时,可以使用 property 来实现计算后的缓存,避免重复计算。可以在 getter 方法中计算并缓存值,之后返回缓存的结果。示例如下:
class Fibonacci:
def __init__(self, n):
self.n = n
self._fib = None # 用于缓存已计算的结果
@property
def fib(self):
if self._fib is None:
print("Calculating Fibonacci...")
a, b = 0, 1
for _ in range(self.n):
a, b = b, a + b
self._fib = a
return self._fib
f = Fibonacci(10)
print(f.fib) # 计算并缓存 Fibonacci(10)
print(f.fib) # 使用缓存结果
1.3.4 数据验证和规范化
使用 property 可以为属性的设置增加验证逻辑。当想要限制对象的某个属性的取值范围或数据类型或对输入进行规范化时,可以使用属性的 setter 方法对输入数据进行校验,并确保它们满足要求。示例如下:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be greater than 0")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be greater than 0")
self._height = value
r = Rectangle(5, 10)
r.width = -5 # 会抛出 ValueError
1.3.5 延迟初始化(Lazy Initialization)
如果类的某些属性需要通过复杂的初始化过程来生成,可以使用 property 来实现延迟初始化,只有在需要时才进行初始化。示例如下:
class ExpensiveResource:
def __init__(self):
self._data = None
@property
def data(self):
if self._data is None:
print("Initializing expensive resource...")
self._data = [1, 2, 3, 4, 5] # 假设这是一个计算开销大的操作
return self._data
resource = ExpensiveResource()
print(resource.data) # 第一次访问时初始化
print(resource.data) # 后续访问直接返回初始化后的数据
1.3.6 封装和信息隐藏
property 还可以用于封装类的内部实现细节,从而隐藏不必要暴露给外部的属性。通过将属性的读取和写入逻辑集中在 getter 和 setter 方法中,外部代码可以通过简单的属性访问方式来与对象交互,而不暴露复杂的内部实现。示例如下:
class BankAccount:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, amount):
if amount < 0:
raise ValueError("Balance cannot be negative!")
self._balance = amount
account = BankAccount(1000)
print(account.balance) # 输出 1000
account.balance = 2000 # 正常设置
print(account.balance) # 输出 2000
1.4 property的不足
property最大的缺点就是它们不能重复使用。如创建一个Student类,录入每个学生实例的各学科成绩,规定数学成绩区间为0~100,用property实现如下:
class Student:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
@property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")
但chinese、english也需要满足成绩区间为0~100,继续用property分别对chinese、english进行修饰。
class Student:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
@property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")
@property
def chinese(self):
return self._chinese
@chinese.setter
def chinese(self, value):
if 0 <= value <= 100:
self._chinese = value
else:
raise ValueError("Valid value must be in [0, 100]")
@property
def english(self):
return self._english
@english.setter
def english(self, value):
if 0 <= value <= 100:
self._english = value
else:
raise ValueError("Valid value must be in [0, 100]")
Student类里的三个属性,math、chinese、english都使用了 property 对属性的合法性进行了有效控制。功能上没有问题,但是代码重复瘫肿,因为三个属性的合法性逻辑都是一样的。若Student类还有地理、生物、历史、化学等十几门的成绩,property实现使得python代码不再优雅!
此时描述符便可以实现对相同逻辑封装复用!property 其实是python中的一种内置描述符,是描述符机制的一种实现,用于简化对对象属性的管理。
2.描述符是什么
2.1 描述符定义
官方文档的定义如下:
In general, a descriptor is an object attribute with “binding behavior”,
one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__().
If any of those methods are defined for an object, it is said to be a descriptor.
通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。这些方法是__get__(),__set__()和 __delete__()。如果为对象定义了任何这些方法,则称其为描述符。
换言之,如果一个类实现了__get__()、__set__()和__delete__()方法中的任意一种,即实现了描述符协议,这个类称为描述符类;用这个类创建的实例对象称之为描述符对象;若另一个类中的类属性指向这个描述符对象,把这个类属性称之为描述符。
Python 中的描述符(Descriptor)是一个实现了特定方法的对象,这些方法定义了如何访问、修改和删除另一个对象的属性。描述符可以用来实现属性访问的定制,通常用于实现属性的访问控制、懒加载、数据验证等功能。
2.2 描述符协议
Python描述符协议是一种用于实现拦截属性访问的机制,它允许对象控制其属性的访问(读取、写入和删除)。通过描述符协议中的方法,一个对象可以拦截并处理对另一个对象的属性的访问请求。主要用于实现属性验证、计算属性、延迟加载等功能。
描述符协议的核心由以下三个方法组成,这些方法通常用来访问和修改属性的值:
方法 |
参数 |
用途 |
---|---|---|
__get__(self, instance, owner) | self:描述符实例本身。 instance:访问该属性的实例。 owner :属性所在的类。 |
该方法用于访问属性的值。有返回值,这个值将被作为属性的值返回给调用者。 |
__set__(self, instance, value) | self:描述符实例本身。 instance:访问该属性的实例。 value:赋给属性的值。 |
该方法用于设置属性的值。允许在设置属性时进行额外的验证或修改。 |
__delete__(self, instance) | self:描述符实例本身。 instance:访问该属性的实例。 |
该方法用于删除属性。允许在删除属性时进行额外的操作,例如记录日志或释放资源。 |
注意:当两个类是继承关系时,self
指的是当前实例,而不是定义self
的类的实例;但描述符类与指向此描述符类实例的类属性所在的类并非继承关系,因此在描述符类中,self
指的是描述符类的实例,instance
才是当前实例。
2.3 创建和使用描述符
描述符有两种类型:数据描述符和非数据描述符。
2.3.1 数据描述符
实现了__get__()和__set__()方法 ,可以不实现__delete__()方法。示例:
class NameDes(object):
def __init__(self):
self.__name = None
def __get__(self, instance, owner):
print('get called')
return self.__name
def __set__(self, instance, value):
if isinstance(value, str):
print('set called')
self.__name = value
else:
raise TypeError("必须是字符串")
def __delete__(self, instance):
print('delete called')
del self.__name
class Person(object):
name = NameDes()
NameDes
实现了__get__()、__set__()和__delete__()方法,因此是一个数据描述符类,可以进行获取,赋值,删除控制;name
是Person
的类属性,并且指向了NameDes()
实例,因此是一个数据描述符。
运行以下代码,直观感受描述符对属性的控制。
p = Person()
p.name = 100
"""
运行结果如下:
Traceback (most recent call last):
File "D:\main.py", line 26, in <module>
p.name = 100
File "D:\main.py", line 15, in __set__
raise TypeError("必须是字符串")
TypeError: 必须是字符串
"""
由于NameDes的__set__()方法中进行了数据类型约束,限制赋值必须是字符串,因此在Person实例p试图对描述符name赋一个数值时,抛出数据类型异常。符合描述符规范赋值应为:
p = Person()
p.name = "小李子"
print(p.name)
del p.name
"""
运行结果如下:
set called
get called
小李子
delete called
"""
2.3.2 非数据描述符
只实现了__get__(),没有实现__set__()。示例:
class NameDes(object):
def __init__(self):
self.__name = None
def __get__(self, instance, owner):
print('get called')
return self.__name
class Person(object):
name = NameDes()
NameDes
只实现了__get__()方法,因此是一个非数据描述符类,只可获取值;name
是Person
的类属性,并且指向了NameDes()
实例,因此是一个非数据描述符。运行以下代码查看结果:
p = Person()
print(p.name) # 输出:get called、None
2.4 描述符注意事项
2.4.1 描述符必须是类属性
不管是数据描述符还是非数据描述符,描述符必须是类属性而非实例属性,验证如下:
class NameDes(object):
def __init__(self):
self.__name = None
def __get__(self,instance, owner):
print('__get__被调用')
return self.__name
def __set__(self, instance, value):
print('__set__被调用')
if isinstance(value, str):
self.__name = value
else:
raise TypeError('必须是字符串')
class Person(object):
def __init__(self):
self.name = NameDes()
p = Person()
print(p.name) # 输出:<__main__.NameDes object at 0x0000017E9BF60FA0>
可以看到,把实例属性定义为描述符时,无法调用__get__方法,__set__方法同理。
2.4.2 描述符用于实例对象
描述符虽然是通过类定义的,但是用于实例对象,而不直接用于类。示例如下:
class Str:
def __get__(self, instance, owner):
print('Str调用...')
def __set__(self, instance, value):
print('Str设置...')
def __delete__(self, instance):
print('Str删除...')
class People:
name = Str()
People.name # 输出:Str调用...
People.name='egon'
print(People.name) # 输出:egon
del People.name
2.4.3 描述符共享问题
执行以下代码,观察描述符被不同实例共享的结果。
class Score:
def __init__(self):
self._value = 0
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if 0 <= value <= 100:
self._value = value
else:
raise ValueError
class Student:
math = Score()
chinese = Score()
english = Score()
def __repr__(self):
return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.chinese, self.english)
std1 = Student()
print(std1) # 输出:<Student math:0, chinese:0, english:0>
std1.math = 85
print(std1) # 输出:<Student math:85, chinese:0, english:0>
std2 = Student()
print(std2) # 输出:<Student math:85, chinese:0, english:0>
上例中std1和std2是Student的两个实例,当std1修改了math描述符的值时,std2的math描述符的值也随之改变,即std2和std1共享了math描述符的值(属性chinese、english同理),其实这是不合理的,从需求实现上来说,不同学生实例的成绩都是独立的!
或许有人说,math、chinese、english三者作为Student类的类属性,本身就是被实例共享的!抛开这三者是描述符属性先不谈,从类属性原理讨论,这种说法也是不正确的!通常,修改类属性值是通过类名.类属性
方式,即使通过实例对象修改(尽管能实现但其实是不合理的)也只能通过实例对象._class_.类属性 = xxx
的方式,实例对象.类属性 = xxx
其实是定义了一个和类属性同名的实例变量。
回归math、chinese、english是三个描述符本身,描述符被视为一种特殊的类属性,顾名思义在于“描述”属性的访问、修改和删除行为,对相同属性访问逻辑进行统一封装,提高了代码复用性,使代码结构更加清晰。描述符虽为类属性但不局限于类属性访问限制,设计描述符时需要具体问题具体分析!从此例的需求出发,每个实例使用描述符时不应该互相影响,修正代码为:
class Score:
def __init__(self, subject):
self.name = subject
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if 0 <= value <= 100:
instance.__dict__[self.name] = value
else:
raise ValueError
class Student:
math = Score("math")
chinese = Score("chinese")
english = Score("english")
def __init__(self, math, chinese, english):
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.chinese, self.english)
std1 = Student(80, 80, 80)
print(std1) # 输出:<Student math:80, chinese:80, english:80>
std2 = Student(55, 66, 77)
print(std2) # 输出:<Student math:55, chinese:66, english:77>
3.属性访问原理
以往没接触描述符时,若NameDes不是一个描述符类,而是一个普通类,将会得到如下运行结果:
class NameDes(object):
def __init__(self):
self.__name = None
class Person(object):
name = NameDes()
p = Person()
print(p.__dict__) # 输出:{}
print(p.name) # 输出:<__main__.NameDes object at 0x0000021017DD1FD0>
p.name = "小李子"
print(p.__dict__) # 输出:{'name': '小李子'}
print(p.name) # 输出:小李子
name是Person类的类属性,Person类的实例p若无与类属性同名的实例属性,执行print(p.name)
时,由于实例p无实例属性name,因此向Person类获取类属性name的值;当执行p.name = "小李子"
将产生一个与类属性同名的实例属性,此后执行的print(p.name)
都将获取实例属性name的值。
若NameDes为2.3.1中定义的数据描述类,执行以下代码,却得到了如下结果:
print(Person.name) # 输出:get called、None
p = Person()
print(p.__dict__) # 输出:{}
print(p.name) # 输出:get called、None
p.name = "小李子" # 输出:set called
print(p.__dict__) # 输出:{}
print(Person.name) # 输出:get called、小李子
print(p.name) # 输出:get called、小李子
为什么第一个print(Person.name)
和第一个p.name
这段代码都没有返回类属性指向的对象,而是触发了类实例的__get__()方法?为什么p.name="小李子"
这段代码并没有产生一个和类属性同名的实例属性 name 且值为’小李子’,赋值操作竟作用在类属性上,还触发了类实例的__set__()方法?
在探究属性访问原理之前,先认识__getattribute__方法,它是属性访问的入口。
3.1__getattribute__与__getattr__
提到__getattribute__,不得不提到与之密切关联的__getattr__,两者均是属性访问拦截器,但有一定区别。官方文档说明如下:
object.__getattribute__(self, name)
Called unconditionally to implement attribute accesses for instances of the class. If the class also defines__getattr__(), the latter will not be called unless__getattribute__() either calls it explicitly or raises an AttributeError. This method should return the (computed) attribute value or raise an AttributeError exception. In order to avoid infinite recursion in this method, its implementation should always call the base class method with the same name to access any attributes it needs, for example, object.__getattribute__(self, name).
__getattribute__()会无条件地被调用于对类实例属性的访问。如果类还定义了_getattr_(),除非__getattribute__()显式地调用它或是引发了 AttributeError,否则它不会被调用。__getattribute__()应当返回(计算的)属性值或是引发一个 AttributeError 异常。为了避免此方法无限递归,方法中应调用基类__getattribute__()来访问所需任何属性,即object.__getattribute__(self,name)。也可调用super().__getattribute__(name)。
object.__getattr__(self, name)
Called when the default attribute access fails with an AttributeError (either getattribute() raises an AttributeError because name is not an instance attribute or an attribute in the class tree for self; or get() of a name property raises AttributeError). This method should either return the (computed) attribute value or raise an AttributeError exception. The object class itself does not provide this method.
__getattr__()在默认属性访问失败、触发 AttributeError 异常时被调用(可能是调用__getattribute__()时,name 即不是一个实例属性,也不是 self 的类继承树中的属性而引发的 AttributeError;或者是 property属性name中的__get__()引发的AttributeError)。此方法应当返回(计算的)属性值或是引发一个 AttributeError 异常。object类本身不提供这个方法。
换言之,__getattribute__和__getattr__都是 Python 中的内置函数,用于拦截对象的属性访问。特别的,__getattribute__在实例属性被访问时(不管属性是否存在)无条件被调用!当类中同时定义__getattribute__和__getattr__时,除非__getattribute__显示调用__getattr__方法或引发AttributeError异常,否则__getattr__方法不会被调用。
__getattribute__和__getattr__都是实例方法,因此通过类名.类属性
访问类属性时不会经过这两个方法;参数name
传入的参数是属性名,不是属性值;在 Python 中,广义的“属性”概念可以包含属性(即实例的字段)和 函数(即方法),“属性拦截器”中的“属性”是广义概念。
3.1.1 重写__getattribute__
因为Python中所有类默认继承object类,而object类提供了很多原始的内置属性和函数,所以自定义的类也会继承这些内置属性和函数。Python基类object中定义的__getattribute__方法对属性拦截后,若属性存在直接返回属性值,否则抛出AttributeError异常。
在开发过程中也可根据需要重写__getattribute__,应该要注意以下几点:
- __getattribute__仅在新式类中可用。
- 如果重写了__getattribute__,则类会调用重写的方法,所以这个方法必须要有返回值,没有返回值默认返回None。特别地,实例对象调用方法还需返回可调用对象,否则抛出异常。
class Student(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __getattribute__(self, attr):
print(f'You have access to properties: {attr}')
# return object.__getattribute__(self, attr)
def show(self):
print('show methods in class student is called!')
s= Student('Northxw',23)
print(s.name)
print(s.age)
s.show()
"""
运行结果:
You have access to properties: name
None
You have access to properties: age
None
You have access to properties: show
Traceback (most recent call last):
File "D:\main.py", line 18, in <module>
s.show()
TypeError: 'NoneType' object is not callable
"""
- 当在__getattribute__中执行属性访问操作即
self.name
或self.__dict__[name]
时,会再次触发__getattribute__的调用,为避免无限递归,方法中应调用基类的__getattribute__来访问何属性,即object.__getattribute__(self,xxx)
或super().__getattribute__(xxx)
。
class ClassA:
x = 'a'
def __getattribute__(self, item):
print('__getattribute__')
return self.item # 或 self.__dict__[name]
if __name__ == '__main__':
a = ClassA()
a.x
运行结果引发异常,提示达到最大递归深度
正确示例:
class ClassA:
x = 'a'
def __init__(self):
self.y = 'b'
def __getattribute__(self, item):
print("__getattribute__被调用...")
try:
return super().__getattribute__(item)
except AttributeError:
return '不存在属性{}'.format(item)
if __name__ == '__main__':
a = ClassA()
# 使用实例对象直接访问存在的类属性时,会调用__getattribute__方法
print(a.x) # 输出:__getattribute__被调用... 和 a
# 使用实例对象直接访问实例存在的实例属性时,会调用__getattribute__方法
print(a.y) # 输出:__getattribute__被调用... 和 b
# 通过类对象调用属性,不会执行__getattribute__方法
print(ClassA.x) # 输出:a
# 使用实例对象直接访问实例不存在的实例属性时,也会调用__getattribute__方法
print(a.z) # 输出:__getattribute__被调用... 和 不存在属性z
3.1.2 定义__getattr__
Python不存在默认的__getattr__方法。 默认情况下,访问不存在的属性无条件调用__getattribute__方法时会触发 AttributeError 异常。只有在类中显式定义了 __getattr__ 方法时,才会被调用来处理访问不存在的属性。
在开发过程中也可根据需要定义__getattr__,应该要注意以下几点:
- 确保访问不存在属性时能触发 AttributeError 异常或重写的__getattribute__显示调用__getattr__。
- 和__getattribute__一样,这个方法必须要有返回值,没有返回值默认返回None。特别地,实例对象调用不存在方法还需返回可调用对象,否则抛出异常。
正确示例:
class ClassA:
x = 'a'
def __init__(self):
self.y = 'b'
def show(self):
print('show methods in class student is called!')
def __getattr__(self, item):
if item != 'wow':
return '__getattr__'
elif item == 'wow':
return self.show
if __name__ == '__main__':
a = ClassA()
print(a.x) # 输出:a
# 使用实例对象直接访问实例存在的实例属性时,不会调用__getattr__方法
print(a.y) # 输出:b
# 使用实例对象直接访问实例不存在的实例属性或方法时,会调用__getattr__方法
print(a.z) # 输出:__getattr__
a.wow() # 输出:show methods in class student is called!
3.1.3 同时重写__getattribute__和定义__getattr__
在类中同时自定义__getattribute__方法 和 __getattr__方法,因为实例对象访问属性会无条件调用__getattribute__方法,所以不管属性是否存在,__getattribute__的优先级必然比__getattr__高。而且,要使得 __getattr__能够被调用,必须在__getattribute__方法中显式调用 __getattr__方法或__getattribute__方法中抛出AttributeError异常。示例如下:
class Person:
def __init__(self, name):
self.name = name
def __getattribute__(self, item):
print('__getattribute__被调用')
if item == 'name':
return object.__getattribute__(self, item)
else:
raise AttributeError()
def __getattr__(self, item):
print('__getattr__被调用')
return f'{item}属性不存在'
p = Person("Alice")
# 访问存在的属性
print(p.name) # 输出:__getattribute__被调用 和 Alice
# 访问不存在的属性
print(p.age) # 输出:__getattribute__ 被调用 和 __getattr__ 被调用 和 age属性不存在
使__getattr__能够被调用,上例是通过在__getattribute__方法中抛出AttributeError异常的方式,不建议通过在__getattribute__方法中显式调用 __getattr__方法,即使这没有语法问题。示例如下并观察运行结果:
class Person:
def __init__(self, name):
self.name = name
def __getattribute__(self, item):
print('__getattribute__ 被调用')
try:
# 尝试访问属性
return super().__getattribute__(item)
except AttributeError:
# 如果属性不存在,则调用 __getattr__
return self.__getattr__(item)
def __getattr__(self, item):
print('__getattr__ 被调用')
return f'{item} 属性不存在'
p = Person("Alice")
# 访问不存在的属性
print(p.age)
"""
运行结果:
__getattribute__ 被调用
__getattribute__ 被调用
__getattr__ 被调用
age 属性不存在
"""
可以看到,实例对象访问不存在的属性age时,“__getattribute__ 被调用”输出了两次!第一次是无条件调用__getattribute__方法的访问age属性的输出结果,第二次则是进入except AttributeError
代码块执行return self.__getattr__(item)
时访问__getattr__属性输出结果。这里的__getattr__方法的显式调用和__getattribute__抛出AttributeError异常程序自动调用__getattr__方法有着本质的不同!
针对__getattr__方法的显式调用,定义的__getattr__方法本身就是一个广义的属性,self.__getattr__(item)
的__getattr__不是因为__getattribute__抛出AttributeError异常(而且此处已经用except捕获了AttributeError异常即程序执行没发生AttributeError异常)而被调用的,而是被当作普通属性显示调用,而访问属性时会无条件调用__getattribute__方法,因此会再一次法输出“__getattribute__ 被调用”,然后进入return super().__getattribute__(item)
语句,由于访问的__getattr__属性存在进而完成相应逻辑执行。针对__getattribute__抛出AttributeError异常自动调用__getattr__方法,__getattr__是一个专门为属性不存在时设计的回调方法,它不会再次自动触发__getattribute__。
因此通常不建议在__getattribute__方法中显式调用__getattr__,原因总结如下,
3.2 属性访问顺序
因为__getattribute__在实例属性被访问时是无条件被调用,所以是属性访问的入口,内部实现了属性查找逻辑。__getattribute__是用C语言实现(源代码在Objects/object.c中,具体文件路径:Objects/object.c:PyObject_GenericGetAttr 和_PyObject_GenericGetAttrWithDict 的实现。https://github.com/python/cpython),主要逻辑在以下函数中:
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}
以下是_PyObject_GenericGetAttrWithDict 的简化逻辑:
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict, int suppress)
{
PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr = NULL;
descrgetfunc f;
// 1. 检查类及其父类中的描述符
descr = _PyType_Lookup(tp, name);
if (descr != NULL) {
f = Py_TYPE(descr)->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) {
// 如果是数据描述符,调用其 __get__ 方法
return f(descr, obj, (PyObject *)tp);
}
}
// 2. 检查实例属性
if (dict == NULL) {
dict = PyObject_GetAttrString(obj, "__dict__");
if (dict == NULL) {
return NULL;
}
}
PyObject *result = PyDict_GetItem(dict, name);
if (result != NULL) {
return result;
}
// 3. 如果找到非数据描述符,调用其 __get__ 方法
if (descr != NULL) {
f = Py_TYPE(descr)->tp_descr_get;
if (f != NULL) {
return f(descr, obj, (PyObject *)tp);
}
return descr;
}
// 4. 如果未找到属性,尝试调用 __getattr__
if (tp->tp_getattr != NULL) {
return tp->tp_getattr(obj, PyUnicode_AsUTF8(name));
}
// 5. 抛出 AttributeError
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
return NULL;
}
在Python中用一个例子来验证属性访问顺序,如下所示:
class Person:
age = 12
class Name:
def __get__(self,instance,owner):
return 'Perter'
class Age:
def __get__(self,instance,owner):
return None
class Student(Person):
name = Name()
age = Age()
def __init__(self, name):
self.name = name
def __getattribute__(self, attr):
print(f'__getattribute__被调用,访问属性: {attr}')
return object.__getattribute__(self, attr)
def __getattr__(self, attr):
return f'没有找到属性:{attr}'
a = Student('Bob')
print(a.age)
print(a.name)
Name.__set__ = lambda self, instance, value: None
print(a.name)
print(a.sex)
"""
__getattribute__被调用,访问属性: age
None
__getattribute__被调用,访问属性: name
Bob
__getattribute__被调用,访问属性: name
Perter
__getattribute__被调用,访问属性: sex
没有找到属性:sex
"""
实例对象a每一次访问属性时,都会进入__getattribute__方法,因为__getattribute__方法是属性访问的入口。对每次访问进行详细分析:
print(a.age)
:Student类中有非数据描述符age,Student类的父类Person中也有类属性age,根据运行结果可知,优先级 非数据描述符 > 父类的字典。
第一个print(a.name)
:Student类中有非数据描述符name,实例对象a中也有实例属性name,根据运行结果可知,优先级 实例对象的字典 > 非数据描述符。
Name.__set__ = lambda self, instance, value: None
第二个print(a.name)
:对Name类添加__set__方法后,Student类中的描述符name由非数据描述符变为数据描述符,此时Student类中有数据描述符name,实例对象a中也有实例属性name,根据运行结果可知,优先级 数据描述符 > 实例对象的字典。
print(a.sex)
:在以上代码的类和对象中俊不存在属性sex,触发触发 AttributeError 异常后自动调用__getattr__方法。
整理上述结论,实例对象访问属性的优先级从高到低为:__getattribute__方法 > 数据描述符 > 实例对象的字典 > 非数据描述符 > 父类的字典 > __getattr__方法。
由于描述符本身是一种特殊的类属性,无法通过示例探究同名描述符与类属性优先级关系,但已知实例对象的字典 > 类的字典 > 父类的字典。
Python中实例对象属性访问完整的优先级从高到低为:
- __getattribute__方法(属性访问入口)
- 数据描述符
- 实例对象的字典
- 类的字典
- 非数据描述符
- 父类的字典(遵循MRO)
- __getattr__方法
4.描述符的应用
4.1 属性验证
描述符可以用于验证对象的属性值类型是否符合规范。
需求背景:例如,对实例属性name和age进行类型限制,name只能是字符串类型,age只能是整数类型。如果用户输入值类型错误,程序报错终止。
设计思路:由于属性访问顺序:数据描述符>实例对象的字典,可以在类中定义与实例属性同名的描述符,实现对实例属性赋值验证。
class Des:
def __init__(self, key, expect_type):
self.key = key
self.expect_type = expect_type
def __get__(self, instance, owner):
print("这是__get__")
return instance.__dict__[self.key]
def __set__(self, instance, value):
print("__set__()方法")
if not isinstance(value, self.expect_type):
raise TypeError("'%s'输入的类型不正确,应该为%s" % (self.key, self.expect_type))
instance.__dict__[self.key] = value
def __delete__(self, instance):
print("这是__delete__")
instance.__dict__.pop(self.key)
class Peopel:
name = Des("name", str)
age = Des("age", int)
def __init__(self, name, age):
self.name = name
self.age = age
p = Peopel(100,18) #执行报错,输出:'name'输入的类型不正确,应该为<class 'str'
p = Peopel("baidu","18") #执行报错,输出:'age'输入的类型不正确,应该为<class 'int'>
p = Peopel("baidu", 18) #输出:__set__()方法 和 __set__()方法
print(p.__dict__) #输出:{'name': 'baidu', 'age': 18}
上次代码解决了每个实例属性验证问题,但是针对实例属性较多的情况会导致类中需要定义很多描述符,导致代码过长,结构不够清晰,可以使用类装饰器方式统一管理。
升级写法:
class Des:
def __init__(self, key, myExpect_type):
self.key = key
self.myExpect_type = myExpect_type
def __get__(self, instance, owner):
return instance.__dict__[self.key]
def __set__(self, instance, value):
if not isinstance(value, self.myExpect_type):
raise TypeError("%s输入的类型错误,应该为%s"%(self.key, self.myExpect_type))
instance.__dict__[self.key] = value
def __delete__(self, instance):
instance.__dict__.pop(self.key)
def decorate(**kwargs):
def wrapped(obj):
for key, val in kwargs.items():
setattr(obj, key, Des(key, val)) # name = Des("name",str)
return obj
return wrapped
@decorate(name = str, age = int)
class Peopel:
def __init__(self, name, age):
self.name = name
self.age = age
p = Peopel("baidu",18)
print(p.__dict__) # 输出:{'name': 'baidu', 'age': 18}
这里decorate函数装饰了Peopel类,并传入Peopel类的每个实例属性需要限制的类型。自定义的decorate函数中定义了一个内嵌的wrapped函数,此内嵌函数接收Peopel类对象为实参(obj为形参),遍历decorate函数的参数字典执行的setattr(obj, key, Des(key, val))
等价于为Peopel类逐个定义描述符key=Des(key, val)
。
4.2 懒加载和缓存
描述符非常适合用来实现懒加载和缓存机制。例如,计算复杂的数据或需要访问远程资源时,可以用描述符来延迟计算直到真正需要时,且还可保存此次计算的结果下次访问时无需重复计算。
需求背景:例如,圆类实例对象每次获取面积时都需要调用计算面积的实例方法area,这会造成重复计算。
设计思路:由于属性访问顺序:实例对象的字典>非数据描述符,可以将area方法转变为非数据描述符,此时调用area方法变为访问area属性,访问area属性时将首次计算的结果保存在描述符对象的__dict__中。
import math
class lazyproperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls):
if instance is None:
return self
else:
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
class Circle:
def __init__(self, radius):
self.radius = radius
@lazyproperty
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
circle = Circle(3)
print(circle.area)
"""
输出:
Computing area
28.274333882308138
"""
4.3 只读属性
通过定义包含__get__方法、set 方法,但__set__ 方法无差别抛出异常,可以创建只读属性。沿用上例4.2中懒加载和缓存的案例,计算圆面积的圆周率是一个客观的、固定的数值,不允许修改。除了上述直接引用math模块中提供的圆周率常量,我们还可以自己定义。
class lazyproperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls):
if instance is None:
return self
else:
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
class ReadonlyNumber:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
raise AttributeError("Cannot modify a read-only attribute")
class Circle:
pi = ReadonlyNumber(3.14)
def __init__(self, radius):
self.radius = radius
@lazyproperty
def area(self):
print('Computing area')
return self.pi * self.radius ** 2
circle = Circle(3)
print(circle.area)
circle.pi = 5
"""
输出:
Computing area
28.26
AttributeError: Cannot modify a read-only attribute
"""
拓展
1. property原理及Python实现
由于property核心实现是用C语言完成,Python文档仅解释其用法、定义,并没有具体的实现逻辑,描述如下:
class property(object):
"""
Property attribute.
fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring
Typical use is to define a managed attribute x:
class C(object):
def getx(self): return self._x
def setx(self, value): self._x = value
def delx(self): del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
Decorators make defining new properties or modifying existing ones easy:
class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
"""
def deleter(self, *args, **kwargs): # real signature unknown
""" Descriptor to change the deleter on a property. """
pass
def getter(self, *args, **kwargs): # real signature unknown
""" Descriptor to change the getter on a property. """
pass
def setter(self, *args, **kwargs): # real signature unknown
""" Descriptor to change the setter on a property. """
pass
def __delete__(self, *args, **kwargs): # real signature unknown
""" Delete an attribute of instance. """
pass
def __getattribute__(self, *args, **kwargs): # real signature unknown
""" Return getattr(self, name). """
pass
def __get__(self, *args, **kwargs): # real signature unknown
""" Return an attribute of instance, which is of type owner. """
pass
def __init__(self, fget=None, fset=None, fdel=None, doc=None): # known special case of property.__init__
"""
Property attribute.
fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring
Typical use is to define a managed attribute x:
class C(object):
def getx(self): return self._x
def setx(self, value): self._x = value
def delx(self): del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
Decorators make defining new properties or modifying existing ones easy:
class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
# (copied from class doc)
"""
pass
@staticmethod # known case of __new__
def __new__(*args, **kwargs): # real signature unknown
""" Create and return a new object. See help(type) for accurate signature. """
pass
def __set__(self, *args, **kwargs): # real signature unknown
""" Set an attribute of instance to value. """
pass
fdel = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
fget = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
fset = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
__isabstractmethod__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
根据以上描述,自行实现 property ,那它将会是类而不是函数。结合描述符协议,模仿其函数结构,用Python实现具有property特性的TestProperty。
class TestProperty(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
print("in __get__")
if obj is None:
return self
if self.fget is None:
raise AttributeError
return self.fget(obj)
def __set__(self, obj, value):
print("in __set__")
if self.fset is None:
raise AttributeError
self.fset(obj, value)
def __delete__(self, obj):
print("in __delete__")
if self.fdel is None:
raise AttributeError
self.fdel(obj)
def getter(self, fget):
print("in getter")
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
print("in setter")
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
print("in deleter")
return type(self)(self.fget, self.fset, fdel, self.__doc__)
注:在getter、setter、deleter方法中使用type(self)(...)
,因为考虑到 proptery 可能被继承。
通过装饰器方式应用自定义TestProperty,为了方便理解代码实现的过程及原理使用不同颜色标注各方法,示例如下:
class C(object):
@TestProperty
def x(self):
print("I'm the 'x' property.")
return self._x
@x.setter
def x(self, value):
print('setter called')
self._x = value
@x.deleter
def x(self):
print('deleter called')
del self._x
把上述代码改写成等价表示形式,各方法颜色和上述一致,注意相同颜色之处,理解它们的关系:
class C(object):
x=TestProperty(x)
x=x.setter(x)
x=x.deleter(x)
代码解读:
方法x使用TestProperty装饰后,即创建了TestProperty 类的一个实例x;实例x调用setter方法装饰方法x,又创建了TestProperty 类的一个实例x;实例x调用deleter方法装饰方法x,再创建了TestProperty 类的一个实例x。最终实现的结果等价于x=TestProperty(x,x,x)。
此例也从某种角度解释了上述规定。必须先要有TestProperty 类的实例,才能调用setter或deleter方法;依次对同一个变量x赋不同值,逐步得到最终具有获取、赋值、删除功能的描述符。
运行以下代码,观察结果:
c = C()
c.x = 20 # 输出:setter called
c.x # 输出:I'm the 'x' property.
del c.x # 输出:deleter called
2. @property与@xxx.getter的关系
@property装饰器用于将一个方法转换为属性的getter方法。通过使用 @property 装饰器,原本的普通方法就变成了属性访问器,之后可以通过访问属性的方式来调用这个方法。@property本身具有修饰类中普通访问方法的功能,这是否意味着@property搭配getter方法的装饰器是多此一举呢?实则不然!
@property.getter 是用于给某个通过@property装饰的方法定义 getter。通常情况下,我们会在属性已经定义了getter后,通过 @xxx.getter来明确指定它的getter。
个人理解,@property是类似属性getter的初始化,@xxx.getter是属性getter的再定义。
有如下示例:
class MyClass:
def __init__(self, value):
self._value = value
# 使用 @property 装饰器将 get_value 方法变成 getter
@property
def value(self):
return self._value
# 使用 @value.setter 装饰器来定义 setter 方法
@value.setter
def value(self, value):
self._value = value
# 使用 @value.getter 装饰器为 value 属性定义 getter
@value.getter
def value(self):
print("Getting value")
return self._value
obj = MyClass(10)
print(obj.value) # 输出 "Getting value" 然后是 10
obj.value = 20 # 使用 setter 修改 value
print(obj.value) # 输出 "Getting value" 然后是 20
4. 使用描述器实现@staticmethod,@classmethod
模拟实现@staticmethod,自定义 StaticMethod 描述符类。
class StaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
print("in staticmethod __get__")
return self.func
class MyClass:
@StaticMethod
def my_static_method():
print("This is a static method")
MyClass.my_static_method()
"""
运行结果:
in staticmethod __get__
This is a static method
"""
MyClass类中原始的my_static_method方法被StaticMethod描述符类装饰等价于my_static_method = StaticMethod(my_static_method)
,此时my_static_method成为了一个描述符,MyClass.my_static_method
会自动调用__get__方法。由于静态方法不需要和类的实例绑定,因此不管是通过类还是实例去访问,都直接返回原始my_static_method方法。最后通过()
调用输出此方法运行结果。
模拟实现@classmethod,自定义 StaticMethod 描述符类。
class ClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
print("in classmethod __get__")
def wrapper(*args, **kwargs):
return self.func(owner, *args, **kwargs)
return wrapper
class Test:
@ClassMethod
def myfunc(cls):
print("hello")
Test.myfunc()
"""
运行结果:
in classmethod __get__
hello
"""
Test类中原始的myfunc方法被ClassMethod描述符类装饰等价于myfunc = ClassMethod(myfunc)
,此时myfunc成为了一个描述符,Test.myfunc
会自动调用__get__方法。由于类方法的第一个参数会自动绑定为类对象本身(约定俗成用 cls 表示)且可能还有别的参数,因此在__get__方中需要定义的一个内部方法 wrapper 接收这些别的参数,并在wrapper返回原始的myfunc方法并传入对应位置的参数,而在外部的__get__中返回wrapper方法。Test.myfunc
实际得到的是wrapper方法,通过()
调用返回wrapper方法的运行结果。
5. 思考__getattribute__和__setattr__、__get__和__set__的关系
除了__getattribute__,还有__setattr__和__delattr__方法,其功能类似描述符的__set__和__delete__。在此了解一下__setattr__方法,__delattr__不做赘述,官方文档说明如下:
object.setattr(self, name, value)
Called when an attribute assignment is attempted. This is called instead of the normal mechanism (i.e. store the value in the instance dictionary). name is the attribute name, value is the value to be assigned to it.
If __setattr__() wants to assign to an instance attribute, it should call the base class method with the same name, for example, object.__setattr__(self, name, value).
For certain sensitive attribute assignments, raises an auditing event object.__setattr__ with arguments obj, name, value.
此方法在一个属性被尝试赋值时被调用。这会代替正常的机制(即将值存储在实例字典中)。name 为属性名称 , value 为要赋给属性的值。
如果__setattr__() 想要为实例属性赋值,它应该调用同名的基类方法,例如object.__setattr__(self,name, value)
对于某些敏感的属性赋值,Python 会触发审计事件 object.__setattr__,该事件会传递 obj(对象)、name(属性名)和 value(赋的值)作为参数。
__setattr__方法会在每次给实例对象的属性赋值时自动被调用,通常用于数据验证、动态属性管理、缓存机制等场景。此方法是Python的内置方法,重写时需要注意 递归问题:在__setattr__方法内部不能直接修改实例属性,即self.name = value
,因为这会触发再次调用__setattr__,导致无限递归。为避免无限递归有两种方式,一是方法中应调用基类的__setattr__来设置属性,即object.__setattr__(self, name, value)
或super().__setattr__(name, value)
;二是转换成对实例对象的__dict__中键值对修改。
class MyClass:
def __setattr__(self, name, value):
print(f"Setting {name} to {value}")
super().__setattr__(name, value) # 或 self.__dict__[name]= value
obj = MyClass()
obj.some_attribute = 10 # 输出: Setting some_attribute to 10
※ 为什么
self.__dict__[name]
会调用__getattribute__,而self.__dict__[name] = value
不会调用__setattr__?
__getattribute__是 Python 中实例对象所有属性访问的入口方法,而__dict__本身也是实例对象的属性之一,必然会再次调用__getattribute__。
__setattr__是 Python 中实例对象所有属性赋值的入口方法,但只有对属性直接赋值(即self.name = value
方式)时才会调用此方法。self.__dict__[name] = value
并不是直接操作__dict__,而是操作__dict__里键值对的值,注意区别self.__dict__ = value
和self.__dict__[name] = value
。
※ 当同时重写__setattr__与__getattribute__时,为什么__setattr__中
super().__setattr__(name, value)
涉及属性访问赋值也不会调用__getattribute__,但self.__dict__[name]= value
会?class MyClass: def __setattr__(self, name, value): print(f"Setting {name} to {value}") object.__setattr__(self, name, value) def __getattribute__(self, name): print(f'You have access to properties: {name}') return object.__getattribute__(self, name) obj = MyClass() obj.some_attribute = 10 # 输出: Setting some_attribute to 10
class MyClass: def __setattr__(self, name, value): print(f"Setting {name} to {value}") super().__setattr__(name, value) def __getattribute__(self, name): print(f'You have access to properties: {name}') return object.__getattribute__(self, name) obj = MyClass() obj.some_attribute = 10 # 输出: Setting some_attribute to 10 和 You have access to properties: __dict__
这是因为object.__setattr__是 Python 解释器的内置方法,它的实现是用 C 语言编写的(在 CPython 中),因此它可以直接访问对象的内部结构(如__dict__),而不需要通过__getattribute__。另外,如果 object.__setattr__通过__getattribute__访问__dict__,那么会导致无限递归。
回归正题,可以发现__getattribute__、__setattr__这对与__get__、__set__这对有很多相似之处。既然都是对实例对象访问或修改属性的控制管理,为什么不直接使用类的__getattribute__、__setattr__实现属性限制,还要衍生描述符并通过__get__、__set__实现呢?
这是因为它们的设计目的和使用场景是不同的。__getattribute__、__setattr__是对所有属性访问和赋值控制方法,对单个属性进行精细化管理有可能对其他的属性的访问设置造成影响。而__get__、__set__可以对单个属性进行精细化管理,可以针对不同的属性实现不同的行为。
总之,__getattribute__和__setattr__确实可以用来控制属性的访问和修改,但它们是全局性的,适用于对整个实例的属性进行统一管理。而描述符(__get__和__set__)为特定属性提供了更细粒度的控制,使得属性管理更加模块化、灵活、可复用。在实际应用中,描述符提供了更好的解决方案,尤其是在需要细致控制属性行为、增强代码可读性和复用性的场景下。
参考:
Python中 property 的实现原理及实现纯 Python 版
手把手教你 Python 描述符,没学会你打我
属性、property和属性描述符
一篇文章让你彻底搞清楚Python中self的含义
__getattribute__和数据描述符-可删除或替换
作者:揪。