୧ʕ •ᴥ•ʔ୨ Silent Lake

Python 之“无法访问”的属性

Published: | Updated: | 700 words | 4 mins

最近碰到一个 Python 中的奇怪现象:明明在类定义里有着一个属性,但是在实例化后的对象上却无法访问到它,会抛出 AttributeError

虽然 root cause 很简单,但是我觉得这个案例可以作为 debug 的例子,于是就有了这篇文章。

示例代码

 1class Parent:
 2    _fields = {}
 3
 4    def __getattr__(self, name):
 5        try:
 6            return self._fields[name]
 7        except KeyError:
 8            raise AttributeError(
 9                f"'{self.__class__.__name__}' object has no attribute '{name}'",
10            )
11
12
13class Unrelated:
14    @property
15    def name(self):
16        return "Unrelated"
17
18
19class Child(Unrelated, Parent):
20    @property
21    def name(self):
22        # access some variable in other module
23        return OtherModule.name
24
25
26child = Child()
27print("name" in dir(child)) # True
28print(child.name) # AttributeError: 'Child' object has no attribute 'name'

问题描述

问题出现在访问 Child 实例属性 name 时。虽然 "name" in dir(child) 返回 True,表明属性 name 存在,但是直接访问 child.name 却抛出了 AttributeError

这乍一看上去很奇怪,明明属性存在却无法访问,这是为什么呢?

分析过程

要分析这个问题,首先我们需要对 Python 中属性查找的过程有一定了解。在 Python 中,当我们使用形如 instance.attribute 来访问实例属性时,会先在该实例对象上查找相应的属性,如果找不到就会抛出 AttributeError。而当我们给类加上了 __getattr__ 方法时,Python 就会在找不到属性时调用这个方法

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.

当我们执行示例代码中的 print(child.name) 这一行时,会有以下 traceback 信息抛出:

 1---------------------------------------------------------------------------
 2KeyError                                  Traceback (most recent call last)
 3Input In [2], in Parent.__getattr__(self, name)
 4      5 try:
 5----> 6     return self._fields[name]
 6      7 except KeyError:
 7
 8KeyError: 'name'
 9
10During handling of the above exception, another exception occurred:
11
12AttributeError                            Traceback (most recent call last)
13Input In [4], in <cell line: 0>()
14----> 1 c.name
15
16Input In [2], in Parent.__getattr__(self, name)
17      6     return self._fields[name]
18      7 except KeyError:
19----> 8     raise AttributeError(
20      9         f"'{self.__class__.__name__}' object has no attribute '{name}'",
21     10     )
22
23AttributeError: 'Child' object has no attribute 'name'
24> <ipython-input-2-b3781a93b20e>(8)__getattr__()
25      6             return self._fields[name]
26      7         except KeyError:
27----> 8             raise AttributeError(
28      9                 f"'{self.__class__.__name__}' object has no attribute '{name}'",
29     10             )

从 traceback 信息中可以看到,child.name 这一次属性访问调用了父类 Parent__getattr__ 方法。但是在子类 Child 的定义中,name 这个属性是明显存在的,它的值是 OtherModulename 属性。由于 __getattr__ 只有在属性访问抛出 AttributeError 时才会被调用,那么问题根源应该就是 OtherModule.name 抛出了 AttributeError,才会调用到 __getattr__

事实上也是如此,return OtherModule.name 这一行抛出了 AttributeError,属性访问的行为 fallback 到了 __getattr__

调试方法

接下来说一下有什么方法可以用来调试这个问题。调试的目的就是为了弄清楚程序逻辑上有什么问题,通常来说有以下几种方法:

  1. 打断点,使用 pdb 进行单步调试
  2. 日志,使用二分法,找到错误的位置

打断点

打断点就是在程序代码中插入 breakpoint(),当程序执行到这行代码时,程序就会停止运行,可以和当前内存中的数据进行交互,方便定位问题。如果是在 REPL 里,也可以通过 import pdb; pdb.run('your code here') 开始单步调试。

