在之前文章中写了一节同名的类和实例变量,如果存在同名的类和实例变量的话,属性查找会优先选择实例变量,而描述器会改变python默认的属性查找行为。如果一个类属性是描述器的话,使用实例调用a.attr1这种形式将只会访问类属性,而不会像之前那样访问实例变量,这是怎么回事,通过一个例子看一下:
class Desc1:
def __get__(self, obj, objtype=None):
print('__get__ age')
return self.age
def __set__(self, obj, value):
print('__set__ age')
self.age = value
class A:
age = Desc1()
def __init__(self, age) -> None:
self.age = age
# def __setattr__(self, name: str, value) -> None:
# self.__dict__[name] = value
# print('A __setattr__', name, self.__dict__[name])
a1 = A(1)
a2 = A(2)
print('a1 age:', a1.age)
print('a2 age:', a2.age)
执行以上代码输出:
__set__ age
__set__ age
__get__ age
a1 age: 2
__get__ age
a2 age: 2
可以看到,a1的age也变成了2,程序访问的是类变量。
python中的属性赋值会受到descriptor(确切说是data descriptor)的影响,同时也会受到__setattr__
函数的影响。python内置函数中还有一个setattr,setattr(x, 'foobar', 123)
等价于 x.foobar = 123
,属于属性赋值的操作,与descriptor和__setattr__
不在一个层级。__setattr__
的说明如下:
object.``__setattr__
(self, name, value)此方法在一个属性被尝试赋值时被调用。这个调用会取代正常机制(即将值保存到实例字典)。 name 为属性名称, value 为要赋给属性的值。
可以看到__setattr__
会取代正常的属性赋值机制,我们将它激活再看,将上面注释的代码放开:
class A:
age = Desc1()
def __init__(self, age) -> None:
self.age = age
def __setattr__(self, name: str, value) -> None:
self.__dict__[name] = value
print('A __setattr__', name, self.__dict__[name])
运行代码,报错了:
A __setattr__ age 1
A __setattr__ age 2
__get__ age
Traceback (most recent call last):
File "d:\code\python_programs\demos\py_class_demos\descriptor_demo1.py", line 25, in <module>
print('a1 age:', a1.age)
File "d:\code\python_programs\demos\py_class_demos\descriptor_demo1.py", line 5, in __get__
return self.age
AttributeError: 'Desc1' object has no attribute 'age'
错误提示Desc1对象没有属性age,但同时也可以看到__setattr__
生效了.
说明在a1.age获取age的时候调用的还是类属性(描述器),我们存值的时候存入的是实例字典,描述符中应当是没有age的,所以报错。
如此属性age的set和get操作就不对等了,set存入的是实例字典,而get则去描述器中找去了,如何是好。
在python描述符指南中,给出了答案:
实例查找通过命名空间链进行扫描,数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是
__getattr__()
(如果存在的话)。
“数据描述器的优先级最高”,这就是程序会优先去描述器中寻找age的原因,修改代码,将__set__
方法注释掉:
class Desc1:
def __get__(self, obj, objtype=None):
print('__get__ age')
return self.age
# def __set__(self, obj, value):
# print('__set__ age')
# self.age = value
运行代码输出:
A __setattr__ age 1
A __setattr__ age 2
a1 age: 1
a2 age: 2
可以看到,代码正常运行了。
下面关于属性查找和属性赋值的顺序总结一下:
属性赋值
- 如果Clz定义了__setattr__方法,那么调用该方法,否则
- 如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__set__方法, 否则
- 等价调用obj.__dict__[‘attr’] = var
属性查找
实例查找通过命名空间链进行扫描,数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是 __getattr__()
(如果存在的话)。
点运算符的查找逻辑在 object.__getattribute__()
中。这里是一个等价的纯 Python 实现:
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
有趣的是,属性查找不会直接调用 object.__getattribute__()
,点运算符和 getattr()
函数均通过辅助函数执行属性查找:
def getattr_hook(obj, name):
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name) # __getattr__
因此,如果 __getattr__()
存在,则只要 __getattribute__()
引发 AttributeError
(直接引发异常或在描述符调用中引发都一样),就会调用它。
同时,如果用户直接调用 object.__getattribute__()
,则 __getattr__()
的钩子将被绕开。
参考:
- python descriptor 详解,https://www.cnblogs.com/xybaby/p/6266686.html
- python属性查找 深入理解(attribute lookup),https://www.cnblogs.com/xybaby/p/6270551.html
- 描述器使用指南,https://docs.python.org/zh-cn/3/howto/descriptor.html#invocation-from-an-instance