注:本篇文章原发于本人博客http://blog.ideawand.com/,此处为节选部分,如果您对我的文章感兴趣,请阅读至结尾并点击原文链接,以阅读全文。
由于工作需要,最近学习Python元编程方面的东西。本文介绍了Metaclass的一个示例,是笔者在学习过程中编写的一个小例子,虽然只有50行代码,但其中涉及了闭包、元编程等内容,而且具有较高的实用性。 Let's Go!
注:本文以Python3.6为例,早期版本中元类的使用方式与本文所述方式不同。 上述目的也可以通过装饰器或者闭包实现,不过这里是为了学习元类,因此使用元类来做。
为了了解元类是什么,我们先看一个很实用的例子,这样先知道了目标,再去分析后面的原理,大家思路会清晰一些。
我们要实现的目标类似于ORM框架:用一个Python类来表示一个Model,这个Model具有很多属性用来存储数据,我们可以为这些属性设置约束条件(例如数据类型等),当给这些属性进行赋值操作时,会自动根据约束检验数据是否合法,就像下面这样:
假设我们要定义一个叫做News的Model用来代表一片新闻,那么我希望能够这样:
# 定义News类作为保存新闻信息的Model
# SmartAttrBase为News类的基类
# SmartAttrDesc类用来存储各个字段的约束条件
# SmartAttrBase和SmartAttrDesc类的具体实现在后文中会有介绍,目前不必关心。
class News(SmartAttrBase):
title = SmartAttrDesc(default="", type_=str, func=lambda x: x.upper())
content = SmartAttrDesc(default="", type_=str)
publisher = SmartAttrDesc(default="", type_=str)
publish_time = SmartAttrDesc(default=0, type_=int)
上面的News Model具有4个属性,其中3个是字符串类型,1个是整数类型,对于title字段,我们要求无论传入什么内容,都转换为大写形式进行存储。如果提供的数据类型不符,则应抛出异常,如下所示:
>>> news = News()
>>> news.title
''
>>> news.title = "The quick brown fox."
>>> news.title
'THE QUICK BROWN FOX.'
>>> news.publish_time = 1508941663
>>> news.publish_time
1508941663
>>> news.publish_time = "20171025"
TypeError: Proprety [publish_time] need type of <class 'int'> but get <class 'str'>
<!--more-->
在《深刻理解Python中的元类》(原文)这篇参考文献中提到,元类做的事情可以归纳为:
这不就是偷梁换柱嘛。我们来看看元类偷梁换柱的例子。为了有一个直观的理解,我们仍然先不给出背后实现的代码,而是观察最终暴露出来的特性。请看下面的代码:
>>> news = News()
>>> print(type(news.title))
<class 'str'>
>>> print(type(news.publish_time))
<class 'int'>
从之前News
类的定义可以知道,News
类的四个成员(title、content、publisher、publish_time)都是SmartAttrDesc
类的实例,但是News
类的实例中,上述四个成员的类型分别变成了对应的str和int型。
News
类被修改了!!!
这只偷梁换柱修改News
类的幕后黑手是什么东西?—— Metaclass !
来一个小热身,请问下列一行代码的含义是什么?
foo = Foo()
对于上述代码,大家的第一反应肯定是这样的:有一个名叫Foo
的类,我们创建了Foo
类的一个实例,并且将这个新创建出来的实例绑定到变量foo
上面。一般我们会称foo
为一个对象,foo
可以有自己的属性,自己的方法。That's easy!
现在,让我们把脑洞开大一些。在Python中,一切事物都是对象,所以类也是一种对象。换句话说,类也可能是被实例化出来的。那么在上述代码中,foo
是否可以代表一个类,而Foo
是用来生成一个类的类 呢?
用下面的代码来说明:
# 以下代码遵循PEP8推荐的命名规范,类的名称使用大写字母开头,实例使用小写字母。
# Bar虽然是Foo的实例,但由于Bar仍然是一个类,所以Bar使用大写字母开头。
Bar = Foo()
bar = Bar()
请打开你的脑洞:在这里,Foo
是一个类;Bar
是Foo
的实例,但是Bar
不是一个普通的实例,因为Bar
本身也是一个类,Bar
这个类原本不存在,它是由Foo
在运行过程中动态创建出来的一个类;bar
是Bar
的实例。
于是,我们可以说,Foo
类是Bar
类的类,这里的Foo
类就是Bar
类的元类, 元类就是类的类,类就是元类的实例。
看完上面拗口的描述,或多或少会有些懵。那么,先忘记元类这个东西,看点我们熟悉的,冷静一下。
看下面的代码:
>>> a = 1
>>> print(type(a))
<class 'int'>
>>> print(a.__class__)
<class 'int'>
Python中每个实例都有一个__class__属性,这个属性表明了当前的这个对象是由哪个类实例化而来的。同时Python中也有一个函数type()
可以用来检测一个实例是哪个类实例化出来的,一般而言,type(a)会返回a.__class__。
由于Python中任何事物都是对象,哪怕是对于最简单的数字来说都不例外,所以对于上述代码而言,变量a
中所存储的数字1
是int
类的一个实例。
再看下面的代码:
class Foo:
pass
foo = Foo()
print(type(foo))
上述代码的输出值为:
<class '__main__.Foo'>
即表明这里的foo
变量是Foo
类的实例。这太正常了,不是吗?有什么好说的呢?但是,如果我们运行下面的代码,输出会是什么呢?
class Foo:
pass
print(type(Foo))
print(Foo.__class__)
可以看到,上述代码的运行结果为:
<class 'type'>
<class 'type'>
可以看到Foo
是我们自己定义的一个类,并且这个类本身具有__class__属性,这就说明,我们定义的这个Foo
类实际上是一个实例,它对应着一个叫做type
的类,这个Foo
类是type
类的一个实例。这个type
类是何方神圣?
回想前面元类的定义,我们定义了Foo
类,结果发现Foo
是一个叫做type
的类的实例,那就说明,这个type
类是一个元类,用type
可以创造新的类!
由于历史原因,type
关键字在Python中有两种完全不同的含义,Python文档中对type关键字也有详细说明。
上述第一种情况,我们已经使用多次,不再赘述,现在重点看一下第二种形式。当传递给type
三个参数时,三个参数分别是:
Let's paly with type
!
class Foo:
pass
print(Foo)
print(type(Foo))
Bar = type("Bar", tuple(), {})
print(Bar)
print(type(Bar))
上述代码输出为:
<class '__main__.Foo'>
<class 'type'>
<class '__main__.Bar'>
<class 'type'>
可以看到,两种形式分别定义了两个类,这两个类几乎完全一样,单纯从输出结果根本无法判断哪个类是用class关键字定义的,哪个类是用type动态生成的。
让我们继续。
class Base:
def func_in_base(self):
print("I am a func of Base")
class Foo(Base):
def __init__(self, name):
self.name = name
def func_in_subclass(self):
print("I am a func of subclass, my name is %s" % self.name)
def init_function_out_of_class(self, name):
self.name = name
def normal_function_out_of_class(self):
print("I am a func of subclass, my name is %s" % self.name)
Bar = type("Bar", (Base,), {"__init__": init_function_out_of_class,
"func_in_subclass": normal_function_out_of_class})
foo = Foo("Myrfy")
foo.func_in_base()
foo.func_in_subclass()
print(type(foo))
print(type(Foo))
bar = Bar("Myrfy")
bar.func_in_base()
bar.func_in_subclass()
print(type(bar))
print(type(Bar))
上述代码的输出为:
I am a func of Base
I am a func of subclass, my name is Myrfy
<class '__main__.Foo'>
<class 'type'>
I am a func of Base
I am a func of subclass, my name is Myrfy
<class '__main__.Bar'>
<class 'type'>
是不是很神奇?在第6~11行,我们采用传统的方法定义了一个Foo
类;在第14~20行,又用动态创建类的方法创建了Bar
类。Bar
类和Foo
类除名称不同以外,在继承和方法上的表现完全一样。
现在请注意上述输出的第8行。由于Bar
类是使用type
创建出来的,稍微回忆一下之前元类的概念,我们说类是元类的实例,那么在上面的例子里面,type
是一个元类,我们实例化了一个type
元类从而得到了一个叫做Bar
的类,所以上述输出的第8行表明Bar
的类型是type
,也就是Bar
是由type
元类创建的。
但是……为什么第4行的输出和第8行相同?为什么我们使用class关键字定义的类也是type
类的实例?
Python中的一切类都是由type
创建出来的!!!
为了理解这个过程,我们需要看看Python解释器在逐行执行Python代码的时候究竟做了什么。以下列代码为例:
class Foo(Base):
def __init__(self, name):
self.name = name
def func_in_subclass(self):
print("I am a func of subclass, my name is %s" % self.name)
先来看一个简化版本,更细节的操作会在下文提到。
当Python解释器遇到上述第1行后,首先看到了class关键字,于是解释器知道了我们要定义一个类,再往后看,解释器得知类的名字应该叫做Foo,再往后看,解释器发现这个类有一个父类,叫做Base。于是在第一行处理完毕后,解释器得到了两个信息:name和bases,即上文传入type
的前两个参数。
随后,解释器会创建一个空的字典,准备存储class body的有关信息,该字典将会成为传入type
的第三个参数。解释器继续读入上述代码的第2行,得知有一个叫做__init__的函数,于是解释器继续向下读入__init__的body的代码,创建出一个函数对象(function object),然后将其插入刚才创建的字典中,取名为"__init__"。 同样的,解释器继续向下,创建func_in_subclass对应的函数对象并将其插入字典。
函数在Python中也是一种对象,函数对象内存储了函数的名称、所属的模块以及对应的指令字节码等信息。当Python解释器遇到def关键字时,会在内存中创建对应的函数对象,并把函数体内的代码转换为Python字节码存储在函数对象中。
至此,调用type
来创建一个类所需的材料都已经准备好了,Python解释器会调用type
来创建出来名为Foo
,基类为Base
,并且具有__init__和func_in_subclass两个方法的类。
所以,这里出现了一个令人震惊的真像:type
类是Python中所有类的元类。平时,我们没有注意到type
的存在,是因为Python解释器将type
作为默认的元类。
type
是Python中很特殊的一个东西,同时也是很基础的东西:Python中用户定义的一切类最终都是通过type
来创建的。
type
之所以可以有如此强大的法力,是因为对type
的调用会导致对type
类的__new__方法的调用,而该方法直接对应着Python解释器C语言代码中的一段程序,这段程序的作用是创建一个新的类型。凡是在Python语言中创建新类型的操作(也就是定义一个类),其最终都会转变为对解释器中相应代码段的调用,从而在内存中分配存储新类型的空间,创建新的结构体用来表示新创建的类型等等。
type
在Python中会有一些特殊表现是其他任何Python类无法具备的,例如type
类的元类是type
本身。这是写在Python解释器代码里的一段特殊逻辑,除type
类以外,没有任何类的元类是它自己。
用幽默一点的话来说就是,type
类有强大的靠山,它的实现逻辑是直接写死在解释器的代码中的!所以type
可以实现一些其他类所不能实现的东西。
节选部分到此为止,阅读原文请访问http://blog.ideawand.com/2017/10/25/smart-attribute-using-metaclass-in-50-lines-of-python-code/ 原始博客建立在github page,如果访问受阻,也可以关注我的微信公众号【极客幼稚园】阅读
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。