专栏首页极客幼稚园 文章汇总Python之Metaclass元类详解与实战:50行代码实现【智能属性】
原创

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

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

0x00 背景

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

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

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

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

我们要实现的目标类似于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-->

0x02 偷梁换柱

《深刻理解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 !

0x03 小开脑洞

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

foo = Foo()

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

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

用下面的代码来说明:

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

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

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

0x04 冷静一下

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

看下面的代码:

>>> a = 1
>>> print(type(a))
<class 'int'>
>>> print(a.__class__)
<class 'int'>

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

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

再看下面的代码:

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可以创造新的类!

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!

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类的实例?

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

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的一些说明

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,如果访问受阻,也可以关注我的微信公众号【极客幼稚园】阅读

微信公众号:极客幼稚园

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Python黑魔法之metaclass

    Python 有很多黑魔法,为了不分你的心,今天只讲 metaclass。对于 metaclass 这种特性,有两种极端的观点:

    somenzz
  • python metaclass ..

    关于Python2.x中metaclass这一黑科技,我原以为我是懂的,只有当被打脸的时候,我才认识到自己too young too simple someti...

    用户5760343
  • 再有人问什么是元类,就把这篇文章扔给他!

    我之前在深入理解python中的类和对象中说过,python中的类也是一个对象,可以说是类对象,可以由 type() 来创建类对象的。有了这个知识我们先看看下面...

    sergiojune
  • 改变python对象规则的黑魔法metaclass

    看到标题,你可能会想改变类的定义有什么用呢?什么时候才需要使用metaclass呢?

    快学Python
  • 比Python更牛的语言有吗?看我用元类(metaclass)花式创建Python类

    为了让更多的人看到本文,请各位读者动动小手,点击右上角【...】,将本文分享到朋友圈,thanks!

    蒙娜丽宁
  • How does it work - with_metaclass

    我在看源代码的时候,经常蹦出这一句:How does it work! 竟然有这种操作?本系列文章,试图剖析代码中发生的魔法。顺便作为自己的阅读笔记,以作提高。

    岂不美哉Frost
  • Python中的元编程

    就像元数据是关于数据的数据一样,元编程是编写程序来操作程序(Just like metadata is data about data, metaprogram...

    py3study
  • 每天一道 python 面试题 - Python中的元类(metaclass) 详细版本

    在理解元类之前,您需要掌握Python的类。Python从Smalltalk语言中借用了一个非常特殊的类概念。

    公众号---人生代码
  • type与元类

    用户1733462
  • 两句话掌握 Python 最难知识点:元类

    运维行业正在变革,推荐阅读:30万年薪Linux运维工程师成长魔法 千万不要被所谓“元类是99%的python程序员不会用到的特性”这类的说辞吓住。因为每个中国...

    小小科
  • 两句话轻松掌握 Python 最难知识点

    千万不要被所谓"元类是99%的python程序员不会用到的特性"这类的说辞吓住。因为每个中国人,都是天生的元类使用者

    IT派
  • 两句话轻松掌握 python 最难知识点——元类

    千万不要被所谓“元类是99%的python程序员不会用到的特性”这类的说辞吓住。因为每个中国人,都是天生的元类使用者 学懂元类,你只需要知道两句话: 道生一,...

    小小科
  • day 25-1 接口类、抽象类、多态

    这是三种动物 tiger      走路  游泳 swan     走路  游泳 飞 oldying  走路  飞

    py3study
  • 类和对象的创建过程(元类,__new__,__init__,__call__)

    一、 type() 1、创建类的两种方式 方式一 class MyClass(object): def func(self,name): ...

    用户1214487
  • Python中使用type、metacl

    我们知道动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

    py3study
  • python3 metaclass--创

    之前学python的时候就看见过metaclass的文章,没看懂,那篇博客后面说到,metaclass是python的黑魔法99% 不会用到。于是果断放弃。

    py3study
  • Python3 与 C# 扩展之~基础拓展

    看着小张准备回家换衣服了,小明有点失落,又有点孤单,于是说道:“逗逼张,你还要听吗?我准备讲类相关的知识了,这些可是我课后自学的哦~”

    逸鹏
  • python 通过元类控制类的创建

    在上面这张图中,A是我们平常在python中写的类,它可以创建一个对象a。其实A这个类也是一个对象,它是type类的对象,可以说type类是用来创建类对象的类,...

    py3study
  • 10-面向对象2

    isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上 。

    用户3106371

扫码关注云+社区

领取腾讯云代金券