Effective Python -- 第 4 章 元类及属性(下)

第 4 章 元类及属性(下)

第 32 条:用 __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 类的同名方法)来做,以避免无限递归。

第 33 条:用元类来验证子类

元类最简单的一种用途,就是验证某个类定义得是否正确。构建复杂的类体系时,可能需要确保类的风格协调一致、确保某些方法得到了覆写,或是确保类属性之间具备某些严格的关系。元类提供了一种可靠的验证方式,每当开发者定义新的类时,它都会运行验证代码,以确保这个新类符合预定的规范。

开发者一般会把验证代码放在本类的 __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

总结

  • 通过元类,可以在生成子类对象之前,先验证子类的定义是否合乎规范。
  • Python 2 和 Python 3 指定元类的语法略有不同。
  • Python 系统把子类的整个 class 语句体处理完毕之后,就会调用其元类的 __new__ 方法。

第 34 条:用元类来注册子类

元类还有一个用途,就是在程序中自动注册类型。对于需要反向查找(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)、插件系统和系统挂钩。

总结

  • 在构建模块化的 Python 程序时,类的注册是一种很有用的模式。
  • 开发者每次从基类中继承子类时,基类的元类都可以自动运行注册代码。
  • 通过元类来实现类的注册,可以确保所有子类都不会遗漏,从而避免后续的错误。

第 35 条:用元类来注解类的属性

元类还有一个更有用处的功能,那就是可以在某个类刚定义好但是尚未使用的时候,提前修改或注解该类的属性。这种写法通常会与描述符(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'}

总结

  • 借助元类,可以在某个类完全定义好之前,率先修改该类的属性。
  • 描述符与元类能够有效地组合起来,以便对某种行为做出修饰,或在程序运行时探查相关信息。
  • 如果把元类与描述符相结合,那就可以在不使用 weakref 模块的前提下避免内存泄漏。

你可能感兴趣的:(Effective,Python,python,类)