python元编程
动态属性
在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性。
属性描述符(get/set/delete)
基本概念
描述符是对多个属性运用相同存取逻辑的一种方式。例如,Django ORM 和 SQL Alchemy 等 ORM 中的字段类型是描述符,把数据库记录中字段里的数据与 Python 对象的属性对应起来。为什么需要描述符:对property来说,最大的缺点就是它们不能重复使用。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。
描述符是property的升级版,允许你为重复的property逻辑编写单独的类来处理。
基本要求:描述符是实现了特定协议的类,这个协议包括
__get__
、__set__
和__delete__
方法。property
类实现了完整的描述符协议。通常,可以只实现部分协议。其实,我们在真实的代码中见到的大多数描述符只实现了__get__
和__set__
方法,还有很多只实现了其中的一个。实现了
__get__
、__set__
或__delete__
方法的类是描述符。用法:描述符的用法是,创建一个描述符类,它的实例对象作为另一个类的属性。
为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用
__get__
和__set__
方法。大致流程:
- 定义一个描述符类D,其内包含一个或多个
__get__()
、__set__()
、__delete__()
方法- 将描述符类D的实例对象d赋值给另一个要代理的类中某个属性attr,即attr=D()
- 之后访问、赋值、删除attr属性,将会自动触发描述符类中的
__get__()
、__set__()
、__delete__()
方法实现:
要定义描述符类很简单,只要某个类中包含了下面一个或多个方法,就算是满足描述符协议,就是描述符类,就可以作为属性操作的代理器。
1
2
3
4 class Descriptor():
def __get__(self, instance, owner):...
def __set__(self, instance, value):...
def __delete__(self, instance):...需要注意的是,
__get__
的返回值需要是属性值或抛异常,另外两个方法要返回None。类属性描述符对象和实例属性同名时:描述符针对的是类属性,但是当一个类中,如果类属性是描述符对象,而实例属性由于这个描述符属性同名
1
2
3
4
5
6
7
8
9
10 class Person:
character = CharacterDescriptor('乐观的')
weight = WeightDescriptor(150)
def __init__(self, character,weight):
self.character = character
self.weight = weight
p = Person('悲观的', 200)
print(p.character) #属性的访问
print(p.weight) #从上面的运行结果可以看出,首先是访问了描述符的__set__方法,这是因为在构建对象的时候,相当于为character和weight赋值,然后再调用__get__方法,这是因为访问了类属性character和weight,但是最终打印出来值却并不是类属性的值,这是因为,实例属性实际上是在“描述符类属性”后面访问的,所以覆盖掉了。
专有名词
描述符类: 实现了描述符协议的类,描述符类的一些协议(
__get__
、__set__
或__delete__
)。实现了
__get__
、__set__
、__delete__
方法的类是描述符,只要实现了其中一个就是。托管类: 将描述符实例作为类属性的类,比如Fruits 类,他有 weight、price 两个类属性,且都被赋予了描述符类的实例。
描述符实例: 描述符类创建出描述符实例,通常来讲,描述符类的实例会被赋给托管类的类属性。
托管实例: 托管类创建出来的实例
托管属性: 托管类中由描述符实例处理的公开属性
存储属性: 可以粗略的理解为、托管实例的属性、在上例中使用
vars(apple)
得到的结果中 price 和 weight 实例属性就是存储属性,它们实际存储着*实例的*属性值非数据描述符:一个类,如果只定义了__get__() 或者是__delete__()方法,而没有定义__set__()方法,则认为是非数据描述符(即没有定义__set__)
数据描述符:一个类,不仅定义了__get__() 方法,还定义__set__(), __delete__() 方法,则认为是数据描述符(即定义了__get__和__set__)
ps: 托管属性是类(Fruits)属性、存储属性是实例(apple)的属性。
>
Quantity
实例是描述符,因此有个放大镜,用于获取值(__get__
),以及一个手抓,用于设置值(__set__
)。
例子
要点
定义位置:为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get__和__set__方法。
独立实例:类使用了一个字典来单独保存专属于实例的数据。这个一般来说是没问题的,除非你用到了不可哈希(unhashable)的对象
不可哈希处理:list的子类是不可哈希的,因此它们不能为描述符类用做数据字典的key。有一些方法可以规避这个问题,但是都不完美。最好的方法可能就是给你的描述符加标签了。描述符可以安全的在这里存储数据。只是要记住,不要在别的地方也给这个描述符添加标签。这样的代码很脆弱也有很多微妙之处。但这个方法的确很普遍,可以用在不可哈希的所有者类上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 class Descriptor(object):
def __init__(self, label):
self.label = label
def __get__(self, instance, owner):
print('__get__', instance, owner)
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
print('__set__')
instance.__dict__[self.label] = value
class Foo(list):
x = Descriptor('x')
y = Descriptor('y')泄漏内存问题:WeakKeyDictionary可以保证描述符类不会泄漏内存:WeakKeyDictionary的特殊之处在于:如果运行期系统发现这种字典所持有的引用,是整个程序里面指向Exam实例的最后一份引用,那么,系统就会自动将该实例从字典的键中移除。
模拟ORM
代码-描述符类1 | #!/usr/bin/env Python |
1 | class ModelMetaClass(type): |
1 | class User(BaseModel): |
1 | True |
成绩管理
代码1 | #!/usr/bin/env Python |
1 | 2020-06-25 20:18:23 lazy_db INFO: first 82 |
成绩管理2.0
代码1 | class Grade: |
1 | 2020-06-25 20:18:23 lazy_db INFO: first 82 |
成绩管理2.1
代码1 | class Grade: |
进阶
添加回调
描述符仅仅是类,也许你想要为它们增加一些方法。举个例子,描述符是一个用来回调property的很好的手段。比如我们想要一个类的某个部分的状态发生变化时就立刻通知我们。下面的大部分代码是用来做这个的:
1 | class CallbackProperty(object): |
这是一个很有吸引力的模式——我们可以自定义回调函数用来响应一个类中的状态变化,而且完全无需修改这个类的代码。这样做可真是替人分忧解难呀。现在,我们所要做的就是调用ba.balance.add_callback(ba, low_balance_warning),以使得每次balance变化时low_balance_warning都会被调用。
但是我们是如何做到的呢?当我们试图访问它们时,描述符总是会调用
__get__
。就好像addcallback方法是无法触及的一样!其实关键在于利用了一种特殊的情况,即,当从类的层次访问时,`_get`方法的第一个参数是None。
1 | class CallbackProperty(object): |
实现底层 @classmethod
1 |
|
可以分这样几步分析:
第一步:@NewDefine_classmethod本质上是一个“类装饰器”,从它的定义可知,它的定义为
class NewDefine_classmethod(function).我们发现,python系统定义的@classmethod其实它的定义也是一样的,如下,
class classmethod(function) .怎么样?它们二者的定义是不是一样?
第二步:NewDefineclassmethod本质上又是一个描述符,因为在它的内部实现了\_get__协议,由此可见,NewDefine_classmethod是“集装饰器-描述符”于一身的。
第三步:运行过程分析,因为study_1=NewDefine_classmethod(study_1),所以,study_1本质上是一个NewDefine_classmethod的对象,又因为NewDefine_classmethod本质上是实现了描述符的,所以,study_1本质上是一个定义在类中的描述符属性。
第四步:因为study1本质上是一个定义在类中的描述符属性。所以在执行Person.study_1的时候,相当于是访问类的描述符属性,所以会进入到描述符的\_get__方法。
现在是不是觉得原来python描述符还有这样神奇的使用呢?
注意:如果修饰的函数本身是具有返回值的,在__get__里面所定义的wrapper里面一定要返回,即return self.function(owner, args, *kwargs)。
还有一个地方需要注意的是,因为这是自定义的底层实现,所以一些集成IDE可能会显示有语法错误,但是这没有关系,这正是python灵活多变的地方,运行并不会出现错误。
实现底层 @staticmethod
staticmethod方法与classmethod方法的区别在于classmethod方法在使用需要传进一个类的引用作为参数。而staticmethod则不用。
1 | class NewDefine_staticmethod: |
类方法classmethod必须第一个参数是cls,这个实际上就是判断所属的那个类,因此在__get__里面的function在调用的时候,第一个参数需要传递为owner,因为所属的“类cls等价于Person等价于owner”,但是因为静态方法不需要任何参数cls或者是self都不需要,因此在__get__实现的时候不能再传递owner参数,否则会显示参数错误。
实现底层 @property
1 | class NewDefine_property: |
基本思想和前面分析的还是一样的,但是有几个地方有所区别,需要注意:
第一:@property的目的是封装一个方法,是这个方法可以被当做属性访问
第二:调用的方式与前面有所不同,__get__里面不能再定义wrapper了,否则不会调用wrapper。得不到想要的结果,为什么呢?
因为调用的方式不一样,根据前面的分析,study_1的本质是描述符属性,但是前面的调用均是使用的
Person.study_1()或者是p.study_1()的形式,还是当成方法去使用的。但是此处不一样了,直接就是当成属性去使用,
p.study1 ,不再是方法调用,因此wrapper函数得不到调用。所以\_get__方法得到了进一步简化。
按需生成属性
Python 魔法方法(三) __getattr__,__setattr__, __delattr__
使用__getattr__、__setattr__和__getattribute__来动态生成属性
Python 语言提供了一些挂钩,使得开发者很容易就能编写出通用的代码,以便将多个系统黏合起来。例如,我们要把数据库的行(row)表示为 Python 对象。由于数据库有自己的一套结构(schema),也称架构、模式、纲要、概要、大纲,所以在操作与行相对应的对象时,我们必须知道这个数据库的结构。然而,把 Python 对象与数据库相连接的这些代码,却不需要知道行的结构,所以,这部分代码应该写得通用一些。
那么,如何实现这种通用的代码呢?普通的实例属性、@property 方法和描述符,都不能完成此功能,因为它们都必须预先定义好,而像这样的动态行为,则可以通过 Python 的__getattr__特殊方法来做。如果某个类定义了__getattr__,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。
实例属性查找
首先需要明白的是实例属性查找的过程:
如果obj是某个类的实例,那么obj.name(以及等价的getattr(obj,’name’))首先调用__getattribute__。如果类定义了__getattr__方法,那么在__getattribute__抛出 AttributeError 的时候就会调用到__getattr__,而对于描述符(__get__)的调用,则是发生在__getattribute__内部的。官网文档是这么描述的
The implementation works through a precedence chain that gives data descriptors priority over instance variables, instance variables priority over non-data descriptors, and assigns lowest priority to
__getattr__()
if provided.
obj = Clz(), 那么obj.attr 顺序如下:
(1)如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__get__方法, 否则
(2)如果“attr”出现在obj的__dict__中, 那么直接返回 obj.__dict__[‘attr’], 否则
(3)如果“attr”出现在Clz或其基类的__dict__中
(3.1)如果attr是non-data descriptor,那么调用其__get__方法, 否则
(3.2)返回 __dict__[‘attr’]
(4)如果Clz有__getattr__方法,调用__getattr__方法,否则
(5)抛出AttributeError
程序每次访问对象的属性时,Python 系统都会调用这个特殊方法,即使属性字典里面已经有了该属性,也依然会触发 __getattribute__ 方法。这样就可以在程序每次访问属性时,检查全局事务状态。
按照 Python 处理缺失属性的标准流程,如果程序动态地访问了一个不应该有的属性,那么可以在 __getattr__ 和 __getattribute__ 里面抛出 AttributeError 异常。
实现通用的功能时,我们经常会在 Python 代码里使用内置的 hasattr 函数来判断对象是否已经拥有了相关的属性,并用内置的 __getattr__ 函数来获取属性值。这些函数会先在实例字典中搜索待查询的属性,然后再调用 __getattr__。
四个魔法函数
访问时机
如果某个类定义了__getattr__ ,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。
程序每次访问对象的属性时,Python 系统都会调用这个特殊方法,即使属性字典里面已经有了该属性,也依然会触发__getattribute__方法。这样就可以在程序每次访问属性
按照Python处理缺失属性的标准流程,如果程序动态地访问了一个不应该有的属性,那么可以在__getattr__ 和__getattribute__ 里面抛出AttributeError异常。
只要对实例的属性赋值,无论是直接赋值,还是通过内置的setattr函数赋值,都会触发__setattr__方法 。
__getattr__
当我们访问一个不存在的属性的时候,会抛出异常,提示我们不存在这个属性。而这个异常就是__getattr__方法抛出的,其原因在于他是访问一个不存在的属性的最后落脚点,作为异常抛出的地方提示出错再适合不过了。
看例子,我们找一个存在的属性和不存在的属性。
1 | class A(object): |
可以看出,访问存在的属性时,会正常返回值,若该值不存在,则会进入最后的兜底函数__getattr__。
__setattr__
在对一个属性设置值的时候,会调用到这个函数,每个设置值的方式都会进入这个方法。
1 | class A(object): |
在实例化的时候,会进行初始化,在__init__里,对value的属性值进行了设置,这时候会调用__setattr__方法。
在对a.value重新设置值100的时候,会再次进入__setattr__方法。
需要注意的地方是,在重写__setattr__方法的时候千万不要重复调用造成死循环。
1 | class A(object): |
这是个死循环。当我们实例化这个类的时候,会进入__init__,然后对value进行设置值,设置值会进入__setattr__方法,而__setattr__方法里面又有一个self.name=value设置值的操作,会再次调用自身__setattr__,造成死循环。
除了上面调用object类的__setattr__避开死循环,还可以如下重写__setattr__避开循环。
1 | class A(object): |
__delattr__
__delattr__是个删除属性的方法
1 | class A(object): |
__delattr__也要避免死循环的问题,就如__setattr__一样,在重写__delattr__,避免重复调用。
__getattribute__
使用__getattribute__对属性的访问做额外处理
假设我们需要在数据库中实现事物(transaction)处理,即每次在访问属性时,需要额外调用特殊方法检查数据库中对应的行是否有效,以及相关的事务是否依然开放。此时使用__getattr__无法实现这种功能,因为第二次访问属性时,Python会直接返回上首次调用时存储在__dict__中的属性值,而不会再次调用__getattr__插寻属性的状态。此种情况下我们需要使用__getattribute__,该方法在用户每次访问属性是都会被调用。
1 | class LazyDB(object): |
要点
- __getattr__ 和 __setatr__,我们可以用惰性的方式来加载并保存对象的属性。
- 要理解 __getattr__ 与 __getattribute__ 的区别:前者只会在待访问的属性缺失时触发,而后者则会在每次访问属性时触发。
- 如果要在 __getattribute__ 和 __setattr__ 方法中访问实例属性,那么应该直接通过super()(也就是object类的同名方法)来做,以避免无限递归。
总结:
(1)对于类装饰器属性,只要出现属性访问(不管是通过对象访问还是类名访问),都会优先调用装饰器的__get__方法;
(2)对于类装饰器属性,若出现属性修改(不管是通过对象访问还是类名访问),都会优先调用装饰器的__set__方法;
(3)对于类装饰器属性,若出现属性删除(不管是通过对象访问还是类名访问),都会优先调用装饰器的__delete__方法
例子
元类
基本概念
类元编程是指在运行时创建或定制类的技艺。在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,而无需使用
class
关键字。类装饰器也是函数,不过能够审查、修改,甚至把被装饰的类替换成其他类。最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类种,例如我们见过的抽象基类。
元类的定义
Python定义元类时,需要从
type
类中继承,然后重写__new__
方法,便可以实现意想不到的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class Meta(type):
def __new__(meta,name,bases,class_dict):
#...各种逻辑实现1
cls = type.__new__(meta,name,bases,class_dict)
print('当前类名',name)
print('父类',bases)
print('全部类属性',class_dict)
#...各种逻辑实现2
return cls
class MyClass(object,metaclass=Meta):
stuff = 33
def foo(self):
pass
1
2
3 当前类名 MyClass
父类 (<class 'object'>,)
全部类属性 {'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 33, 'foo': <function MyClass.foo at 0x0000019E028315E8>}元类可以获知那个类的名称、其所继承的父类,以及定义在class语句体中的全部类属性
元类的本质
在Python当中万物皆对象,我们用 class
关键字定义的类本身也是一个对象, 负责产生该对象的类称之为元类 ,元类可以简称为类的类, 元类的主要目的是为了控制类的创建行为 。
type
是Python的一个内建元类,用来直接控制生成类,在Python中任何class
定义的类其实都是type
类实例化的结果。- 只有继承了
type
类才能称之为一个元类,否则就是一个普通的自定义类,自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程。
小结
元类的各种操作可以实现类的验证和注册逻辑,均可以在元类的 __new__
方法中实现,主要原因是当子类对象构建时,会先调用元类的 __new__
方法,产生一个空对象,然后再调用子类的 __init__
方法给对象属性进行赋值。
验证子类
元类是python比较高级的用法,简而言之,元类就是创建类的类。
而type就是一个元类,是用来创建类对象的类。
因此,要定义元类就要使其继承type类。
通常情况下,开发者在使用OOP的方式编程时,往往会用到__init__方法,即构造函数。
该方法会在类初始化时运行。但是我们可以将验证的时机提前,以至于提前到类创建之时,因此就会用到__new__方法。
1 | class Base(type): |
元类中所编写的验证逻辑,针对的是该基类的子类,而不是基类本身。
__new__()
方法接收到的参数依次是:
- 当前准备创建的类的对象;
- 类的名字;
- 类继承的父类集合;
- 类的方法集合。
案例1 :编写一个多边形类,当边数小于
3
时,其类报错,实现其验证逻辑。
1 | class ValidatePolygon(type): |
注册子类
元类还有一个用途,就是在程序中 自动注册类型
。开发者每次从基类中继承子类时,基类的元类都可以自动运行注册代码。
案例2 :实现对象的序列化与反序列化
1 | ###建立类名与该类对象的映射关系,维护registry字典。 |
通过元类来实现类的注册,可以确保所有的子类都不会遗漏,从而避免后续的错误。
获取__init__的默认参数
获取__init__的默认参数,并在classmethod方法中为没有给定的属性赋默认值,提升代码的健壮性
元类定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 #!/usr/bin/env Python
# -- coding: utf-8 --
"""
@version: v0.1
@author: narutohyc
@file: meta_interface.py
@Description:
@time: 2020/6/15 20:29
"""
import collections
from abc import (ABC,
abstractmethod,
ABCMeta)
import inspect
class DicMetaClass(ABCMeta):
def __new__(cls, name, bases, attrs, **kwargs):
if name == 'DicMeta':
return super().__new__(cls, name, bases, attrs, **kwargs)
# 获取__init__函数的 默认值
argspec = inspect.getfullargspec(attrs["__init__"])
init_defaults = dict(zip(argspec.args[-len(argspec.defaults):], argspec.defaults))
cls.__init_defaults = init_defaults
attrs['__init_defaults__'] = init_defaults
return super().__new__(cls, name, bases, attrs, **kwargs)抽象父类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 #!/usr/bin/env Python
# -- coding: utf-8 --
"""
@version: v0.1
@author: narutohyc
@file: meta_interface.py
@Description:
@time: 2020/6/15 20:29
"""
from abc import (ABC,
abstractmethod,
ABCMeta)
class DicMeta(ABC, metaclass=DicMetaClass):
def __init__(self):
pass
def to_dict(self):
'''
返回字典
'''
pass
def load_from_mapping(cls, mapping_datas):
'''
用字典来构建实例对象
'''
assert isinstance(mapping_datas, collections.abc.Mapping)
obj = cls.__new__(cls)
[setattr(obj, k, v) for k, v in mapping_datas.items()]
return obj子类实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 #!/usr/bin/env Python
# -- coding: utf-8 --
"""
@version: v0.1
@author: narutohyc
@file: text_meta.py
@Description:
@time: 2020/5/22 14:55
"""
from augmentation.meta_class.meta_interface import DicMeta
from utils.utils_func import gen_md5, str2bool
import re
class TaskMeta(DicMeta):
'''
数据包装类的bean结构
'''
def __init__(self, text, doc_id, sentence_id, reg_lst,
has_reg=True,
flag=None,
dataset='train',
text_source="primitive"):
super(TaskMeta, self).__init__()
self.text = text
self.doc_id = doc_id
self.sentence_id = sentence_id
if reg_lst and isinstance(reg_lst[0], list):
reg_lst = ['%s %s %s' % (tag, start_idx, value) for tag, start_idx, value in reg_lst]
self.reg_lst = sorted(reg_lst, key=lambda reg: int(re.sub(' +', ' ', reg).split(" ", 2)[1])) if reg_lst else []
self.flag = list(set(i.split(' ', 2)[0] for i in self.reg_lst)) if flag is None else flag
self.has_reg = str2bool(has_reg)
self.dataset = dataset
self.text_source = text_source
self._id = gen_md5(self.text)
def load_from_mapping(cls, mapping_datas):
'''
用字典来构建 TaskMeta实例
'''
obj = super(TaskMeta, cls).load_from_mapping(mapping_datas)
obj._id = gen_md5(obj.text)
[setattr(obj, k, v) for k, v in obj.__init_defaults__.items() if not hasattr(obj, k)]
if obj.flag is None:
obj.flag = list(set(i.split(' ', 2)[0] for i in obj.reg_lst))
obj.has_reg = str2bool(obj.has_reg)
return obj
def to_dict(self):
'''
当该类没有其他多余属性时 可以直接返回self.__dict__的副本
'''
return {"text": self.text,
"doc_id": self.doc_id,
"sentence_id": self.sentence_id,
"reg_lst": self.reg_lst,
"flag": list(self.flag),
"has_reg": self.has_reg,
"dataset": self.dataset,
"text_source": self.text_source,
"_id": self._id}测试类:
1
2
3
4
5
6
7
8
9
10 task_meta_0 = TaskMeta.load_from_mapping({'text': '斯坦福大学开发的基于条件随机场的命名实体识别系统,该系统参数是基于CoNLL、MUC-6、MUC-7和ACE命名实体语料训练出来的。',
'doc_id': 'id1', 'sentence_id': 'id1',
'reg_lst': ['学校 0 斯坦福大学', '标注 33 CoNLL', '标注 39 MUC-6', '标注 45 MUC-7', '标注 51 ACE']})
task_meta_1 = TaskMeta.load_from_mapping({'text': '斯坦福大学开发的基于条件随机场的命名实体识别系统,该系统参数是基于CoNLL、MUC-6、MUC-7和ACE命名实体语料训练出来的。',
'doc_id': 'id1', 'sentence_id': 'id1',
'reg_lst': ['学校 0 斯坦福大学', '标注 33 CoNLL', '标注 39 MUC-6', '标注 45 MUC-7', '标注 51 ACE'],
'flag': ['学校', '标注'], 'has_reg': True,
'dataset': 'train', 'text_source': 'primitive', '_id': '3b895befc659345be8686bd7de4d7693'})
task_meta_0.to_dict == task_meta_1.to_dict
Out[33]: True可以看出,taskmeta_0和task_meta_1两者的 值是完全相同的,这里就可以做到共享\_init__默认参数的效果
注解类的属性
元类还有一个更有用处的功能,那就是可以在某个类刚定义好但是尚未使用的时候,提前修改或注解该类的属性。这种写法通常会与描述符(descriptor) 搭配起来(参见本书第31条),令这些属性可以更加详细地了解自己在外围类中的使用方式。
例如,要定义新的类,用来表示客户数据库里的某- -行。同时,我们还希望在该类的相关属性与数据库表的每一列之间, 建立对应关系。于是,用下面这个描述符类,把属性与列名联系起来。
1
2
3
4
5
6
7
8
9
10
11
12 class Field:
def __init__(self, name):
self.name = name
self.internal_name = '_' + self.name
def __get__(self, instance, owner):
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类,定义该类的时候,我们要为每个类属性指定对应的列名。
1
2
3
4
5 class Customer:
first_name = Field('first_name')
last_name = Field('last_name')
prefix = Field('prefix')
suffix = Field('suffix')问题在于,上面这种写法显得有些重复。在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 构造器。
1
2
3
4
5
6
7
8 class Meta(type):
def __new__(meta, name, bases, class_dict):
for key, value in class_dict.items():
if isinstance(value, Field):
value.name = key
value.internal_name = '_' + key
cls = type.__new__(meta, name, bases, class_dict)
return cls下面定义一一个基类,该基类使用刚才定义好的Meta作为其元类。凡是代表数据库里面某一行的类,都应该从这个基类中继承,以确保它们能够利用元类所提供的功能:
1
2 class DatabaseRow(object, metaclass=Meta):
pass采用元类来实现这套方案时,Field 描述符类基本上是无需修改的。唯一 要调整的地方就在于:现在不需要再给构造器传人参数了,因为刚才编写的Meta.__new__ 方法会自动把相关的属性设置好。
1
2
3
4 class Field:
def __init__(self):
self.name = None
self.internal_name = None有了元类、新的DatabaseRow基类以及新的Field描述符之后,我们在为数据行定义DatabaseRow子类时,就不用再像原来那样,编写重复的代码了。
1
2
3
4
5 class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
ORM例子
代码ORM 是 python编程语言后端web框架 Django的核心思想,“Object Relational Mapping”,即对象-关系映射,简称ORM。
一个句话理解就是:创建一个实例对象,用创建它的类名当做数据表名,用创建它的类属性对应数据表的字段,当对这个实例对象操作时,能够对应MySQL语句
1 | class ModelMetaclass(type): |
1 | Found mapping :uid ==> ('uid', 'int unsigned') |
要点
借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性。
描述符与元类能够有效地组合起来,以便对某种行为做出修饰,或在程序运行时探查相关信息。
如果把元类与描述符相结合,那就可以在不使用weakref模块的前提下避免内存泄漏。