Python语言为类编程提供了三个特殊方法可以很好控制属性的访问、生成控制,这个三个特殊方法分别为:__getattr____setattr____getattribute__。其中前两个特殊方法分别对应内建函数(built-in function):getattrsetattr

@property和描述符等这些Python特性都需要事先建立其对属性的处理逻辑,但这三个特殊方法可以按需生成满足开发者需求的属性。接下来我们一一分析。

__getattr__ 特殊方法

如果一个类定义了__getattr__,那么当系统在该类的实例字典中找不到要查询的属性时,就会调用这个特殊方法。我们可以使用这种特性来按需动态生成需要的属性。

1
2
3
4
5
6
7
8
9
class LazyDB:

def __init__(self):
self.db = None

def __getattr__(self, attr):
value = 'attr for %s' % attr
setattr(self, name, value)
return value

我们在交互式下使用上面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> db = LazyDB()
>>> db.__dict__
{'db': None}
>>> db.foo
'attr for foo'
>>> db.bar
'attr for bar'
>>> db.__dict__
{'bar': 'attr for bar', 'foo': 'attr for foo', 'db': None}
>>> db.bar
'attr for bar'
>>> db.__dict__
{'bar': 'attr for bar', 'foo': 'attr for foo', 'db': None}
>>>

对于一开始缺失的属性,Python的__getattr__特殊方法会为其创建,然后添加到类实例字典__dict__中,以达到惰性创建属性。有时候,可以利用这个特性为缺失的属性提供默认值很方便。

接下来我们为这个简单的LazyDB添加日志记录功能。把程序对__getattr__的调用行为记录下来。

1
2
3
4
5
class LoggingLazyDB(LazyDB):

def __getattr__(self, attr):
print('logging: called __getattr__(%s)' %attr)
super().__getattr__(attr)

有一个细节需要注意:为了避免LoggingLazyDB在被属性访问时,__getattr__被无线递归调用,记得调用父类的__getattr__方法。

在交互式下演示该类。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> l = LoggingLazyDB()
>>> l.bar
logging: called __getattr__(bar)
'attr for bar'
>>> l.foo
logging: called __getattr__(foo)
'attr for foo'
>>> l.__dict__
{'bar': 'attr for bar', 'db': None, 'foo': 'attr for foo'}
>>> l.bar
'attr for bar'
>>> l.foo
'attr for foo'

第一次访问barfoo属性时有日志记录,因为调用了LoggingLazyDB类实例的特殊方法__getattr__,同时父类创建属性并添加到字典中。于是,下次访问barfoo属性时只需要访问字典即可。

但是,在某些场景下,我们需要属性每次被访问的情况,对它做记录,而不是仅仅在调用__getattr__方法时才记录。比如,我们还要在数据库系统中实现事务处理。用户下次访问某个属性时,我们需要确认数据库中对于的行是否依然有效,以及相关事务是否依然处于开启状态。

为了解决这个问题,需要特殊方法__getattribute__

__getattribute__ 特殊方法

如果一个类实现了特殊方法__getattribute__,那么该类的实例每次有属性访问时,Python的类系统都会调用这个特殊方法,即使该类实例的属性字典里已经有被访问的属性。

这个特性我们可以用来监控属性的访问情况、动态更新属性状态等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TimingDB:

def __init__(self):
self.db = 'db'

def __getattribute__(self, attr):
print('logging: call __getattribute__(%s) at %s' % (attr, time.ctime()))
try:
value = super().__getattribute__(attr)
print('attribute in __dict__ return here')
return value
except AttributeError:
value = 'value created at %s' % time.ctime()
setattr(self, attr, value)
return value

在交互式下使用该类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> t = TimingDB()
>>> t.db
logging: call __getattribute__(db) at Thu Jan 18 08:22:49 2018
attribute in __dict__ return here
'db'
>>> t.foo
logging: call __getattribute__(foo) at Thu Jan 18 08:22:54 2018
'value created at Thu Jan 18 08:22:54 2018'
>>> t.foo
logging: call __getattribute__(foo) at Thu Jan 18 08:22:57 2018
attribute in __dict__ return here
'value created at Thu Jan 18 08:22:54 2018'
>>> t.__dict__
logging: call __getattribute__(__dict__) at Thu Jan 18 08:23:03 2018
attribute in __dict__ return here
{'foo': 'value created at Thu Jan 18 08:22:54 2018', 'db': 'db'}

如果程序访问一个在__dict__里没有的属性super().__getattribute__(attr)该代码会抛出AttributeError,然后由捕获该异常的代码段创建属性并添加到__dict__中(有setattr)负责。否则,程序直接返回该属性而不抛出AttributeError异常。

如果一个类实现了__getattribute__方法,程序每次调用hasattrgetattr内建函数时都会调用该特殊方法。

