__getattr__
、__getattribute__
和 __setattr__
实现按需生成的属性Python 语言提供了一些挂钩,使得开发者很容易就能编写出通用的代码,以便将多个系统黏合起来。例如,我们要把数据库的行(row)表示为 Python 对象。由于数据库有自己的一套结构(schema),所以在操作与行相对应的对象时,必须知道这个数据库的结构。然而,把 Python 对象与数据库相连接的这些代码,却不需要知道行的结构,所以,这部分代码应该写得通用一些。
那么,如何实现这种通用的代码呢?普通的实例属性、@property 方法和描述符,都不能完成此功能,因为它们都必须预先定义好,而像这样的动态行为,则可以通过 Python 的 __getattr__
特殊方法来做。如果某个类定义了 __getattr__
,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。
class LazyDB(object):
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = 'Value for %s' % name
setattr(self, name, value)
return value
下面,访问 data 对象所缺失的 foo 属性。这会导致 Python 调用刚才定义的 __getattr__
方法,从而修改实例的 __dict__
字典:
data = LazyDB()
print('Before:', data.__dict__)
print('foo: ', data.foo)
print('After: ', data.__dict__)
>>>
Before: {'exists': 5}
foo: Value for foo
After: {'exists': 5, 'foo': 'Value for foo'}
然后,给 LazyDB 添加记录功能,把程序对 __getattr__
的调用行为记录下来。请注意,为了避免无限递归,需要在 LoggingLazyDB 子类里面通过 super().__getattr__()
来获取真正的属性值。
class LoggingLazyDB(LazyDB):
def __getattr__(self, name):
print('Called __getattr__(%s)' % name)
return super().__getattr__(name)
data = LoggingLazyDB()
print('exists:', data.exists)
print('foo: ', data.foo)
print('foo: ', data.foo)
>>>
exists: 5
Ca1led __getattr__(foo)
foo: Value for foo
foo: Value for foo
由于 exists 属性本身就在实例字典里面,所以访问它的时候,绝不会触发 __getattr__
。而 foo 属性刚开始并不在实例字典中,所以初次访问的时候会触发 __getattr__
。由于 __getattr__
又会调用 setattr 方法,并把 foo 放在实例字典中,所以第二次访问 foo 的时候,就不会再触发 __getattr__
了。
这种行为非常适合实现无结构数据(schemaless data,无模式数据)的按需访问。初次执行 __getattr__
的时候进行一些操作,把相关的属性加载进来,以后再访问该属性时,只需从现有的结果之中获取即可。
现在假设还要在数据库系统中实现事务(transaction,交易)处理。用户下次访问某属性时,要知道数据库中对应的行是否依然有效,以及相关事务是否依然处于开启状态。这样的需求,无法通过 __getattr__
挂钩可靠地实现出来,因为 Python 系统会直接从实例字典的现存属性中迅速查出该属性,并返回给调用者。
为了实现此功能,可以使用 Python 中的另外一个挂钩,也就是 __getattribute__
。程序每次访问对象的属性时,Python 系统都会调用这个特殊方法,即使属性字典里面已经有了该属性,也依然会触发 __getattribute__
方法。这样就可以在程序每次访问属性时,检查全局事务状态。下面定义的这个 ValidatingDB 类,会在 __getattribute__
方法里面记录每次调用的时间。
class ValidatingDB(object):
def __init__(self):
self.exists = 5
def __getattribute__(self, name):
print('Called __getattribute__(%s)' % name)
try:
return super().__getattribute__(name)
except AttributeError:
value = 'value for %s' % name
setattr(self, name, value)
return value
data = ValidatingDB()
print('exists:', data.exists)
print('foo: ', data.foo)
print('foo: ', data.foo)
>>>
Called __getattribute__(exists)
exists: 5
Called __getattribute__(foo)
foo: Value for foo
Called __getattribute__(foo)
foo: Value for foo
按照 Python 处理缺失属性的标准流程,如果程序动态地访问了一个不应该有的属性,那么可以在 __getattr__
和 __getattribute__
里面抛出 AttributeError 异常。
class MissingPropertyDB(object):
def __getattr__(self, name):
if name == 'bad_name':
raise AttributeError('%s is missing' % name)
# ...
data = MissingPropertyDB()
data.bad_name
>>>
AttributeError: bad_name is missing
实现通用的功能时,经常会在 Python 代码里使用内置的 hasattr 函数来判断对象是否已经拥有了相关的属性,并用内置的 getattr 函数来获取属性值。这些函数会先在实例字典中搜索待查询的属性,然后再调用__getattr__
。
data = LoggingLazyDB()
print('Before: ', data.__dict__)
print('foo exists: ', hasattr(data, 'foo'))
print('After: ', data.__dict__)
print('foo exists: ', hasattr(data, 'foo'))
>>>
Before: {'exists': 5}
Called __getattr__(foo)
foo exists: True
After: {'exists': 5, 'foo': 'Value for foo'}
foo exists: True
在上例中,__getattr__
方法只调用了一次。反之,如果本类实现的是 __getattribute__
方法,那么每次在对象上面调用 hasattr 或 getattr 函数时,此方法都会执行。
data = validatingDB()
print('foo exists: ', hasattr(data, 'foo'))
print('foo exists: ', hasattr(data, 'foo'))
>>>
Called __getattribute__(foo)
foo exists: True
Called __getattribute__(foo)
foo exists: True
现在,假设当程序把值赋给 Python 对象之后,要以惰性的方式将其推回数据库。此功能可以用 Python 所提供的 __setattr__
挂钩来实现,它与前面所讲的那两个挂钩类似,可以拦截对属性的赋值操作。但是与 __getattr__
和 __getattribute__
不同的地方在于,不需要分成两个方法来处理。只要对实例的属性赋值,无论是直接赋值,还是通过内置的 setattr 函数赋值,都会触发 __setattr__
方法。
class SavingDB(object):
def __setattr__(self, name, value):
# Save some data to the DB log
#...
super().__setattr__(name, value)
下面定义的这个 LoggingSavingDB 类,是 SavingDB 的子类,每次对它的属性赋值时,都会触发 __setattr__
方法。
class LoggingSavingDB(SavingDB):
def __setattr__(self, name, value):
print('Called __setattr__(%s, %r)' % (name, value))
super().__setattr__(name, value)
data = LoggingSavingDB()
print('Before: ', data.__dict__)
data.foo = 5
print('After: ', data.__dict__)
data.foo = 7
print('Finally:', data.__dict__)
>>>
Before: {}
Called __setattr__(foo, 5)
After: {'foo': 5}
Called __setattr__(foo, 7)
Finally: {'foo': 7}
使用 __getattribute__
和 __setattr__
挂钩方法时要注意:每次访间对象属性时,它们都会触发,而这可能并不是你想要的效果。例如,想在查询对象的属性时,从对象内部的一份字典里面,搜寻与待查属性相关联的属性值。
class BrokenDictionaryDB(object):
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print('Called __getattribute__(%s)' % name)
return self._data[name]
上面这段代码,会在 __getattribute__
方法里面访问 self._data。试着运行一下,就会发现:这段代码将导致 Python 程序反复递归,从而令其突破最大的栈深度并崩溃。
data = BrokenDictionaryDB({'foo': 3})
data.foo
>>>
Called __getattribute__(foo)
Called __getattribute__(_data)
Called __getattribute__(_data)
...
Traceback ...
RuntimeError: maximum recursion depth exceeded
问题在于,__getattribute__
会访问 self._data,而这就意味着需要再次调用 __getattribute__
,然后它又会继续访问 self._data,并无限循环。解决办法是采用 super().__gctattributc__
方法,从实例的属性字典里面直接获取 _data 属性值,以避免无限递归。
class DictionaryDB(object):
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
data_dict = super().__getattribute__('_data')
return data_dict[name]
与之类似,如果要在 __setattr__
方法中修改对象的属性,那么也需要通过 super().__setattr__
来完成。
__getattr__
和 __setattr__
,可以用惰性的方式来加载并保存对象的属性。__getattr__
与 __getattribute__
的区别:前者只会在待访问的属性缺失时触发,而后者则会在每次访问属性时触发。__getattribute__
和 __setattr__
方法中访问实例属性,那么应该直接通过 super()(也就是 object 类的同名方法)来做,以避免无限递归。元类最简单的一种用途,就是验证某个类定义得是否正确。构建复杂的类体系时,可能需要确保类的风格协调一致、确保某些方法得到了覆写,或是确保类属性之间具备某些严格的关系。元类提供了一种可靠的验证方式,每当开发者定义新的类时,它都会运行验证代码,以确保这个新类符合预定的规范。
开发者一般会把验证代码放在本类的 __init__
方法里面运行,这是由于程序在构建该类的对象时,会调用本类型的 __init__
方法。但如果改用元类来进行验证,还可以把验证时机再往前提一些,以便尽早发现错误。
在讲解如何用元类来验证子类之前,首先要明白如何为一般的对象定义元类。定义元类的时候,要从 type 中继承,而对于使用该元类的其他类来说,Python 默认会把那些类的 class 语句体中所含的相关内容,发送给元类的 __new__
方法。于是,就可以在系统构建出那种类型之前,先修改那个类的信息:
class Meta(type):
def __new__(meta, name, bases, class_dict):
print((meta, name, bases, class_dict))
return type.__new__(meta, name, bases, class_dict)
class MyClass(object, metaclass=Meta):
stuff = 123
def foo(self):
pass
元类可以获知那个类的名称、其所继承的父类,以及定义在 class 语句体中的全部类属性:
>>>
(<class '__main__.Meta'>,
'MyClass',
(<class 'object'>,),
{'__module__': '__main__',
'__qua7name__': 'MyClass',
'foo': <function MyClass.foo at 0x102c7dd08>,
'stuff': 123})
Python 2 的写法稍有不同,它是通过名为 __metaclass__
的类属性来指定元类的。而 Meta.__new__
接口则一致。
为了在定义某个类的时候,确保该类的所有参数都有效,可以把相关的验证逻辑添加到 Meta.__new__
方法中。例如,要用类来表示任意多边形。为此,可以定义一种特殊的验证类,使得多边形体系中的基类,把这个验证类当成自己的元类。要注意的是,元类中所编写的验证逻辑,针对的是该基类的子类,而不是基类本身。
class ValidatePolygon(type):
def __new__(meta, name, bases, class_dict):
# Don't validate the abstract Polygon class
if bases != (object,):
if class_dict['sides'] < 3:
raise valueError('Polygons need 3+ sides')
return type.__new__(meta, name, bases, class_dict)
class Polygon(object, metaclass=ValidatePolygon):
sides = None # Specified by subclasses
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Triangle(Polygon):
sides = 3
假如尝试定义一种边数少于 3 的多边形子类,那么 class 语句体刚一结束,元类中的验证代码立刻就会拒绝这个 class。也就是说,如果开发者定义这样一种子类,那么程序根本就无法运行。
print('Before class')
class Line(Polygon):
print('Before sides')
sides = 1
print('After sides')
print('After class')
>>>
Before class
Before sides
After sides
Traceback ...
ValueError: Polygons need 3+ sides
__new__
方法。元类还有一个用途,就是在程序中自动注册类型。对于需要反向查找(reverselookup,简称反查)的场合,这种注册操作是很有用的,它使得可以在简单的标识符与对应的类之间,建立映射关系。
例如,想按照自己的实现方式,将 Python 对象表示为 JSON 格式的序列化数据,那么,就需要用一种手段,把指定的对象转换成 JSON 字符串。下面这段代码,定义了一个通用的基类,它可以记录程序调用本类构造器时所用的参数,并将其转换为 JSON 字典:
class Serializable(object):
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({'args': self.args})
有了这个类,就可以把一些简单且不可变的数据结构,轻松地转换成字符串了。例如,下面这个 Point2D 类,很容易就能转为字符串。
class Point2D(Serializable):
def __init__(self, x, y):
super().__init__(x, y)
self.x = x
self.y = y
def __repr__(self):
return 'Point2D(%d, %d)' % (self.x, self.y)
point = Point2D(5, 3)
print('Object: ', point)
print('Serialized:', point.serialize())
>>>
Object: Point2D(5, 3)
Serialized: {"args": [5, 3]}
现在,需要对这种 JSON 字符串执行反序列化(deserialize)操作,并构建出该字符串所表示的 Point2D 对象。下面定义的这个 Deserializable 类,继承自 Serializable,它可以把 Serializable 所产生的 JSON 字符串还原为 Python 对象:
class Deserializable(Serializable):
@classmethod
def deserialize(cls, json_data):
params = json.loads(json_data)
return cls(*params['args'])
有了 Deserializable,就可以用一种通用的方式,对简单且不可变的对象执行序列化和反序列化操作。
class BetterPoint2D(Deserializable):
# ...
point = BetterPoint2D(5, 3)
print('Before: ', point)
data = point.serialize()
print('Seria1ized:', data)
after = BetterPoint2D.deserialize(data)
print('After: ', after)
>>>
Before: BetterPoint2D(5, 3)
Serialized: {"args": [5, 3]}
After: BetterPoint2D(5, 3)
这种实现方案的缺点是,必须提前知道序列化的数据是什么类型(例如,是 Point2D 或 BetterPoint2D 等),然后才能对其做反序列化操作。理想的方案应该是:有很多类都可以把本类对象转换为 JSON 格式的序列化字符串,但是只需要一个公用的反序列化函数,就可以将任意的 JSON 字符串还原成相应的 Python 对象。
为此,可以把序列化对象的类名写到 JSON 数据里面。
class BetterSerializable(object):
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args,
})
def __repr__(self):
# ...
然后,把类名与该类对象构造器之间的映射关系,维护到一份字典里面。这样凡是经由 register_class 注册的类,就都可以拿通用的 deserialize 函数做反序列化操作。
registry = {}
def register_class(target_class):
registry[target_class.__name__] = target_class
def deserialize(data):
params = json.loads(data)
name = params['class']
target_class = registry[name]
return target_class(*params['args'])
为了确保 deserialize 函数正常运作,必须用 register_class 把将来可能要执行反序列化操作的那些类,都注册一遍。
class EvenBetterPoint2D(BetterSerializable):
def __init__(se1f, x, y):
super().__init__(x, y)
self.x = x
self.y = y
register_class(EvenBetterPoint2D)
接下来,就可以对任意的 JSON 字符串执行反序列化操作了,执行操作时,不需要知道该字符串表示的是哪种类型的数据。
point = EvenBetterPoint2D(5, 3)
print('Before: ', point)
data = point.serialize()
print('Serialized:', data)
after = deserialize(data)
print('After: ', after)
>>>
Before: EvenBetterPoint2D(5, 3)
Serialized: {"class": "EvenBetterPoint2D", "args": [5, 3]}
After: EvenBetterPoint2D(5, 3)
这种方案也有缺点,那就是开发者可能会忘记调用 register_class 函数。
class Point3D(BetterSerializable):
def __init__(self, x, y, z):
super().__init__(x, y, z)
self.x = x
self.y = y
self.z = z
# Forgot to call register_class! Whoops !
如果要对某份数据执行反序列化操作,而开发者又没有提前把该数据所在的类注册好,那么程序就会在运行的时候崩溃。
point = Point3D(5, 9, -4)
data = point.serialize()
deserialize(data)
>>>
KeyError: 'Point3D'
如果写完 class 语句体之后,忘记调用 register_class,那么即使从 BetterSerializable 中继承了子类,也依然无法利用 deserialize 函数对其执行反序列化操作。所以,这种写法很容易出错,而且对于编程新手尤其危险。在 Python 3 中使用类修饰器(classdecorator)时,也会出现同样的问题。
应该想个办法,保证开发者在继承 BetterSerializable 的时候,程序会自动调用 register_class 函数,并将新的子类注册好。这个功能可以通过元类来实现。定义完子类的 class 语句体之后,元类可以拦截这个新的子类。于是,就能够在子类的 class 语句体得到处理之后,立刻注册这一新的类型。
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
class RegisteredSerializable(BetterSerializable,
metaclass=Meta):
pass
现在,定义完 RegisteredSerializable 的子类之后,开发者可以确信:该类肯定已经通过 register_class 函数注册好了,于是 deserialize 函数也就可以正常运作了。
class Vector3D(RegisteredSerializable):
def __init__(self, x, y, z):
super().__init__(x, y, z)
self.x,self.y, self.z = x, y, z
v3 = Vector3D(10, -7, 3)
print('Before: ', v3)
data = v3.serialize()
print('Serialized:', data)
print('After: ', deserialize(data))
>>>
Before: vector3D(10, -7, 3)
Serialized: {"class": "vector3D", "args": [10, -7, 3]}
After: vector3D(10, -7, 3)
只要类的继承体系正确无误,就可以用元类来实现类的注册,以确保每一个子类都不会遗漏。通过刚才的范例可以看出:这种方案,适用于序列化和反序列化操作。此外,它还适用于数据库的对象关系映射(object-relationship mapping,ORM)、插件系统和系统挂钩。
元类还有一个更有用处的功能,那就是可以在某个类刚定义好但是尚未使用的时候,提前修改或注解该类的属性。这种写法通常会与描述符(descriptor)搭配起来,令这些属性可以更加详细地了解自己在外围类中的使用方式。
例如,要定义新的类,用来表示客户数据库里的某一行。同时,还希望在该类的相关属性与数据库表的每一列之间,建立对应关系。于是,用下面这个描述符类,把属性与列名联系起来。
class Field(object):
def __init__(self, name):
self.name = name
self.internal_name = '_' + self.name
def __get__(self, instance, instance_type):
if instance is None: return self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
由于列的名称已经保存到了 Field 描述符中,所以可以通过内置的 setattr 和 getattr 函数,把每个实例的所有状态都作为 protected 字段,存放在该实例的字典里面。在本书前面的例子中,为了避免内存泄漏,我们曾经用 weakref 字典来构建描述符,而刚才的那段代码,目前看来,似乎要比 weakref 方案便捷得多。
接下来定义表示数据行的 Customer 类,定义该类的时候,要为每个类属性指定对应的列名。
class Customer(object):
# Class attributes
first_name = Fie1d('first_name')
last_name = Field('last_name')
prefix = Field('prefix')
suffix = Field('suffix')
Customer 类用起来比较简单。通过下面这段演示代码可以看出,Field 描述符能够按照预期,修改 __dict__
实例字典:
foo = Customer()
print('Before:', repr(foo.first_name), foo.__dict__)
foo.first_name = 'Euclid'
print('After: ', repr(foo.first_name), foo.__dict__)
>>>
Before: '' {}
After: 'Euclid' {'_first_name': 'Euclid'}
问题在于,上面这种写法显得有些重复。在 Customer 类的 class 语句体中,既然要将构建好的 Field 对象赋给 Customer.first_name,那为什么还要把这个字段名(本例中是 ‘first_name’)再传给 Field 的构造器呢?
之所以还要把字段名传给 Field 构造器,是因为定义 Customer 类的时候,Python 会以从右向左的顺序解读赋值语句,这与从左至右的阅读顺序恰好相反。首先,Python 会以 Field(‘first_name’) 的形式来调用 Field 构造器,然后,它把调用构造器所得的返回值,赋给 Customer.field_name。从这个顺序来看,Field 对象没有办法提前知道自己会赋给 Customer 类里的哪一个属性。
为了消除这种重复代码,现在用元类来改写它。使用元类,就相当于直接在 class 语句上面放置挂钩,只要 class 语句体处理完毕,这个挂钩就会立刻触发。于是,可以借助元类,为 Field 描述符自动设置其 Field.name 和 Field.internal_name,而不用再像刚才那样,把列的名称手工传给 Field 构造器。
class Meta(type):
def __new__(meta, name, bases, class_dict):
for key, value in class_dict.items():
if isinstance(value, Fie1d):
value.name = key
value.internal_name = '_' + key
cls = type.__new__(meta, name, bases, class_dict)
return cls
下面定义一个基类,该基类使用刚才定义好的 Meta 作为其元类。凡是代表数据库里面某一行的类,都应该从这个基类中继承,以确保它们能够利用元类所提供的功能:
class DatabaseRow(object, metaclass=Meta):
pass
采用元类来实现这套方案时,Field 描述符类基本上是无需修改的。唯一要调整的地方就在于:现在不需要再给构造器传入参数了,因为刚才编写的 Meta.__new__
方法会自动把相关的属性设置好。
class Field(object):
def __init__(self):
# These wi1l be assigned by the metaclass.
self.name = None
self.internal_name = None
# ...
有了元类、新的 DatabaseRow 基类以及新的 Field 描述符之后,我在为数据行定义 DatabaseRow 子类时,就不用再像原来那样,编写重复的代码了。
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
新的 BetterCustomer 类的行为与旧的 Customer 类相同:
foo = BetterCustomer()
print('Before:', repr(foo.first_name), foo.__dict__)
foo.first_name = 'Euler'
print('After: ', repr(foo.first_name), foo.__dict__)
>>>
Before: '' {}
After: 'Euler' {'_first_name': 'Euler'}