python属性查找与属性赋值

在之前文章中写了一节同名的类和实例变量,如果存在同名的类和实例变量的话,属性查找会优先选择实例变量,而描述器会改变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

可以看到,代码正常运行了。

下面关于属性查找和属性赋值的顺序总结一下:

属性赋值

  1. 如果Clz定义了__setattr__方法,那么调用该方法,否则
  2. 如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__set__方法, 否则
  3. 等价调用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__() 的钩子将被绕开。


参考:

  1. python descriptor 详解,https://www.cnblogs.com/xybaby/p/6266686.html
  2. python属性查找 深入理解(attribute lookup),https://www.cnblogs.com/xybaby/p/6270551.html
  3. 描述器使用指南,https://docs.python.org/zh-cn/3/howto/descriptor.html#invocation-from-an-instance
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值