详解python metaclass(元类)


Posted in Python onAugust 13, 2020

元编程,一个听起来特别酷的词,强大的Lisp在这方面是好手,对于Python,尽管没有完善的元编程范式,一些天才的开发者还是创作了很多元编程的魔法。Django的ORM就是元编程的一个很好的例子。

本篇的概念和例子皆在Python3.6环境下

一切都是对象

Python里一切都是对象(object),基本数据类型,如数字,字串,函数都是对象。对象可以由类(class)进行创建。既然一切都是对象,那么类是对象吗?

是的,类也是对象,那么又是谁创造了类呢?答案也很简单,也是类,一个能创作类的类,就像上帝一样,开启了万物之始。这样的类,称之为元类(classmeta)。

类的定义

对象是通过类创建的,这个很好理解。例如下面的代码:

class Bar(object):
  pass

bar = Bar()
print(bar, bar.__class__)  # <__main__.Bar object at 0x101eb4630> <class '__main__.Bar'>
print(Bar, Bar.__class__) # <class '__main__.Bar'> <class 'type'>

可以看见对象 bar 是类 Bar 创建的实例。然而 Bar,看起来却是由一个叫 type 的类创建的实例。即 bar <-- Bar < -- type

上面的例子,对象是动态创建的,类则是通过关键字 class 声明定义的。class关键字背后的玄机是什么呢?

实际上,class Bar(object) 这样的代码,等价于 Bar = type('Bar', (objects, ), {})
即类 type 通过实例化创建了它的对象 Bar,而这个 Bar 恰恰是一个类。这样能创建类的类,就是 Python 的元类。

从创建 Bar 的代码上来看,元类 type 的 __init__ 方法有3个参数,

  • 第一个是创建的类的名字
  • 第二个是其继承父类的元类列表,
  • 最后就是一个属性字典,即该类所具有的属性。

type 元类

type是小写,因而很容易误以为它是一个函数。通过help(type)可以看到它的定义如下:

class type(object):
  """
  type(object_or_name, bases, dict)
  type(object) -> the object's type
  type(name, bases, dict) -> a new type
  """
  def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
    """
    type(object_or_name, bases, dict)
    type(object) -> the object's type
    type(name, bases, dict) -> a new type
    # (copied from class doc)
    """
    pass

   @staticmethod # known case of __new__
  def __new__(*args, **kwargs): # real signature unknown
    """ Create and return a new object. See help(type) for accurate signature. """
    pass

如前所述,__init__方法接受三个参数,type 实例化的过程,会创建一个新的类。创建类的代码来自 __new__ 方法,它的参数其实和 __init__,一样。至于它们之间有什么关系,后面再做介绍。目前只要知道,当调用 type 进行实例化的时候,会先自动调用 __new__ 方法,然后再接着调用 __init__方法,在类外面来看,最终会实例化一个对象,这个对象是一个类。

从 type 的定义来看,它继承 object,Python3的所有类,都继承来着 object,类type 也是 object 的实例,令人奇怪的是,object 既是类也是对象,它也是由 type实例化而来。有一种鸡生蛋,蛋生鸡的悖论。暂且先不管,只要知道所有类的顶级继承来自 object 就好。

自定义元类

既然元类可以创建类,那么自定义元类就很简单了,直接继承类 type 即可。先看下面一个例子:

class MyType(type):
  pass


class Bar(object, metaclass=MyType):
  pass


print(MyType, MyType.__class__) # <class '__main__.MyType'> <class 'type'>
print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyType'>

可以看到,Bar在声明的时候,指定了其元类,此时的类 Bar 的__class__属性不再是 type,而是 MyType。即之前定义 Bar 的代码不再是 Bar = type('Bar', (objects, ), {}), 而是 Bar = MyType('Bar', (objects, ), {})。创建的元类的代码是MyType = type('MyType', (objects, ), {})

如果一个类没有显示的指定其元类,那么会沿着继承链寻找父类的元类,如果一直找不到,那么就使用默认的 type 元类。

元类冲突

每个类都可以指定元类,但是父类和子类的元类要是一条继承关系上的,否则会出现元类冲突。并且这个继承关系中,以继承最后面的元类为其元类。

元类的查找顺序大致为,先查看其继承的父类,找到父类的元类即停止。若直接父类没有元类,直到顶级父类 object ,此时父类(object)的元类是 type(basemetaclass),再看其自身有没有指定元类(submetaclass),如果指定了元类(submetaclass),再对比这个子元类(submetaclass)和父元类(basemetaclass),如果它们毫无继承关系,那么将会抛出元类冲突的错误。如果指定的子元类是父元类的父类,那么将会使用父元类,否则将使用期指定的子元类。