1
2
3
4
5
6
7
8
9
10
11
>>> t = TimingDB()
>>> t.__dict__
logging: call __getattribute__(__dict__) at Thu Jan 18 08:30:43 2018
attribute in __dict__ return here
{'db': 'db'}
>>> hasattr(t, 'abc')
logging: call __getattribute__(abc) at Thu Jan 18 08:30:50 2018
True
>>> getattr(t, 'feng')
logging: call __getattribute__(feng) at Thu Jan 18 08:31:05 2018
'value created at Thu Jan 18 08:31:05 2018'

这里要注意__getattribute____getattr__特殊方法的区别:后者只会在访问的属性缺失时触发,前者则是每次访问属性时触发。

__setattr__ 特殊方法

__setattr__特殊方法可以拦截对属性的赋值操作。只要对实例属性进行赋值,不论是通过object.attr = value还是使用内建函数setattr(object, attr, value)都会出发__setattr__特殊方法。

1
2
3
4
5
class SavingDB:

def __setattr__(self, attr, value):
print('save (%s, %s) at %s' % (attr, value, time.ctime()))
super().__setattr__(attr, value)

在交互式下演示代码:

1
2
3
4
5
6
7
8
>>> s = SavingDB()
>>> s.bar = 'foo'
save (bar, foo) at Thu Jan 18 08:39:11 2018
>>> s.bar
'foo'
>>> s.__dict__
{'bar': 'foo'}
>>>

和上面__getattr__的思路类似,我们为SavingDB添加日志功能。

1
2
3
4
5
class LoggingSavingDB(SavingDB):

def __setattr__(self, attr, value):
print('logging: called __setattr__(%s, %s)' % (attr, value))
super().__setattr__(attr, value)

在交互式下演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> l = LoggingSavingDB()
>>> l.__dict__
{}
>>> l.foo = 'bar'
logging: called __setattr__(foo, bar)
save (foo, bar) at Thu Jan 18 08:43:53 2018
>>> l.__dict__
{'foo': 'bar'}
>>> l.foo = 'bar'*2
logging: called __setattr__(foo, barbar)
save (foo, barbar) at Thu Jan 18 08:44:09 2018
>>> l.__dict__
{'foo': 'barbar'}
>>>

因此发现,__setattr__的行为和__getattribute__很像,都会在访问对象(读取或设置)时别调用。

处理递归问题

在使用__getattr____setattr____getattribute__这三个特殊方式时经常会已到递归问题,导致代码无法正常执行,而是栈异常而使程序退出。这个通过一个例子来讲述如何避免使用特殊方法时遇到的无线递归问题。

1
2
3
4
5
6
7
8
class RecursionDB:

def __init__(self):
self._db = dict()

def __getattribute__(self, attr):
print('called __getattribute__(%s)' % attr)
return self._db[attr]

交互式下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> r = RecursionDB()
>>> r.abc
called __getattribute__(abc)
called __getattribute__(_db)
called __getattribute__(_db)
called __getattribute__(_db)
called __getattribute__(_db)
called __getattribute__(_db)
called __getattribute__(_db)
called __getattribute__(_db)
.
.
.
File "~\python-3.5.2\lib\idlelib\PyShell.py", line 1344, in write
return self.shell.write(s, self.tags)
RecursionError: maximum recursion depth exceeded while calling a Python object

为什么会出现RecursionError呢?代码中,__getattribute__会访问self._db,由于__getattribute__的特性,意味着需要再次调用__getattribute__特殊方法,而它继续调用self._db,因此出现无线循环。为了解决这个问题,需要调用父类的特殊方法,代码如下。

1
2
3
4
5
6
7
8
9
class NoRecursionDB:

def __init__(self):
self._db = dict(bar='foo')

def __getattribute__(self, attr):
print('called __getattribute__(%s)' % attr)
_db = super().__getattribute__('_db')
return _db[attr]

类似地,另外两个特殊方法也需同样的处理。

小结

Python提供三个特殊方法__getattr____setattr____getattribute__实现对属性的访问控制。利用这三个特殊方法可以灵活地处理属性的访问控制,例如惰性属性。如果类定义了__getattr__特殊方法,那么当系统在该类的实例字典中找不到要查询的属性时,就会调用这个特殊方法。

__setattr____getattr__特殊方法可以实现惰性的方式来加载并保存对象的属性。

要注意__getattribute____getattr__特殊方法的区别:前者只会在访问的属性缺失是触发,后者则是每次访问属性时触发。

最后就是处理递归问题,通过super()来触发父类的特殊访问以避免当前类的属性的无限调用—无限循环。

转载请包括本文地址:https://allenwind.github.io/blog/5492
更多文章请参考:https://allenwind.github.io/blog/archives/