《流畅的Python》笔记。
本篇主要讨论Python中的元类。Python中所有的类都直接或间接地是元类type的实例。阅读本篇时,请时刻注意你所阅读的内容指的是**"实例"(或者说"对象")、"类"还是"元类"**,否则很容易被绕晕。
1. 前言
Python中,几乎所有的东西都是对象。不光类的实例是对象,连类本身也是对象。
不像C++、Java等静态语言,在编译前就必须将类定义好,运行时不能再创建新类,Python则可以在运行时动态创建新类,且不通过关键字class
。创建类的类叫做元类。元类也是类,它可以派生出新的元类,但所有元类最顶层的超类只有一个,就是我们经常用到的type
。Python中所有的类都直接或间接地是type
的实例。
在运行时能通过元类动态创建类是Python的魅力,但想要理解这个"魅力"确并不是那么容易。本篇内容主要有:元类的简单示例,类装饰器,元类的原理、定义及使用方式,最后使用元类来弥补中描述符的不足。
本篇只能算是对元类的初步介绍,更深层次的内容还需进一步学习。
2. 初识元类
通常,如果要创建对象,需要先在某个模块中用class
关键字定义好类,再在业务代码中创建这个类的实例。与这种事先定义的方式相反,可以通过type
在运行时创建类,以下是它的示例:
>>> a = "this is a string">>> type(a)>>> MyClass = type("MyClass", (object,), {"x": 1, "x2": lambda self: self.x * 2})>>> mc = MyClass()>>> mc.x1>>> mc.x2()2 # 请留意下方这三个特殊属性>>> mc.__class__ # __class__的值是实例所属的类 >>> MyClass.__bases__ # __bases__的值是类的所有直接超类( ,)>>> MyClass.__mro__ # __mro__的值是类的所有超类( , ) >>> MyClass.__class__ # 这表明MyClass这个类是type的对象 复制代码
上述MyClass
的定义等同于如下定义:
class MyClass(object): x = 1 def x2(self): return self.x * 2复制代码
type
通常被当做函数使用,但它其实是一个类。当只传入一个实例时,它返回实例的类型;当传入3个参数时,它生成并返回一个类:
type(cls_name, bases, attr_dict)复制代码
其中:
cls_name
是要创建的类的名称的字符串;bases
是一个元组,它存储即将创建的类的直接父类们,比如MyClass
继承自object
(如果只继承自object
,可以将bases
设置为空元组);attr_dict
是新类的属性字典。不光包括数据属性,还包括了方法(含特殊方法)。不过,如果是数据属性,这些数据属性将成为类属性,而不是实例属性。如果想创建实例属性,请在attr_dict
中传入__init__
的定义,或者传入__dict__
。
为了更详细的介绍type
的用法,我们用它来构造一个类工厂函数。
3. 类工厂函数
对于数据结构固定的数据,如果想将其包装成类对象,传统的做法是使用class
定义每个类,比如为宠物应用定义各种动物类:
class Dog: def __init__(self, name, weight, owner): self.name = name self.weight = weight self.owner = owner复制代码
不知道各位在敲这段代码时有没有抱怨:name
,weight
,owner
敲了三遍!如果再多几种动物类,这种样板代码得写多少?当然,对于相关的类可以选择继承。但如果数据间不相关呢?难道定义每个类的时候都将这种样板代码敲一遍?这个时候就可以用类工厂函数来减少这种样板代码。下方代码展示了type
更具体的用法,生成的类比较简单,适合用于处理格式固定的数据。这个工厂函数其实是在模仿collections.namedtuple
:
def record_factory(cls_name, field_name): try: # 假设传入的field_name是字符串,获取属性名 field_names = field_name.replace(",", " ").split() except AttributeError: pass # 如果不是字符串,则当做可迭代对象处理 field_names = tuple(field_names) # 将属性名存到元组中 # __init__不作用于这个工厂函数!这是为要创建的类定义的构造方法 def __init__(self, *args, **kwargs): attrs = dict(zip(self.__slots__, args)) attrs.update(kwargs) for name, value in attrs.items(): setattr(self, name, value) def __iter__(self): # 让即将创建的类可迭代 for name in self.__slots__: yield getattr(self, name) def __repr__(self): # 格式化输出 values = ", ".join("{}={!r}".format(*i) for i in zip(self.__slots__, self)) return "{}({})".format(self.__class__.__name__, values) # 类将拥有的属性 cls_attrs = dict(__slots__=field_names, __init__=__init__, __iter__=__iter__, __repr__=__repr__) return type(cls_name, (), cls_attrs) # 继承自object复制代码
下面是这个类工厂函数的用法:
>>> Dog = record_factory("Dog", "name weight owner")>>> dog = Dog("test", 5, "Kevin")>>> dogDog(name='test', weight=5, owner='Kevin')>>> dog.weight = 6>>> dogDog(name='test', weight=6, owner='Kevin')>>> name, weight, owner = dog>>> name, weight, owner('test', 6, 'Kevin')复制代码
下面我们将进一步了解元类。
4. 元类
面向对象的思想有两大关系:类的继承和类的实例化。在Python中,type
和object
就像两个紧密合作的管理员,type
主管实例化,object
主管继承。
我们都知道,Python中所有的类都是从object
继承而来的。但如果你看过下方的代码后,不知道对这一点的理解会不会动摇:
>>> type.__bases__(,)>>> type.__class__ >>> object.__bases__()>>> object.__class__ 复制代码
这段代码翻译成中文就是:object
是type
的实例,type
是object
的子类,因此type
也是type
自身的实例。这里完美地扯出了一个"先有蛋还是先有鸡"的问题:既然object
是type
的实例,那就得先由type
创建object
;但type
又是object
的子类,也就是得先有object
,再有type
,所以到底是谁先有?
这个关系直到现在我也没搞清楚。如果是现实世界,可以说人类暂时还没搞清楚是先有鸡还是先有蛋,但Python这种编程语言可是人造的东西,况且底层还是C语言,所以我肯定不信什么"互为祖先,同时产生"这种说法,而且这在代码里不是死循环吗?查了很多资料,基本都是引用的这张图,其中虚线表示实例关系,实线表示继承关系:
但大家都回避了前面那个问题:object
从type
实例化而来,可type
又从object
派生而来,到底谁先存在?
只能去看源码。源码中type
确实继承自object
,但object
的定义中并没有metaclass
关键字出现(后面会讲到,如果以class
的形式从元类实例化类,需要使用这个关键字);并且,object
中有这么一个定义:
__class__ = None # (!) forward: type, real value is ''复制代码
这就让疑惑更深了:object
究竟是不是type
的实例?type
中有如下定义:
__bases__ = ( object, ) __base__ = object __mro__ = ( None, # (!) forward: type, real value is '' object, )复制代码
更深层的源代码暂时还啃不动。官方中说明了类的构建过程:所有的类,不管指没指明元类,都会经由type()
,产生实际使用的类,这也验证了所有的类都是type
的实例这一说法。
这两者的具体关系还有待继续研究。但我们毕竟不是语言专家,我们更看重的是元类怎么使用。对于这些概念,我们只需要知道:
- 元类
type
可以创建类; - 所有的类都直接或间接的是
type
的实例; type
是自身的实例;type
可以被继承,用于创建新的元类。
4.1 类装饰器
在继续元类之前,我们先来解决属性描述符没有解决的问题:储存属性需要手动指定,而自动生成的名称所表达的意思又不够明显:
>>> Food.weight.storage_name'_Quantity#0'复制代码
这是文章中自动生成储存属性的名称时采用的策略,但我们更希望是下面这种形式:
>>> Food.weight.storage_name'_Quantity#weight'复制代码
上一篇中也说过,描述符类很难获取托管类的类属性名称的。使用类装饰器则能解决这个问题。类装饰器和函数装饰器非常相似,是参数为类对象的函数,返回原来的类或修改后的类。这里我们将它装饰到Food
类上,而不是Quantity
类上(Food
和Quantity
的具体定义请查看文章。以下代码不能直接运行,请自行导入所需的类):
@entityclass Food: # 这个类比上一篇有所省略 weight = Quantity() # 并没有传入储存属性的名称 def __init__(self, weight): self.weight = weightdef entity(cls): for key, attr in cls.__dict__.items(): if isinstance(attr, Validated): # 如果这个属性是Validated类的实例 type_name = type(attr).__name__ # 则修改它的storage_name属性的值 attr.storage_name = "_{}#{}".format(type_name, key) return cls复制代码
其实实现的思路很简单:Quantity
之所以无法获取Food
类的属性名,是因为在Food
中生成Quantity
实例时,Food
这个类都还没有创建完毕,自然只能手动传入。那等Food
类创建完毕了再设置值不就行了?与函数装饰器类似,类装饰器会在Food
生成后立即运行。
也可以用类装饰器来替换掉类中的某些方法。但类装饰器有一个重大缺点:只能对直接依附的类有效。这意味着,被装饰类的子类不一定继承装饰器所做的修改,具体情况视改动的方式而定。
小插曲:我看到这个概念的时候,无法理解为什么这被称之为"缺点":继承的时候子类重写了父类的同名方法,这不再正常不过吗?难道是不准让子类重写这个方法,要让这个方法在整个继承体系中都保持唯一?那不重写不就完了吗?如果整个项目就一个人做,当然能保证不重写这个方法。但往往软件开发是个团队项目,其他人并不一定清楚这个方法能不能被重写。
要保持方法在整个继承体系中保持唯一,不被子类所覆盖,这就得祭出元类。
4.2 使用元类
当使用到元类的时候,其实就是在定制化类及其子类的行为。下面我们使用元类替换掉前面的类装饰器:
# Validated和Quantity都在上一篇文章中,以下代码不能直接运行!class EntityMeta(type): # 在元类中,通常将self换成cls def __init__(cls, name, bases, attr_dict): # 逻辑和类装饰器是一样的 super().__init__(name, bases, attr_dict)# 这一步将name,bases,attr_dict绑定到了cls上 for key, attr in attr_dict.items(): if isinstance(attr, Validated): type_name = type(attr).__name__ attr.storage_name = "_{}#{}".format(type_name, key)class Entity(metaclass=EntityMeta): """带有验证字段的业务实体""" # 什么都不用做,除非像添加新方法class Food(Entity): # 对这个类进行了简化 weight = Quantity() def __init__(self, weight): self.weight = weight复制代码
请注意区分这些类的关系:
EntityMeta
是元类type
的子类,所以它也是个元类。Entity
使用class
关键字来定义新的类,而不是调用type()
函数来创建新的类;在定义Entity
时,使用了metaclass
关键字,表明这是元类EntityMeta
的实例,而不是EntityMeta
的子类,即,这不是继承关系。同时,Entity
也(间接)是type
的实例。Food
是Entity
的子类,也是元类EntityMeta
和type
的实例。
列出这些关系,是想提醒大家,如果要自行定义元类,请时刻注意,究竟谁是谁的子类,谁是谁的实例。下面来运行一下这段代码:
>>> Food.weight.storage_name'_Quantity#weight' # 行为符合预期复制代码
这里又产生了3个问题。
第1个问题是:从EntityMeta
的__init__
中可以看到,参数cls
存的是元类的实例的引用,即类Entity
或者类Food
的引用,但整个初始化过程中,根本就没有用到cls
,可结果表明,这些修改确实作用到了Food
上,那么这是怎么作用Food
上的呢?
EntityMeta.__init__()
这个方法中的语句并不多,简答分析就能知道,问题出在super().__init__()
,即type.__init__()
上。但这个方法的具体实现我暂时也不知道,只能给出我的猜想:我们都知道,对于普通的类(例如Food
)来说,它的对象(例如f
)保存在内存中的某个位置a
上,Food
的__init__
操作内存a
上的数据;而开篇就提到,所有的类也都是对象,于是类比一下,元类(例如EntityMeta
)的实例(例如Food
)肯定也存储在内存的某个位置b
,那么type.__init__
肯定将传入的参数关联到了内存b
(实例Food
)上。所以,这些操作最后在Food
上生效了。平时对类的使用,其实就是用内存b
中的数据创建内存a
中的数据。
元类之所以难理解,难就难在我们并不习惯于"根据类来创建类"这种思路,我们习惯的是"根据类来创建实例"。这里也再次申明,没有"根据类来创建类"这种说法,一切都是"根据类来创建实例"!当涉及到元类时,就把元类看做平常使用的类,把元类生成的类看做平常使用的实例(或者说对象)。如果能这样想,下面两个问题也就能很好回答:
- 两个
__init__
谁先运行呢? - 前面说到,在元类中定义的方法能影响到整个继承体系,即使子类重写这个方法也没有用,那这是怎么做到的呢?
要彻底回答这两个问题,就要涉及到运行时与导入时的概念。
5. 运行时&导入时
为了正确地做元编程,必须知道Python解释器在什么时候运行各个代码块。Python程序员区分运行时与导入时这两个概念,但其实这两个术语并没有严格定义,而且两者有交集。
在导入时,解释器会编译源文件,解释器会从上到下一次性解析完整个.py
模块,然后生成用于执行的字节码,并将其存到.pyc
文件中(这类文件在本地的__pycache__
文件夹中)。所以,虽然Python是解释型语言,边解释边运行,但解释的不是.py
源文件,而是.pyc
中的字节码数据。
导入时除了编译,还会做些其他的事情。由于Python中的语句几乎都是可执行的,稍不注意,某些本该在运行时才运行的语句会在导入时就运行,导致用户程序的状态被修改。这里指的就是import
语句:
- 在Java中,
import
只用作声明,运行的时候才真正执行import
后面的包中的代码。 - 在Python中,
import
不仅仅是声明。模块首次被导入时,模块中所有的代码都会被运行并缓存,以后再导入相同的模块时直接使用缓存(只做名称绑定);所导入的模块中如果还有import
,那么这些模块只要没被导入过,也会被运行一遍。这表明,运行时与导入时产生了交集。
之前我在网上搜索这个概念的时候,很多博主都说,导入时会运行模块中所有的代码。其实并不是这样的,Python解释器确实会将模块从头执行到尾,但是:
- 对于函数,解释器只执行完
def
关键字所在的行,它会编译函数的定义体,把函数对象绑定到对应的全局名称上,但显然函数定义体不会被执行。只有到运行时,解释器才通过全局名称找到函数定义体,再执行它。 - 对于类,情况就不一样了。导入时,解释器会执行每个类的定义体,甚至会执行嵌套类的定义体。这么做的结果就是,定义了类的属性和方法(方法的定义体依然不会被执行),并构建了类这个对象。
绕了这么一大圈,终于和元类发生了关系!类这个对象在导入时就会被创建,所以,元类在导入时就会初始化它的实例:类。
为了更真切的体验这个过程,下面创建几个类,并观察解释器在导入时和运行时的行为。
下面的代码会用到类装饰器和元类,前面说到类装饰器在子类中不一定起作用,但元类一定起作用,请留意这两个的行为。
5.1 一般情况
这里指没有元类的情况。首先创建两个模块,代码可能有点长,但都很简单。注意这两个模块的名称,首先是evaltime.py
:
# evaltime.pyfrom evalsupport import deco_alphaprint('<[1]> evaltime module start')class ClassOne: # 它嵌套了一个类 print('<[2]> ClassOne body') def __init__(self): print('<[3]> ClassOne.__init__') def __del__(self): print('<[4]> ClassOne.__del__') def method_x(self): print('<[5]> ClassOne.method_x') class ClassTwo(object): print('<[6]> ClassTwo body')@deco_alphaclass ClassThree: # 它被类装饰器装饰 print('<[7]> ClassThree body') def method_y(self): # 注意才场景2中观察这个方法的行为 print('<[8]> ClassThree.method_y')class ClassFour(ClassThree): # 这里有一个继承,ClassThree被类装饰器装饰过 print('<[9]> ClassFour body') def method_y(self): # 注意才场景2中观察这个方法的行为 print('<[10]> ClassFour.method_y')if __name__ == '__main__': print('<[11]> ClassOne tests', 30 * '.') one = ClassOne() one.method_x() print('<[12]> ClassThree tests', 30 * '.') three = ClassThree() three.method_y() print('<[13]> ClassFour tests', 30 * '.') four = ClassFour() four.method_y()print('<[14]> evaltime module end')复制代码
接着是evalsupport.py
:
# evalsupport.pyprint('<[100]> evalsupport module start')def deco_alpha(cls): print('<[200]> deco_alpha') def inner_1(self): print('<[300]> deco_alpha:inner_1') cls.method_y = inner_1 return clsclass MetaAleph(type): print('<[400]> MetaAleph body') def __init__(cls, name, bases, dic): print('<[500]> MetaAleph.__init__') def inner_2(self): print('<[600]> MetaAleph.__init__:inner_2') cls.method_z = inner_2 # 实例中的这个属性如果有,则会被替换 # 如果没有,则新建这个属性并赋值为内嵌函数inner_2的引用print('<[700]> evalsupport module end')复制代码
上面这两个模块的代码中有<[N]>
标记,N
表示数字。现在请大家模拟以下两种场景,记录标记出现的顺序,最后再和真实结果比较。
场景1:在Python控制台中以交互的方式导入evaltime.py
模块,即
>>> import evaltime.py复制代码
场景2:在命令行中运行evaltime.py
模块,即
$ python3 evaltime.py复制代码
建议模拟完后再看下面的结果:
# 场景1>>> import evaltime.py<[100]> evalsupport module start # 运行evalsupport.py模块<[400]> MetaAleph body # MetaAleph的定义体运行了<[700]> evalsupport module end # 函数deco_alpha定义体在导入时并没有被执行!<[1]> evaltime module start # 开始运行evaltime.py模块<[2]> ClassOne body # ClassOne的定义体被运行了,但其中的方法没有被运行<[6]> ClassTwo body # 嵌套的ClassTwo的定义体也被运行了<[7]> ClassThree body # ClassThree的定义体被执行。<[200]> deco_alpha # 跳到了类装饰器中,函数定义体在导入时被执行了!证明导入时创建了类对象<[9]> ClassFour body # 类定义体被执行<[14]> evaltime module end # 模块执行完毕# 场景2$ python3 evaltime.py<[100]> evalsupport module start<[400]> MetaAleph body<[700]> evalsupport module end<[1]> evaltime module start<[2]> ClassOne body<[6]> ClassTwo body<[7]> ClassThree body<[200]> deco_alpha<[9]> ClassFour body # 在此行之前,和导入时没有区别,毕竟要执行得先导入嘛<[11]> ClassOne tests .............................. # 开始执行if中的内容了<[3]> ClassOne.__init__ # 初始化ClassOne<[5]> ClassOne.method_x # 调用ClassOne的method_x方法<[12]> ClassThree tests .............................. <[300]> deco_alpha:inner_1 # ClassThree的method_y被替换了<[13]> ClassFour tests ..............................<[10]> ClassFour.method_y # 类装饰器在子类上不起作用<[14]> evaltime module end # 模块运行结束<[4]> ClassOne.__del__ # ClassOne在被销毁时调用__del__方法复制代码
不知大家的模拟是否和结果一致?
场景2中的结果证明了,类装饰器在子类中不一定起作用。
两个场景中,类装饰器在导入时都运行了一次,这证明了类对象在导入时创建,而不是在运行时创建。
5.2 加入元类
还剩下的两个问题将在这个例子中找到答案。不过,还得再创建一个模块evaltime_meta.py
,并建议大家回顾一下MetaAleph
的实现:
# evaltime_meta.pyfrom evalsupport import deco_alpha, MetaAlephprint('<[1]> evaltime_meta module start')@deco_alphaclass ClassThree(): # 被类装饰器装饰 print('<[2]> ClassThree body') def method_y(self): print('<[3]> ClassThree.method_y')class ClassFour(ClassThree): print('<[4]> ClassFour body') def method_y(self): print('<[5]> ClassFour.method_y')class ClassFive(metaclass=MetaAleph): # 它是元类MetaAleph的实例! print('<[6]> ClassFive body') def __init__(self): print('<[7]> ClassFive.__init__') def method_z(self): print('<[8]> ClassFive.method_y')class ClassSix(ClassFive): # 它也是元类MetaAleph的实例! print('<[9]> ClassSix body') def method_z(self): print('<[10]> ClassSix.method_y')if __name__ == '__main__': print('<[11]> ClassThree tests', 30 * '.') three = ClassThree() three.method_y() print('<[12]> ClassFour tests', 30 * '.') four = ClassFour() four.method_y() print('<[13]> ClassFive tests', 30 * '.') five = ClassFive() five.method_z() print('<[14]> ClassSix tests', 30 * '.') six = ClassSix() six.method_z()print('<[15]> evaltime_meta module end')复制代码
还是那两个场景:
场景1:在Python控制台中导入evaltime_meta.py
>>> import evaltime_meta.py复制代码
场景2:在命令行中运行evaltime_meta.py
$ python3 evaltime_meta.py复制代码
以下是两个场景的结果:
# 场景1>>> import evaltime_meta.py<[100]> evalsupport module start<[400]> MetaAleph body<[700]> evalsupport module end<[1]> evaltime_meta module start<[2]> ClassThree body<[200]> deco_alpha<[4]> ClassFour body # 到这里为止,和上一个场景1的情况一样<[6]> ClassFive body # 执行了ClassFive定义体<[500]> MetaAleph.__init__ # 元类中的初始化方法在导入时被执行了!也证明导入时创建了类对象<[9]> ClassSix body<[500]> MetaAleph.__init__ # 再次触发元类中的初始化方法,<[15]> evaltime_meta module end# 场景2$ python3 evaltime_meta.py<[100]> evalsupport module start<[400]> MetaAleph body<[700]> evalsupport module end<[1]> evaltime_meta module start<[2]> ClassThree body<[200]> deco_alpha<[4]> ClassFour body<[6]> ClassFive body<[500]> MetaAleph.__init__<[9]> ClassSix body<[500]> MetaAleph.__init__ # 到此行位置,和场景1的情况一样<[11]> ClassThree tests ..............................<[300]> deco_alpha:inner_1 # 方法被类装饰器替换<[12]> ClassFour tests ..............................<[5]> ClassFour.method_y # 类装饰器对子类不起作用<[13]> ClassFive tests ..............................<[7]> ClassFive.__init__ # 初始化ClassFive的实例five<[600]> MetaAleph.__init__:inner_2 # 方法被替换<[14]> ClassSix tests ..............................<[7]> ClassFive.__init__ # 初始化ClassFive的子类ClassSix的实例six<[600]> MetaAleph.__init__:inner_2 # 子类的方法也被替换了!<[15]> evaltime_meta module end复制代码
这组例子再一次证明了类对象在导入时创建!并且元类对它的类对象的初始化也在导入时进行。其实,导入时对于元类来说就是它的运行时。
现在来回答之前留下的两个问题:
- 元类只要有实例,元类的
__init__
方法就一定先于实例的__init__
方法先执行。比较这两者的__init__
方法有些牵强,毕竟类对象(例如ClassFive
)在运行时创建,因此元类的__init__
方法必定在导入时执行;而类实例在运行时才创建,类对象的__init__
方法也就只能在运行时才执行。其实就是一个显而易见的逻辑:什么时候创建"实例",就什么时候执行"类"中的__init__
方法咯。不过得清楚,这里的"实例"和"类"究竟指代的是谁。 - 上一条解释其实已经回答了"元类为什么能覆盖所有子类的方法"。
ClassFive
是元类MetaAleph
的实例,而不是继承自MetaAleph
;ClassSix
虽继承自ClassFive
,但又不是继承自MetaAleph
,它仅仅只是MetaAleph
的又一个实例而已。这里说的覆盖对元类而言根本就不是覆盖,元类仅仅只是在为它的实例的属性赋值而已:你(ClassSix
)只是我(MetaAleph
)的实例,你继承自我的另一个实例(ClassFive
),又不是继承自我,所以你跟我谈什么继承与覆盖?我只是在给你的属性赋值而已!
本文对元类的介绍到此结束。这些内容仅仅只是元类的皮毛。其中有很多地方依然没有弄懂,继续努力吧!
5.3 补充
其实如果想弥补本文中类装饰器的缺陷,可以不用定义元类,现在有更方便的方法:定义特殊方法__init_subclass__
。它的作用和本文中的元类一样,但比创建元类简单直观得多。在创建子类时,子类都会被这个方法初始化。
6. 总结
本文首先展示了元类的基本用法:直接用type()
函数创建类,然后将元类用到了类工厂函数中。之后加入了一个小插曲,类装饰器;接着深入介绍了元类的概念性知识,并展示了如何使用class
和metaclass
关键字从元类创建类。最后,介绍了运行时与导入时的概念,并通过代码展示了这两者的区别,尤其展示了类的创建时间。
迎大家关注我的微信公众号"代码港" & 个人网站 ~