元类和元编程

别用!

Posted by UlyC on October 16, 2018

元类就是深度的魔法,99%的⽤户应该根本不必为此操⼼。

如果你想搞清楚 究竟是否需要⽤到元类,那么你就不需要它。

那些实际⽤到元类的⼈都⾮常 清楚地知道他们需要做什么,⽽且根本不需要解释为什么要⽤元类。

—— TimPeters

元类和元编程

一、元类

概念

元类(metaclass1,就是创建类的类。

这么说可能不太好理解,下面我们来解释下上面这句话:

在⼤多数编程语⾔中,类就是⼀组⽤来描述如何⽣成⼀个对象的代码段,在python中也不例外。

实例对象是由类生成的,而python中,类本身也是可以被传递和自省的对象。

类对象是用什么创建和生成的呢?答案是元类,元类就是一种知道如何创建和管理类的对象,也可以叫做类生成器。

__ class __ 和type

知道了元类的概念,那么我们怎么查看python中的元类具体是什么东西呢?

python中对象有个魔法属性__class__,可以查看对象是由哪个类创建出来的:

Python 3.7.0 (default, Jun 28 2018, 08:04:48) [MSC v.1912 64 bit (AMD64)]

>>> "abc".__class__
<class ''str''>  # 字符串“abc”是由“str”类实例化出来的
>>> num = 123
>>> num.__class__
<class ''int''>   # 没错,int、str等数据类型也是一个个的类,
# 具体的字符串和数字其实是他们实例化对象。
>>> class Demo(object):
...     pass
>>> demo = Demo()
>>> demo.__class__
<class '__main__.Demo'>
>>> Demo.__class__
<class 'type'>

可以看到类对象的类,就是type

这个type其实就是平时我们用来查看数据类型的内建函数:

>>> type(num)
<class ''int''>  
>>> type(Demo)
<class 'type'>

在只使用这个功能时,作用与__class__相同。

而其实type就是一个元类,它还有⼀种完全不同的功能:动态的创建类。2

语法为:type(类名, 由⽗类名称组成的元组(针对继承的情况,可以为空), 包含属性的字典)

例如:

>>> type("Demo2",(object),{})
<class ''__main__.Demo2''>  
>>> demo2 = Demo2()
>>> demo2
<__main__.Demo2 object at 0x000001F71DBE5EF0>      

由type动态创建的 Demo2类对象,和由“class ...”创建的Demo,两者完全等价。

python 3以后3,默认的元类皆为type,即所有的类对象默认都是由type创建的,接下来让我们来自定义一个元类。

创建一个元类

最简单的,自定义一个继承自type的子类,想要使用它创建类对象时在类中使用__metaclass__声明一下:

>>> class Meta(type):
...     pass
...
>>> class Base(object):
...      __metaclass__ = Meta       # python2
...
>>> class Base(metaclass=Meta):     # python3
...     pass

一般来说,定义的元类应该重新实现__init__()__new__()方法。

  • 如果需要修改类的属性,使用元类的__new__方法
  • 如果只是做一些类属性检查的工作,使用元类的__init__方法。
>>> class Meta(type):
...     def __new__(cls, name, bases, dct):   # 注意第一个参数是cls而不是self
...         print('create class %s' % name)
...         return type.__new__(cls, name, bases, dct)
...
...     def __init__(cls, name, bases, dct):
...         print('Init class %s' % name)
...         type.__init__(cls, name, bases, dct)   # 注意__init__方法禁止返回值
...
>>> class Base(object, metaclass=Meta):
...     pass
...
create class Base
Init class Base
>>> Base
<class '__main__.Base'>

metaclass拓展

事实上,__metaclass__实际上可以被任意调⽤,它只是规定了类“按照什么样的规则去生成”,并不需要是⼀个正式 的类。

比如,我们有一个比较二的需求:你决定在你的模块⾥,所有的类的属性都应该是⼤写形式。

>>> def upper_attr(future_class_name, future_class_parents, future_class_attr):
...		"""遍历属性字典,把不是__开头的属性名字变为⼤写"""
...     newAttr = {}
...     for name,value in future_class_attr.items():
...             if not name.startswith("__"):
...                     newAttr[name.upper()] = value
...     return type(future_class_name, future_class_parents, newAttr)

>>> class Foo(object, metaclass=upper_attr):
...     bar= 'bip'
...
>>> foo = Foo()
>>> foo.bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'bar'
>>> foo.BAR
'bip'

元类冲突

假如有两个不同的元类,要生成一个继承这两个类的子类,会产生什么情况呢?

>>> class MetaA(type):
...     pass
...
>>> class MetaB(type):
...     pass
...
>>> class A(object, metaclass=MetaA):
...     pass
...
>>> class B(object, metaclass=MetaB):
...     pass
...
>>> class C(A, B):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

这时会报错, 元类冲突。

我们需要手动构造新的子类元类,让新的子类元类继承自A和B的元类:

>>> class MetaC(MetaA, MetaB):
...     pass
...
>>> class C(object, metaclass=MetaC):
...     pass
...

这样就不会报错了。

二、元类的应用

应用元类之前我们首先要知道使用元类编程的缺点:

  • 实现麻烦
  • 代码可读性不高
  • 不易维护

其实在开头引用TimPeters的话就说明,不要随意在生产代码中使用元类,而且现有的编码规范也极不推荐使用。

好吧,了解元类的使用对你更深入理解一些框架是有帮助的,如果你执意要学习这门禁术,那就往下看吧!

===========================禁术分割线 =======================

就元类本身而言,它的作用是:

  • 拦截类的创建
  • 修改类
  • 返回修改之后的类

使用元类还是有一些好处的:

  • 意图更加明确。当然你的metaclass名字要起好
  • 面向对象。可以隐式继承到子类
  • 可以更好地组织代码
  • 可以用__new____init__,__call__等方法更好地控制

这里先简单说几个简单的应用实例:

实现单例模式

class Singleton(type):
    instance = None
    def __call__(cls, *args, **kw):
		"""通过重写__call__拦截实例的创建,(实例通过调用括号运算符创建的)"""
        if not cls.instance:
            cls.instance = super().__call__(*args, **kw)
        return cls.instance

面向切面(AOP)编程

在运行时,动态地将代码切入到类的指定方法、指定位置上的编程称为面向切面的编程(AOP)。

简单地说,如果不同的类要实现相同的功能,可以将其中相同的代码提取到一个切片中,等到需要时再切入到对象中去。这些相同的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。

【注意】:python的装饰器天然适合AOP,简单易读又好用。使用元类AOP纯属自找麻烦,这里主要是说下原理,并不是让大家去用。(在生产代码中炫技很讨厌的说(′゜c_,゜` ) )

面向切面比较常用的是为类方法添加记录日志功能,下面使用元类来实现此功能:

def trace(func):
    def callfunc(self, *args, **kwargs):
        with open'debug_log.txt', 'a'as f:           
            f.write('Calling %s: %s ,%s\n'%(func.__name__, args, kwargs))
            result = func(self, *args, **kwargs)
            f.write('%s returned %s\n'%(func.__name__, result))
        return result
    return callfunc
 
 
class LogMeta(type):
    def __new__(cls, name, bases, attr_dct):
        for k, v in attr_dct.items():
            if isinstance(v, types.FunctionType):
                # 如果v是函数类型, 使用trace处理,添加日志
                attr_dict[k] = trace(v)
        return type.__new__(cls, name, bases, attr_dct)
 

class Foo(object metaclass=LogMeta):
    num = 0
 
    def spam(self):
        Foo.num += 1
        return Foo.num

对抽象基类的支持

简单的说,抽象基类就是包含一个或者多个抽象方法的类。

它本身不实现抽象方法,强制子类去实现,同时抽象基类自己不能被实例化,没有实现抽象方法的子类也无法实例化。

python内置的abc(abstract base class)来实现抽象基类。

from abc import ABCMeta, abstractmethod

class Base(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

使用这种方式如果没有在子类里实现foo方法你是没有办法实例化抽象基类Base的子类的 。

另外应该优先使用collections定义的抽象基类,比如要实现一个容器我们可以继承 collections.Container

实现ORM

这个比较复杂,在下一章再详细说。

三、元编程

概念

元编程(英语:Metaprogramming),又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。

编写元程序的语言称之为元语言。被操纵的程序的语言称之为“目标语言”。一门编程语言同时也是自身的元语言的能力称之为“反射”或者“自反”。

以上引自维基百科,元编程

许多人将「元类编程」和「元编程」混为一谈,甚至说“元编程 == 元类编程”,这是非常不对的。

就比如python,使用Cpython解释器时,C语言就是元语言,python就是目标语言,C语言对python进行了元编程。

元编程在不同的语言中有不同的实现,在Python中,元编程实现通常有这几个手段:

  • 魔法方法
  • 描述器(descriptor)
  • 元类
  • eval函数

应用

元编程常见的应用场景很多,扩展(重构)语法、开发DSL、生成代码、根据特定场景自动选择代码优化、解决一些正交的架构设计问题、AOP等等。

所以元编程存在的目的,就是多提供了一个抽象层次。

至于元编程有什么缺点,争议还是比较大的。比如以重构语法的应用为例,很多元编程的反对者就认为这样会导致代码的可读性、可维护性降低,分化社区,影响交流,因为每个开发人员都能搞一个自己的方言 。

对于作为”胶水语言”的python,对各种其他语言库的支持(ctypes、js2py等),更是元编程应用很好的实例。

很经典的一个应用就是使用元类在python语言和数据库中间增加一个抽象层:ORM层。

ORM的简单实现

ORM

ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。

以下代码参考自文章使用元类-—廖雪峰的官方网站

编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码:

class User(Model):
    # 定义类的属性到列的映射:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()

其中,父类Model和属性类型StringFieldIntegerField是由ORM框架提供的,剩下的魔术方法比如save()全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。

class Field(object):
    """负责保存数据库表的字段名和字段类型"""
    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type

    def __str__(self):
        return '<%s:%s>' % (self.__class__.__name__, self.name)
    
    
class StringField(Field):
    def __init__(self, name):
        super().__init__(name, 'varchar(100)')   #python2中super里要带参数
        
        
class IntegerField(Field):

    def __init__(self, name):
        super().__init__(name, 'bigint')
        

class ModelMetaclass(type):
    """自定义元类"""
    def __new__(cls, name, bases, attrs):
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        print('Found model: %s' % name)
        
        # 排除掉对Model类的修改
        mappings = {}
        for k, v in attrs.items():
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
        
        # 从类属性中删除该Field属性,否则,实例的属性会遮盖类的同名属性,运行错误    
        for k in mappings.keys():
            attrs.pop(k)  
        
        attrs['__mappings__'] = mappings   # 保存属性和列的映射关系
        attrs['__table__'] = name     # 假设表名和类名一致(简化)
        return type.__new__(cls, name, bases, attrs)
 

class Model(dict, metaclass=ModelMetaclass):
    """只简单实现了INSERT功能"""
    def __init__(self, **kw):
        super(Model, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []  
        
        for k, v in self.__mappings__.items():
            fields.append(v.name)
            params.append('?')
            args.append(getattr(self, k, None))
        
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))

这样,就实现了一个简单的ORM框架。

后记

本来想在这里一起把抽象基类(abc)和面向切面编程(AOP)的几种实现一起谈谈,结果篇幅又是远超我的想象,还是以后有机会慢慢写吧….

参考链接

[1].Python 中的元类编程

[2]. 使用元类-—廖雪峰的官方网站

[3].怎么理解元编程? - 知乎用户的回答 - 知乎

[4].元类与面向切面编程——再见紫罗兰的博客

[5].『简单的』Python 元类 - Pegasus Wang的文章 - 知乎

[6]. What are metaclasses in Python?——StackOverflow

[7]. Python元编程-被遗忘的远古凶兽

  1. Meta- 这个前缀在希腊语中的本意是「在…后,越过…的」,类似于拉丁语的 post-,比如 metaphysics 就是「在物理学之后」(形而上学),这个词最开始指一些亚里士多德的著作,因为它们通常排序在《物理学》之后。 但西方哲学界在几千年中渐渐赋予该词缀一种全新的意义:关于某事自身的某事。比如 meta-knowledge 就是「关于知识本身的知识」,meta-data 就是「关于数据的数据」,meta-language 就是「关于语言的语言」,类似的,metaclass就是「关于类的类」。 

  2. 在python 2中,默认的元类是types.ClassType,就是所谓的旧样式类。python2.2以后已不提倡使用。 

  3. 既然type也是一种“类”,为什么不写成“Type”呢? 想想“str”、“int”等“类”,这里应该是为了和他们保持一致。 


知识共享许可协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。