深入理解Python的元类

类也是对象

在理解Python的元类之前,你需要掌握Python的类相关的知识。Python的类,有一个借鉴自Smalltalk语言的特殊的概念。

在大多数编程语言中,类就是一组描述如何生成对象的一段代码块。在Python中这一点也是成立的。

但是,在Python中类还远不止这些功能,类也是对象,没错,对象。

只要你使用关键字class修改,Python解释器会执行这代码代码,并创建一个对象,如:

这代码代码在执行的时候,会在内存中创建一个名为ObjectCreator的对象,这个对象(类)自身有创建对象(实例)的能力,这也是为什么它称之为类。 但是,同时,它是一个对象,因此,你可以对它做如下操作:

你可以把它赋值给一个变量

你可以拷贝它

你可以给它添加属性

你可以把它当成一个函数的入参

动态创建类

既然类也是对象,那么你可以在运行时创建它们,就像你创建其他对象一样。 首先,你可以创建在一个函数中使用class关键字创建一个类:

但是这并不够动态,以为你仍然需要你自己写下完整的类。 既然类是对象,那么它们肯定可以通过某种东西来生成。当你使用class关键字的时候,Python自动的创建这个类对象,但是和Python的大多数事情一样,它给了你手动创建的方法。 还记的type函数么?这个古老但强大的函数能够让你知道一个对象的类型是什么,就像这样:

同时,type有一个完全不一样的功能,它可以在运行时创建类对象,type可以接收类的参数的描述信息,然后返回一个类。(我知道,根据传入参数的不同,同一个函数拥有两种完全不同的用法是一件很傻的事情,但这在Python中是为了保持向后兼容性)。

type这样工作:

例如:

可以手动的这样创建:

你注意到我们使用'MyShinyClass'作为类的名称,同时作为变量保存这个类的引用。它们可以不一样,但是我们没有理由来将问题复杂化。

type接收一个字典来定义这个类的属性,所以:

也可以写成:

>>> Foo = type('Foo', (), {'bar':True})

然后可以当成正常的类来使用:

当然,你也可以继承它,如:

可以写成:

最后,如果你想为你的类添加方法,只需要定义一个有适当签名的函数,然后把它当成一个属性分配给它。

你甚至可以在你动态创建完类之后给它添加更多的方法,就像一个正常创建的类添加方法一样:

现在你应该明白:在Python中,类都是对象,你可以在运行时动态的创建一个类。 这就是当你使用关键字class的时候,Python所做的,而且是通过元类来做的。

什么是元类(metaclass)

元类就是创建类的'职工'。你定义类来达到创建对象的目的,对么?但是我们已经知道Python类都是对象。

好吧,元类就是用来创建这些类(对象)的,元类就是类的类,你可以这样想象它们:

你已经知道type可以帮你做这样的事情:

MyClass = type('MyClass', (), {})

那是因为,事实上type函数就是一个元类,type就是Python用来在幕后创建所有类的元类。

现在,你可能会好奇,为什么type是小写的,而不是Type?

我猜测是因为为了保持一致性,就像str这个类用来创建所有的字符串对象,int用来创建所有的整数对象,type仅仅是用来创建类对象的类。

你可以通过查看class属性来看看。

所有的事情,在python中都是对象,包括ints, strings, functions和classes.所有的它们都是对象,所有的它们都是通过一个类创建的。

那,class__的__class是什么呢?

所以,元类仅仅是用来创建类的东西,你可以把它称之为'class factory'。 type是Python使用的内置的metaclass, 但是很正常,你可以创建你自己的元类。

metaclass 属性

当你定义个类的时候,可以添加一个metaclass属性。

如果你这么做,Python会使用这个metaclass来创建类Foo。小心,这个地方非常tricky。 你首先定义了==class Foo(object)==,但是类对象Foo还没有在内存中创建。

Python会去查找类定义中的metaclass,如果它找到了,它就会创建这个对象类Foo。如果没有,它就会使用type来创建这个类。

多理解几遍。

当你创建一个类:

Python会做如下这些操作:

首先判定,在Foo中是否存在 __metaclass__属性? 如果存在,Python会在内存中通过__metaclass__创建一个名字为Foo的类对象。如果Foo中找不到__metaclass__,它会在MODULE级别查找__metaclass__,然后阐释做同样的事情。 如果它最终都没有找到任何的__metaclass__,它会使用它父类的metaclass来创建类对象。 需要注意的是,__metaclass__属性是不能够被继承的,父类的元类是可以被继承的。如果Bar使用一个__metaclass__属性通过type()来创建(不是通过type.new()),子类不会继承这个行为。

