第四章:元类及属性(Ⅳ)

这篇文章是基于 《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 模块的前提下避免内存泄露。

在这里插入图片描述

发布了247 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/weixin_39541632/article/details/104571057