submetaclass <- basemetaclass使用 submetaclass 作为最终元类,
basemetaclass <- submetaclass, 使用 basemetaclass 作为最终元类,
两者无继承关系,抛出冲突。

有点像绕口令,且看代码例子

class MyType(type):
  pass

# 等价于 MyType = type('MyType', (object, ), {})

class Bar(object, metaclass=MyType):
  pass

# 等价于 Bar = MyType('Bar', (object, ), {})

class Foo(Bar):
  pass

# 等价于 Foo = MyType('Foo', (Foo, object, ), {})

print(Bar, Bar.__class__)  # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyType'>

Bar的父元类(basemetaclass)type,指定子元类(submetaclass)是 MyType, MyType 继承自 type,所以Bar的元类是 MyType。

又如:

class MyType(type):
  pass


class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=type):
  pass


print(Bar, Bar.__class__)  # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyType'>

尽管 Foo 也指定了元类(submetaclass) type,可是其父类的元类(basemetaclass)是 MyType, MyType
是 type的子类,因此 Foo的元类抛弃了指定的(submetaclass) type,而是沿用了其父类的MyType。

当 submetaclass 和 basemetaclass 没有继承关系的时候,将会元类冲突

class MyType(type):
  pass

class MyOtherType(type):
  pass

class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=MyOtherType):
  pass

运行代码,当定义的时候就会出现TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict)元类冲突的错误。

修改代码如下:

class MyType(type):
  pass

class MyOtherType(MyType):
  pass

class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=MyOtherType):
  pass


print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyOtherType'>

可以看到 Bar 和 Foo 分别有自己的元类,并且都符合继承关系中寻找。再调换一下元类看看:

class MyType(type):
  pass

class MyOtherType(MyType):
  pass

class Bar(object, metaclass=MyOtherType):
  pass


class Foo(Bar, metaclass=MyType):
  pass


print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyOtherType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyOtherType'>

都使用了Foo还是使用了元子类作为元类。究其原因,其实也很好理解。定义父类的时候,使用了元类MyOtherType 。定义子类的时候,通过继承,找到了创建父类的元类,那么父类就是 MyOtherType 的实例。

如果使用 MyType 做为元类,那么他就是 MyType 的实例,MyType的实例会比MyOtherType具有的属性少,那么在继承链上,它又是 Bar的子类,这样看就是子类比父类还狭窄了,显然不是一个好的关系。即变成了下面的关系

Bar     <-  MyOtherType
 
  |           ↑
  |           |
  ↓           |

Foo     <-  MyType

因此当 MyType 是 MyOtherType的父类的时候,即使 Foo 指定了 MyType作为元类,还是会被忽略,使用其父元类MyOtherType。

上面的线的箭头要一直,才能使用各自指定的元类,否则使用箭头指向的那个类作为元类。元类没有继承关系,元类冲突。

对象(类)实例化

目前为止,我们了解了类的定义,即类是如何被元类创建出来的,但是创建的细节尚未涉及。即元类是如何通过实例化创建类的过程。这也是对象创建的过程。

前文介绍了一个对象是通过类创建的,类对象是通过元类创建的。创建类中,会先调用元类的__new__方法,设置其名称,继承关系和属性,返回一个实例。然后再调用实例的__init__方法进行初始化实例对象。

class MyType(type):

  def __init__(self, *args, **kwargs):
    print('init ', id(self), args, kwargs)

  def __new__(cls, *args, **kwargs):
    print('new', id(cls), args, kwargs)
    instance = super(MyType, cls).__new__(cls, *args, **kwargs)
    print(id(instance))
    return instance


class Bar(object, metaclass=MyType):
  pass

运行代码可以看见输出:

new 4323381304 ('Bar', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Bar'}) {}
4323382232
init  4323382232 ('Bar', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Bar'}) {}

注意,上面代码仅关注 Bar 类的创建,即 Bar =MyType('Bar', (object, ), {})这个定义代码。MyType进行实例化创建 Bar的过程中,会先用 其 __new__ 方法,后者调用了父类 type的 __new__方法,并返回了元类的实例, 同时调用这个实例的__init__方法,后者对改实例对象进行初始化。这也就是为什么方法名为 __init__

通常我们会在 __init__方法初始化一些实例对象的属性如果 __new__ 方法什么也不返回,那么 __init__ 方法是不会被调用的。

instance = super(MyType, cls).__new__(cls, *args, **kwargs), 有的地方也喜欢写成 type.__new__或者 type,前者是python中如何调用父类方法的问题,后者是直接使用type创建类的过程。比较推荐的写法还是使用 super 调用其父类的方法的方式。

类是元类的对象,普通类创建对象的过程,也是一样。因此,只要重写 __new__方法,还可以实现一个类还可以创建另外一个类的实例的魔法。

移花接木

重写 __new__ 方法,让其创建另外一个类的实例。

class Bar:
  def __init__(self, name):
    self.name = name
    print('Bar init')

  def say(self):
    print('say: Bar {}'.format(self.name))


class Foo(object):

  def __init__(self):
    print('self {}'.format(self))

  def __new__(cls, *args, **kwargs):
    instance = super(Foo, cls).__new__(Bar, *args, **kwargs)
    print('instance {}'.format(instance))
    instance.__init__('a class')
    return instance

  def say(self):
    print('say: Foo')


m = Foo()
print('m {}'.format(m))
m.say()

输出

instance <__main__.Bar object at 0x104033240>
Bar init
m <__main__.Bar object at 0x104033240>
say: Bar a class

在类 Foo 中,通过重写 __new__返回了一个 Bar 类的实例对象,然后调用 Bar 实例的 __inti__ 方法初始化,由于返回了 bar 实例,因此 Foo 的实例没有被创建,因此也不会调用它的实例方法 __inti__ 。这样就把 移花(Bar)接木(Foo)上了。

也许有人会觉得这样的诡异魔法有什么用呢?实际上,Tornado框架使用了这样的技术实现了一个叫 Configurable 的工厂类,用于创建不同网络IO下的epoll还是select模型。有兴趣可以参考其实现方式。

元类的应用

讨论了那么多原理的东西,最后肯定是要应用到实际中才有意义。既然类可以被动态的创建,那么很多定义在类的方法,岂不是也可以被动态的创建了呢。这样就省去了很多重复工作,也能实现酷酷的元编程。

元类可以创建单例模式,也可以用来实现 ORM,下面介绍的是Django使用元类实现的查找方式。更经典的model定义网上有很多例子,就不再介绍了。下面介绍一个model通过manger管理器实现查询方法的例子

import inspect


class QuerySet:

  def get(self, *args, **kwargs):
    print('get method')
    return self

  def filter(self, *args, **kwargs):
    print('filter method')
    return self


class BaseManager:

  def __init__(self):
    pass

  @classmethod
  def from_queryset(cls, queryset_class, class_name=None):
    if class_name is None:
      class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
    class_dict = {
      '_queryset_class': queryset_class,
    }
    class_dict.update(cls._get_queryset_methods(queryset_class))
    return type(class_name, (cls,), class_dict)

  def get_queryset(self):
    return self._queryset_class()

  @classmethod
  def _get_queryset_methods(cls, queryset_class):
    def create_method(name, method):
      def manager_method(self, *args, **kwargs):
        return getattr(self.get_queryset(), name)(*args, **kwargs)

      manager_method.__name__ = method.__name__
      manager_method.__doc__ = method.__doc__
      return manager_method

    new_methods = {}
    for name, method in inspect.getmembers(queryset_class, predicate=inspect.isfunction):
      if hasattr(cls, name):
        continue
      queryset_only = getattr(method, 'queryset_only', None)
      if queryset_only or (queryset_only is None and name.startswith('_')):
        continue
      new_methods[name] = create_method(name, method)
    return new_methods


class Manager(BaseManager.from_queryset(QuerySet)):
  pass


class ModelMetaClass(type):

  def __new__(cls, *args, **kwargs):
    name, bases, attrs = args
    attrs['objects'] = Manager()
    return super(ModelMetaClass, cls).__new__(cls, name, bases, attrs)


class Model(object, metaclass=ModelMetaClass):
  pass


class User(Model):
  pass


User.objects.get()
User.objects.filter()
User.objects.filter().get()

这样model就用使用期管理器Manger 下的方法了。通过model的元类ModelMetaClass,定义model的时候,就初始化了一个 Manger对象挂载到Model下面,而定义Manger的时候,也通过元类将QuerySet下的查询方法挂载到Manger下了。

总结

Python里一切都是对象,对象都是由类进行创建实例化而来。既然一切是对象,那么类也是对象,而类这种对象又是由一种更高级类创建而来,即所谓的元类。

元类可以创建类,Python默认的元类是 type。通过继承type,可以自定义元类,在自定义元类的时候定义或者重载 __new__,可以创建该类的实例对象,同时也可以修改类创建对象的行为。类通过 __new__创建实例对象,然后调用实例对象的 __init__初始化实例对象。

在使用自定义元类的时候,子类的的元类和父类的元类有关系,前者指定的元类必须和父类的元类是一个继承关系上的,否则会出现元类冲突。子类选取元类的取决于指定的元类和父元类的继承关系,子元类若是父元类的子类,则指定的元类为子元类,否则将会被忽略,使用父元类为其元类。

元类是元编程的一种技术手段,常用于实现工厂模式的策略。通过定义元类动态创建类和展开,可以实现很多设计精妙的应用。ORM 正式其中一种常用的方法。

以上就是详解python metaclass(元类)的详细内容,更多关于python metaclass(元类)的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python BeautifulSoup中文乱码问题的2种解决方法
Apr 22 Python
web.py在SAE中的Session问题解决方法(使用mysql存储)
Jun 24 Python
使用Python的Scrapy框架十分钟爬取美女图
Dec 26 Python
python递归删除指定目录及其所有内容的方法
Jan 13 Python
分分钟入门python语言
Mar 20 Python
Python SQL查询并生成json文件操作示例
Aug 17 Python
使用urllib库的urlretrieve()方法下载网络文件到本地的方法
Dec 19 Python
Python使用统计函数绘制简单图形实例代码
May 15 Python
Python使用numpy模块实现矩阵和列表的连接操作方法
Jun 26 Python
Python os模块常用方法和属性总结
Feb 20 Python
django实现将修改好的新模型写入数据库
Mar 31 Python
python文件编写好后如何实践
Jul 07 Python
MAC平台基于Python Appium环境搭建过程图解
Aug 13 #Python
Expected conditions模块使用方法汇总代码解析
Aug 13 #Python
深入了解Python装饰器的高级用法
Aug 13 #Python
python高级特性简介
Aug 13 #Python
Pytest如何使用skip跳过执行测试
Aug 13 #Python
matplotlib基础绘图命令之bar的使用方法
Aug 13 #Python
Python logging模块原理解析及应用
Aug 13 #Python
You might like
php 编写安全的代码时容易犯的错误小结
2010/05/20 PHP
PHP导入Excel到MySQL的方法
2011/04/23 PHP
PHP之sprintf函数用法详解
2014/11/12 PHP
Yii把CGridView文本框换成下拉框的方法
2014/12/03 PHP
php常用数组array函数实例总结【赋值,拆分,合并,计算,添加,删除,查询,判断,排序】
2016/12/07 PHP
Laravel Eloquent ORM 实现查询表中指定的字段
2019/10/17 PHP
学习ExtJS Column布局
2009/10/08 Javascript
js apply/call/caller/callee/bind使用方法与区别分析
2009/10/28 Javascript
javascript实现div浮动在网页最顶上并带关闭按钮效果实例
2013/08/13 Javascript
JavaScript中property和attribute的区别详细介绍
2015/03/03 Javascript
node.js 使用ejs模板引擎时后缀换成.html
2015/04/22 Javascript
使用getBoundingClientRect方法实现简洁的sticky组件的方法
2016/03/22 Javascript
jquery拖动层效果插件用法实例分析(附demo源码)
2016/04/28 Javascript
jQuery绑定事件-多种实现方式总结
2016/05/09 Javascript
js querySelector() 使用方法
2016/12/21 Javascript
半个小时学json(json传递示例)
2016/12/25 Javascript
详解Angular 4.x Injector
2017/05/04 Javascript
单行 JS 实现移动端金钱格式的输入规则
2017/05/22 Javascript
BootStrap给table表格的每一行添加一个按钮事件
2017/09/07 Javascript
详解webpack模块化管理和打包工具
2018/04/21 Javascript
python strip()函数 介绍
2013/05/24 Python
深入理解Python中的内置常量
2017/05/20 Python
Python中super函数的用法
2017/11/17 Python
把JSON数据格式转换为Python的类对象方法详解(两种方法)
2019/06/04 Python
Django通过json格式收集主机信息
2020/05/29 Python
HTML5 Convas APIs方法详解
2015/04/24 HTML / CSS
你所知道的集合类都有哪些?主要方法?
2012/12/31 面试题
电话销售经理岗位职责
2013/12/07 职场文书
七年级政治教学反思
2014/02/03 职场文书
毕业生自荐材料范文
2014/12/30 职场文书
保留意见审计报告
2015/06/05 职场文书
2016读书月活动心得体会
2016/01/14 职场文书
教师外出学习心得体会
2016/01/18 职场文书
学法用法心得体会(2016推荐篇)
2016/01/21 职场文书
python自动统计zabbix系统监控覆盖率的示例代码
2021/04/03 Python
详解OpenCV获取高动态范围(HDR)成像
2022/04/29 Python