现在问题是,你可以放什么东西在metaclass中呢? 答案是:能够创建一个类的东西。 然而什么可以创建一个类呢?type,或者是其他子类,或者使用type。

自定义元类

元类的主要目的是当一个类创建的时候,能够自动的修改这个类。

当我们想创建一个匹配上线文信息的类的时候,我们平常都是通过API来处理。想想一个很傻的例子,当你决定你的模块中的所有的类的属性都应该是大写,有几种做法可以处理这样的事情,其中通过在模块级别设置__metaclass__可以达到目的。

这种方式,模块中所有的类会使用元类来创建,我们仅仅需要告诉元类把所有的属性修改成大写即可。

幸运的是,__metaclass__只要实际的能够调用就行,它不需要是一个正式的类,所以我们,通过使用function开始一个简单的例子。

现在,我们使用一个真实的类来实现元类,但是做的是同样的事情。

但是这不是真正的OOP。我们直接调用type,我们不去覆盖或者调用父类的__new__方法。

你可能已经注意到了有个额外的参数upperattr_metaclass,这并没有什么特别的。类方法的第一个参数总是表示当前的实例,就像在普通的类方法中的self参数一样。当然了,为了清晰起见,这里的名字我起的比较长。但是就像self一样,所有的参数都有它们的传统名称。因此,在真实的产品代码中一个元类应该是像这样的:

如果使用super方法的话,我们还可以使它变得更清晰一些,这会容易继承(是的,你可以拥有元类,从元类继承,从type继承)

就是这样,除此之外,关于元类真的没有别的可说的了。使用到元类的代码比较复杂,这背后的原因倒并不是因为元类本身,而是因为你通常会使用元类去做一些晦涩的事情,依赖于自省,控制继承等等。确实,用元类来搞些“黑暗魔法”是特别有用的,因而会搞出些复杂的东西来。但就元类本身而言,它们其实是很简单的:

拦截一个类的创建

修改类

返回被修改之后的类

为什么使用元类而不是使用函数来代替?

既然__metaclass__可以接受任何可以调用的对象,那为什么还要使用很明显更加复杂的类呢? 这么做有几个原因:

意图会更加明显。当你看到UpperAttrMetaclass(type)的时候,你知道接下来将会怎么样。

你可以使用OOP。元类可以继承自元类,覆盖父类的方法,元类甚至可以使用元类。

如果你设置了一个元类的类,类的子类将会是它元类的实例。

你能够更好的组织你的代码。当你使用元类的时候肯定不会是像我上面举的这种简单场景,通常都是针对比较复杂的问题。将多个方法归总到一个类中会很有帮助,也会使得代码更容易阅读。

你可以使用__new__, __init__以及__call__这样的特殊方法。它们能帮你处理不同的任务。就算通常你可以把所有的东西都在__new__里处理掉,有些人还是觉得用__init__更舒服些。

该死,这东西的名字是metaclass,肯定非善类,我要小心!

为什么你要使用元类?

那现在会我们的问题,为什么你会去使用这样一种容易出错且晦涩的特性?好吧,一般来说,你根本就用不上它:

“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” —— Python界的领袖 Tim Peters

元类的主要用途是创建API。一个典型的例子是Django ORM。它允许你像这样定义:

但是,如果你这样做:

它不会返回一个IntegerField对象,他会返回一个int,甚至可以直接从数据库中取出数据。这大概是因为model.Model定义了metaclass,同时使用了一些魔法能够将你刚刚定义的简单的Person类转变成对数据库的一个复杂hook。Django框架将这些看起来很复杂的东西通过暴露出一个简单的使用元类的API将其化简,通过这个API重新创建代码,在背后完成真正的工作。

结束语

首先,你知道了类其实是能够创建出类实例的对象。好吧,事实上,类本身也是实例,它们是元类的实例。

在Python中,一切皆对象,他们要么是类的是实例,要么是元类的实例。 除了type。 type实际上是它自己的实例,在纯Python环境中这可不是你能够做到的,这是通过在实现层面耍一些小手段做到的。其次,元类是很复杂的。对于非常简单的类,你可能不希望通过使用元类来对类做修改。你可以通过其他两种技术来修改类:

当你需要动态修改类时,99%的时间里你最好使用上面这两种技术。当然了,其实在99%的时间里你根本就不需要动态修改类。

猜你喜欢

转载自blog.csdn.net/sinat_38682860/article/details/88749101