IPython 为例,进入 REPL 后执行以下代码来进入单步调试:

 1In [1]: import pdb
 2
 3In [2]: class OtherModule:
 4   ...:     pass
 5   ...:
 6   ...: # Parent/Child class definitions
 7
 8In [3]: c = Child()
 9
10In [4]: pdb.run('c.name')
11> <string>(1)<module>()

然后输入 s 回车来跳进 <module>() 里面,输入 n 回车来执行下一行,按 q 结束调试。

 1In [4]: pdb.run('c.name')
 2> <string>(1)<module>()
 3(Pdb) s
 4--Call--
 5> <ipython-input-2-20572dc1ce97>(24)name()
 6-> @property
 7(Pdb) n
 8> <ipython-input-2-20572dc1ce97>(27)name()
 9-> return OtherModule.name
10(Pdb) n
11AttributeError: type object 'OtherModule' has no attribute 'name'
12> <ipython-input-2-20572dc1ce97>(27)name()
13-> return OtherModule.name
14(Pdb) n
15--Return--
16> <ipython-input-2-20572dc1ce97>(27)name()->None
17-> return OtherModule.name
18(Pdb)
19--Call--
20> <ipython-input-2-20572dc1ce97>(8)__getattr__()
21-> def __getattr__(self, name):
22(Pdb)
23> <ipython-input-2-20572dc1ce97>(9)__getattr__()
24-> try:
25(Pdb) q

你也可以在上面第 11 行后输入 p dir(OtherModule) 来查看 OtherModule 对象的属性列表。

 1(Pdb) n
 2AttributeError: type object 'OtherModule' has no attribute 'name'
 3> <ipython-input-2-20572dc1ce97>(27)name()
 4-> return OtherModule.name
 5(Pdb) p dir(OtherModule)
 6['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
 7'__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__',
 8'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
 9'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
10'__sizeof__', '__str__', '__subclasshook__', '__weakref__']
11(Pdb)

用途

看到这里你可能想问,__getattr__ 这个方法有什么用处?接下来就举个例子,用它来实现 wrapper 类。

 1class Wrapped:
 2    def method1(self):
 3        print("Called method1")
 4
 5    def method2(self):
 6        print("Called method2")
 7
 8
 9class Wrapper:
10    def __init__(self, wrapped):
11        self._wrapped = wrapped
12
13    def __getattr__(self, name):
14        print(f"Calling {name}")
15        return getattr(self._wrapped, name)
16
17
18w = Wrapper(Wrapped())
19w.method1()
20# Calling method1
21# Called method1

就像这样,可以很方便地在不修改原来对象的情况下实现对原来对象的方法调用的监控,比如可以加上日志,统计调用量或者加上额外处理等。

Bonus

在 Python Data Model 文档里还有这样一个 magic method: __getattribute__。它会在属性访问时无条件调用。

object.__getattribute__(self, name)

Called unconditionally to implement attribute accesses for instances of the class.

我们知道在 Python 里并没有事实上的私有变量,只有约定上的私有属性。其实借助 __getattribute__ 方法,我们就可以把约定上的私有属性变为“事实上”的私有属性。

Bonus
 1class Base:
 2
 3    def __init__(self):
 4        self.public_attr = 0
 5        self._private_attr = 1
 6
 7    def __getattribute__(self, name):
 8        if name.startswith("_") and not name.startswith("__"):
 9            if type(self) is not Base:
10                raise AttributeError(f"Unaccessible private attribute: {name}")
11        val = super().__getattribute__(name)
12        if name == "__dict__" and type(self) is not Base:
13            val = {k: v for k, v in val.items() if not k.startswith("_")}
14        return val
15
16
17class Sub(Base):
18    pass
19
20
21b = Base()
22print(b.public_attr)    # 0
23print(b._private_attr)  # 1
24s = Sub()
25print(s.public_attr)    # 0
26print(s._private_attr)  # AttributeError: Unaccessible private attribute: _private_attr

#python   #Debugging Cases  

Reply to this post by email ↪