最近碰到一个 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 anAttributeError
because name is not an instance attribute or an attribute in the class tree forself
; or__get__()
of a name property raisesAttributeError
). This method should either return the (computed) attribute value or raise anAttributeError
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
这个属性是明显存在的,它的值是 OtherModule
的 name
属性。由于 __getattr__
只有在属性访问抛出 AttributeError
时才会被调用,那么问题根源应该就是 OtherModule.name
抛出了 AttributeError
,才会调用到 __getattr__
。
事实上也是如此,return OtherModule.name
这一行抛出了 AttributeError
,属性访问的行为 fallback 到了 __getattr__
。
调试方法
接下来说一下有什么方法可以用来调试这个问题。调试的目的就是为了弄清楚程序逻辑上有什么问题,通常来说有以下几种方法:
打断点
打断点就是在程序代码中插入 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