详解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编写android截屏脚本双击运行即可
Jul 21 Python
python学习 流程控制语句详解
Jun 01 Python
Python设计足球联赛赛程表程序的思路与简单实现示例
Jun 28 Python
对numpy 数组和矩阵的乘法的进一步理解
Apr 04 Python
详解Python 爬取13个旅游城市,告诉你五一大家最爱去哪玩?
May 07 Python
使用python获取(宜宾市地震信息)地震信息
Jun 20 Python
python从list列表中选出一个数和其对应的坐标方法
Jul 20 Python
python多线程与多进程及其区别详解
Aug 08 Python
python脚本之一键移动自定格式文件方法实例
Sep 02 Python
tensorflow 环境变量设置方式
Feb 06 Python
PyCharm2020.1.2社区版安装,配置及使用教程详解(Windows)
Aug 07 Python
使用pd.merge表连接出现多余行的问题解决
Jun 16 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
基于MySQL到MongoDB简易对照表的详解
2013/06/03 PHP
PHP用星号隐藏部份用户名、身份证、IP、手机号等实例
2014/04/08 PHP
php操作XML、读取数据和写入数据的实现代码
2014/08/15 PHP
php简单实现屏蔽指定ip段用户的访问
2015/04/29 PHP
Display SQL Server Version Information
2007/06/21 Javascript
jQuery表格行换色的三种实现方法
2011/06/27 Javascript
理解JavaScript中worker事件api
2015/12/25 Javascript
vue 使用Jade模板写html,stylus写css的方法
2018/02/23 Javascript
React Native开发封装Toast与加载Loading组件示例
2018/09/08 Javascript
使用 webpack 插件自动生成 vue 路由文件的方法
2019/08/20 Javascript
vue学习之Vue-Router用法实例分析
2020/01/06 Javascript
[48:56]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 VG vs KG
2018/03/31 DOTA
python之yield表达式学习
2014/09/02 Python
Python访问MySQL封装的常用类实例
2014/11/11 Python
在Django的视图(View)外使用Session的方法
2015/07/23 Python
编写Python小程序来统计测试脚本的关键字
2016/03/12 Python
python实现kMeans算法
2017/12/21 Python
Tornado 多进程实现分析详解
2018/01/12 Python
Python安装pycurl失败的解决方法
2018/10/15 Python
OpenCV 边缘检测
2019/07/10 Python
Python socket模块ftp传输文件过程解析
2019/11/05 Python
Pycharm中配置远程Docker运行环境的教程图解
2020/06/11 Python
Django框架实现在线考试系统的示例代码
2020/11/30 Python
7款设计巧妙的css3飘带状3D立体效果的导航菜单和表单窗口
2013/02/04 HTML / CSS
Book Depository亚太地区:一家领先的国际图书零售商
2019/05/05 全球购物
青年文明号复核材料
2014/02/11 职场文书
渔夫的故事教学反思
2014/02/14 职场文书
2014年征兵标语
2014/06/20 职场文书
国际金融专业自荐信
2014/07/05 职场文书
司法局群众路线教育实践活动开展情况总结
2014/10/25 职场文书
新婚姻法离婚协议书范文
2014/11/30 职场文书
南京导游词
2015/02/03 职场文书
财务部岗位职责范本
2015/04/14 职场文书
病假证明模板
2015/06/19 职场文书
2015暑期爱心支教策划书
2015/07/14 职场文书
python playwright 自动等待和断言详解
2021/11/27 Python