这篇文章是基于 《Effective Python——编写高质量Python代码的59个有效方法》[美] 布雷特·斯拉特金 著 爱飞翔 译 这本书中的内容,写写自己在某方面的感悟,并摘录一些作为读书笔记供今后鞭策。侵删。
第 35 条:用元类来注解类的属性
元类还有一个更有用处的功能,那就是可以在某个类刚定义好但是尚未使用的时候,提前修改或注解该类的属性。这种写法通常会与描述符搭配起来,令这些属性可以更加详细地了解自己在外围类中的使用方式。
例如,要定义新的类,用来表示客户数据库里的某一行。同时,我们还希望在该类的相关属性与数据库表的每一列之间,建立对应关系。于是,用下面这个描述符类,把属性与列名联系起来。
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 和 getatty 函数,把每个实例的所有状态都作为 protected 字段,存放在该实例的字典里面。也可以使用 weakref 字典来构建描述符,而刚才的那段代码,目前来看,似乎要比 weakref 方案便捷得多。
接下来定义表示数据行的 Customer 类,定义该类的时候,我们要为每个类属性指定对应的列名。
class Customer(object):
first_name = Field('first_name')
last_name = Field('last_name')
prefix = Field('prefix')
suffix = Field('suffix')
Customer 类用起来比较简单。通过下面这段演示代码可以看出,Field 描述符能够按照预期,修改 __ dict __ 实例字典:
foo = Customer()
print(repr(foo.first_name), foo.__dict__)
foo.first_name = 'Euclid'
print(repr(foo.first_name), foo.__dict__)
问题在于,上面这种写法显得有些重复。在 Customer 类的 class 语句体中,我们既然要构建好的 Field 对象赋给 Customer.first_name,那为什么还要把这个字段名再传给 Field 的构造器呢?
之所以还要把字段名传给 Field 构造器,是因为定义 Customer 类的时候,Python 会以从右向左的顺序解读赋值语句,这与从左至右的阅读顺序恰好相反。首先,Python 会以 Field(‘first_name’) 的形式来调用 Field 构造器,然后,它把调用构造器所得的返回值,赋给 Cusomer.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 instance(value, Field):
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):
self.name = None
self.internal_name = None
# ...
有了元类,新的 DatabaseRow 基类以及新的 Field 描述符之后,我们再为数据行定义 DatabaseRow 子类时,就不用再像原来那样,编写重复代码了。
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
memo
- 借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性。
- 描述符与元类能够有效地组合起来,以便对某种行为做出修饰,或在程序运行时探查相关信息。
- 如果把元类与描述符相结合,那就可以不使用 weakref 模块的前提下避免内存泄露。