python的元类以及应用

前言

记得在学习了一段时间python之后,开始涉及比较复杂的概念。 其中看到了元类这个东西,很抽象,看完很迷惑,于是开始一番搜索。 搜索完让我更加迷惑。
首先是元类的定义比较难以理解,其次是搜索元类的应用的时候,发现出来的资料,全都是与ORM相关的。
由此我有几个问题:
元类是什么?
元类有什么用?
ORM一定要用元类实现吗?

官网介绍元类应用

在python官方文档上介绍了元类很多的应用。但是搜索资料的时候,都是通过元类实现的ORM。

python的对象

要了解元类有什么作用,要先了解一下python的面向对象的概念。

所有都是对象

在python中,不管字符串、列表、字典都是对象,包括类本身也是对象。
使用type()函数可获得对象的类型:

print(type(1))
print(type('python'))

class A:
    pass

print(type(A))

打印结果:

<class 'int'>
<class 'str'>
<class 'type'>

题外话,如果print(type(type))呢?

由此可知,当你定义一个类的时候,类本身也是一个对象。所有通过class定义的类都是type的实例。

type的作用

type函数除了能够获得对象的类型,还能创建一个新的type实例,也就是和class的作用是一样的。

#通过class创建类
class Person:
    def __init__(self, age):
        self.age = age

# 通过type创建类
def init(self, age):
    self.age = age


Person2 = type('Person', (), {'__init__': init})

当type接收一个参数的时候就获取它的类型,当接收三个参数的时候,就返回一个type的实例,也就是一个类。
type(name, bases, dict, ***kwargs)
name就是class的名字,当我们用type()去检查Person的实例的时候,会获得class的name。

p = Person(21)
print(p)
<class '__main__.Person'>

bases是父类的集合,注意它是一个元组,可以多继承。 如果bases是空的,那么就会继承object
dict是一个字典,用来传递类的属性,上面的例子传递了一个init函数。
由此,我们可以通过type动态生成一个类,再由这个类去实例化一个对象。
类也是对象,我们不妨称为【类对象】区别普通【对象】

graph TD
type --实例化--> 类对象
类对象  --实例化--> 对象

元类

通过上述内容,我们可以得出2个重要结论:
类也是对象
类可以被动态创建
可以通过type进行创建

metaclass

除了使用type来创建类之外,python还提供了metaclass的来影响类的创建。
看一个简单的例子:

class MetaClass(type):
    def __new__(cls, *args, **kwargs):
        print('运行到元类的__new__')
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        print('运行到元类的__init__')

    def __call__(self, *args, **kwargs):
        print('运行到元类的__call__')
        return self


class A(metaclass=MetaClass):
    pass


print('开始实例化A')
a = A()

输出:

运行到元类的__new__
运行到元类的__init__
开始实例化A
运行到元类的__call__

在创建类的时候,通过传入metaclass关键字参数时,python会用metaclass的值去创建类。
上文说过,python中的类也是对象。 我们结合一下普通类和对象,以及对象的实例化过程:

class A:
    def __new__(cls, *args, **kwargs):
        print('运行到普通类的__new__')
        return super().__new__(cls)

    def __init__(self):
        print('运行到普通类的__init__')


a = A()

输出:

运行到普通类的__new__
运行到普通类的__init__

对比二者的创建过程是非常相似的。 也就是普通对象创建过程是:类的new——>类的init
类对象的创建过程:元类的new——>元类的init
普通类的实例化——>对象
元类的实例化——>类对象

  • 这边元类的call有一些特殊的地方,因为普通类的call是在实例被调用的时候触发,而元类的实例就是类对象,那么在类对象进行实例化的时候,就会触发。后面有机会深入探讨元类的执行过程的时候再补充。

由此,我们可以通过metaclass对类的创建进行拦截,完成我们的需求。接下来举几个例子来说明一下体验一下元类的实际应用。

元类的应用

单例

先看一下不用元类实现的单例:

class Singleton:
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls)
        return cls._instance


a = Singleton()
b = Singleton()

print(a == b)

输出:

True

用元类实现的单例:

class MetaSingleton(type):
    def __init__(self, *args, **kwargs):
        self._instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = super().__call__(*args, **kwargs)
        return self._instance


