前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python之Metaclass元类详解与实战:50行代码实现【智能属性】

Python之Metaclass元类详解与实战:50行代码实现【智能属性】

原创
作者头像
用户5410317
修改2021-01-20 15:19:33
2810
修改2021-01-20 15:19:33
举报

注:本篇文章原发于本人博客http://blog.ideawand.com/,此处为节选部分,如果您对我的文章感兴趣,请阅读至结尾并点击原文链接,以阅读全文。

0x00 背景

由于工作需要,最近学习Python元编程方面的东西。本文介绍了Metaclass的一个示例,是笔者在学习过程中编写的一个小例子,虽然只有50行代码,但其中涉及了闭包、元编程等内容,而且具有较高的实用性。 Let's Go!

注:本文以Python3.6为例,早期版本中元类的使用方式与本文所述方式不同。 上述目的也可以通过装饰器或者闭包实现,不过这里是为了学习元类,因此使用元类来做。

0x01 我们的目标(没有蛀 牙Bug)

为了了解元类是什么,我们先看一个很实用的例子,这样先知道了目标,再去分析后面的原理,大家思路会清晰一些。

我们要实现的目标类似于ORM框架:用一个Python类来表示一个Model,这个Model具有很多属性用来存储数据,我们可以为这些属性设置约束条件(例如数据类型等),当给这些属性进行赋值操作时,会自动根据约束检验数据是否合法,就像下面这样:

假设我们要定义一个叫做News的Model用来代表一片新闻,那么我希望能够这样:

代码语言:txt
复制
# 定义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字段,我们要求无论传入什么内容,都转换为大写形式进行存储。如果提供的数据类型不符,则应抛出异常,如下所示:

代码语言:txt
复制
>>> 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-->

0x02 偷梁换柱

《深刻理解Python中的元类》(原文)这篇参考文献中提到,元类做的事情可以归纳为:

  • 拦截类的创建
  • 修改类
  • 返回修改之后的类

这不就是偷梁换柱嘛。我们来看看元类偷梁换柱的例子。为了有一个直观的理解,我们仍然先不给出背后实现的代码,而是观察最终暴露出来的特性。请看下面的代码:

代码语言:txt
复制
>>> 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 !

0x03 小开脑洞

来一个小热身,请问下列一行代码的含义是什么?

代码语言:txt
复制
foo = Foo()

对于上述代码,大家的第一反应肯定是这样的:有一个名叫Foo的类,我们创建了Foo类的一个实例,并且将这个新创建出来的实例绑定到变量foo上面。一般我们会称foo为一个对象,foo可以有自己的属性,自己的方法。That's easy!

现在,让我们把脑洞开大一些。在Python中,一切事物都是对象,所以类也是一种对象。换句话说,类也可能是被实例化出来的。那么在上述代码中,foo是否可以代表一个类,而Foo用来生成一个类的类 呢?

用下面的代码来说明:

代码语言:txt
复制
# 以下代码遵循PEP8推荐的命名规范,类的名称使用大写字母开头,实例使用小写字母。
# Bar虽然是Foo的实例,但由于Bar仍然是一个类,所以Bar使用大写字母开头。
Bar = Foo()
bar = Bar()

请打开你的脑洞:在这里,Foo是一个类;BarFoo的实例,但是Bar不是一个普通的实例,因为Bar本身也是一个类,Bar这个类原本不存在,它是由Foo在运行过程中动态创建出来的一个类;barBar的实例。

于是,我们可以说,Foo类是Bar类的类,这里的Foo类就是Bar类的元类, 元类就是类的类,类就是元类的实例。

0x04 冷静一下

看完上面拗口的描述,或多或少会有些懵。那么,先忘记元类这个东西,看点我们熟悉的,冷静一下。

看下面的代码:

代码语言:txt
复制
>>> a = 1
>>> print(type(a))
<class 'int'>
>>> print(a.__class__)
<class 'int'>

Python中每个实例都有一个__class__属性,这个属性表明了当前的这个对象是由哪个类实例化而来的。同时Python中也有一个函数type()可以用来检测一个实例是哪个类实例化出来的,一般而言,type(a)会返回a.__class__。

由于Python中任何事物都是对象,哪怕是对于最简单的数字来说都不例外,所以对于上述代码而言,变量a中所存储的数字1int类的一个实例。

再看下面的代码:

代码语言:txt
复制
class Foo:
    pass
foo = Foo()
print(type(foo))

上述代码的输出值为:

代码语言:txt
复制
<class '__main__.Foo'>

即表明这里的foo变量是Foo类的实例。这太正常了,不是吗?有什么好说的呢?但是,如果我们运行下面的代码,输出会是什么呢?

代码语言:txt
复制
class Foo:
    pass
print(type(Foo))
print(Foo.__class__)

可以看到,上述代码的运行结果为:

代码语言:txt
复制
<class 'type'>
<class 'type'>

可以看到Foo是我们自己定义的一个类,并且这个类本身具有__class__属性,这就说明,我们定义的这个Foo类实际上是一个实例,它对应着一个叫做type的类,这个Foo类是type类的一个实例。这个type类是何方神圣?

回想前面元类的定义,我们定义了Foo类,结果发现Foo是一个叫做type的类的实例,那就说明,这个type类是一个元类,用type可以创造新的类!

0x05 type的历史

由于历史原因,type关键字在Python中有两种完全不同的含义,Python文档中对type关键字也有详细说明。

  • 当type后面只有一个参数时,type是一个内置函数,用来返回一个对象所属的类
  • 当type后面有3个参数时,type代表一个类,这个类在初始化时接受3个参数,并且返回一个新的type类的实例,实质上就是动态定义了一个类。

上述第一种情况,我们已经使用多次,不再赘述,现在重点看一下第二种形式。当传递给type三个参数时,三个参数分别是:

  • name: 一个字符串,要动态创建的类的名字,这个参数会成为新创建的类的__name__属性
  • bases: 一个tuple,要动态创建的类的基类列表,这个参数会成为新类的__bases__属性
  • dict: 一个字典,要创建出来的类的具体内容,该字典的内容最后会成为新类的__dict__属性

Let's paly with type!

代码语言:txt
复制
class Foo:
    pass
print(Foo)
print(type(Foo))

Bar = type("Bar", tuple(), {})
print(Bar)
print(type(Bar))

上述代码输出为:

代码语言:txt
复制
<class '__main__.Foo'>
<class 'type'>
<class '__main__.Bar'>
<class 'type'>

可以看到,两种形式分别定义了两个类,这两个类几乎完全一样,单纯从输出结果根本无法判断哪个类是用class关键字定义的,哪个类是用type动态生成的。

让我们继续。

代码语言:txt
复制
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))

上述代码的输出为:

代码语言:txt
复制
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类的实例?

0x06 类是怎么创建出来的?

Python中的一切类都是由type创建出来的!!!

为了理解这个过程,我们需要看看Python解释器在逐行执行Python代码的时候究竟做了什么。以下列代码为例:

代码语言:txt
复制
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的一些说明

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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00 背景
  • 0x01 我们的目标(没有蛀 牙Bug)
  • 0x02 偷梁换柱
  • 0x03 小开脑洞
  • 0x04 冷静一下
  • 0x05 type的历史
  • 0x06 类是怎么创建出来的?
    • 关于type的一些说明
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档