class Singleton(metaclass=MetaSingleton):
    pass


a = Singleton()
b = Singleton()

print(a == b)

输出:

True

注意看二者的差别,普通类通过new在实例化对象的时候,判断是否已经实例化过了。而元类的new实例化的是一个类对象,在创建出来的类对象进行实例化的时候才会调用元类的call,所以在call中进行判断类对象是否已经实例化了。

缓存实例

类似上面的单例,我们创建一个缓存实例的例子。
不使用元类实现:

class ObjectCache:
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_obj_cache'):
            cls._obj_cache = {}
        if args not in cls._obj_cache:
            cls._obj_cache[args] = super().__new__(cls)
        return cls._obj_cache[args]


a = ObjectCache('a')
b = ObjectCache('a')
c = ObjectCache('c')
print(a == b)
print(a == c)

输出:

True
False

使用元类实现:

class MetaObjectCache(type):
    def __init__(self, *args, **kwargs):
        self._object_cache = {}
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if args not in self._object_cache:
            self._object_cache[args] = super().__call__(*args, **kwargs)
        return self._object_cache[args]


class ObjectCache(metaclass=MetaObjectCache):
    def __init__(self, *args, **kwargs):
        pass


a = ObjectCache('a')
b = ObjectCache('a')
c = ObjectCache('c')

print(a == b)
print(a == c)

输出:

True
False

实现接口类

python虽然是鸭子类型,但是其中的abc模块也提供了抽象基类的功能。

import abc


class Base(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def test(self):
        pass


class A(Base):
    pass


a2 = A()

这段代码中,如果子类A没有实现test的方法,就会抛出错误:

TypeError: Can't instantiate abstract class A with abstract method test

可以通过元类实现类似的功能:

class Base(type):
    def __new__(cls, *args, **kwargs):
        name, base, attrs = args
        if not base:
            cls._methods = {}
            for k, v in attrs.items():
                if callable(v):
                    cls._methods[k] = v
        else:
            for k in cls._methods:
                if k not in attrs:
                    raise Exception(f'子类没有实现{k}方法')
        return super().__new__(cls, *args, **kwargs)


class A(metaclass=Base):
    def test(self):
        pass


class B(A):
    pass

输出:

Exception: 子类没有实现test方法

代码中的name, base, attrs = args就是上文提到tpye的参数,这里直接用*args传参。
在元类的new函数中,先判断类对象是否有父类,如果没有父类,就将方法储存起来。 如果有父类,就遍历_methods,判断每一个方法都存在attrs中,否则就是子类没有实现某个方法。
这里也体现了元类的一个特征,父类如果通过metaclass创建,那么子类也会受到影响。类B会首先检查此类是否有metaclass,如果没有就会去检查父类是否有metaclass.

拓展类功能

class MetaList(type):
    def __new__(cls, *args, **kwargs):
        _, _, attrs = args
        attrs['add'] = lambda self, val: self.append(val)
        return super().__new__(cls, *args, **kwargs)


class Mylist(list, metaclass=MetaList):
    pass


L = Mylist()
print(L)
L.add(3)
print(L)

输出:

[]
[3]

这样便利用了元类拓展了类的功能。

元类的实质

通过上面的分析和例子,可以得出结论,元类可以用来控制类、拦截类,包括创建,实例化。python通过元类提供了一种动态创建类的功能。
在python中确实是比较难以理解的概念,因此Tim Peters曾说:

元类就是深度的魔法,99%的⽤户应该根本不必为此操⼼。
如果你想搞清楚 究竟是否需要⽤到元类,那么你就不需要它。
那些实际⽤到元类的⼈都⾮常 清楚地知道他们需要做什么,⽽且根本不需要解释为什么要⽤元类。
—— TimPeters

特别是上面的几个例子,好像也没有非要通过元类来实现不可。并且,虽然元类能够拦截类的创建过程,但是“动态”的概念体现不够深刻。
后面的内容会解决开头的疑问:ORM和元类有什么关系?ORM一定要用元类实现吗?
通过ORM这个例子,也能更好体现,“动态”创建类。

为什么ORM要使用元类

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容