深度分析 Python 元类 abc

2,699 阅读6分钟

元类背景

我们先看 Python 下实现单例模式的一种写法:

class Singleton(type):

    def __init__(cls, *args, **kwargs):
        cls._instance = None
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance
        
class Test(metaclass=Singleton):

    def test(self):
        print("test")

test1 = Test()
test2 = Test()

print(id(test1), id(test2))
# 4307114640 4307114640

测试结果,很明显功能是实现了,两次实例化对应的对象为同一个:

>>> test1 = Test()
... test2 = Test()
>>> test1
<__main__.Test object at 0x10aa13d68>
>>> test2
<__main__.Test object at 0x10aa13d68>
>>> id(test1)
4473306472
>>> id(test2)
4473306472

没错,这种方式就是用元类(metaclass)控制类的实例化。

元类的秘密

通过元类实现单例模式我们已经对元类有个基本的认识,下面我们就看看元类到底是什么?

1. 初识元类

type 两种使用方式:

  • type(object) 获取对象类型。
  • type(name, bases, dict) 创建类对象。
>>> class A(object):
...     pass
... 
>>> A
<class '__main__.A'>
>>> A()
<__main__.A object at 0x108a4ac50>
>>> type("B", (object, ), dict())
<class '__main__.B'>
>>> type("B", (object, ), dict())()
<__main__.B object at 0x108a0eba8>

上面的例子可以发现 type 可以初始化类对象,功能与类定义的方式一致。所以 type(元类)是可以实现类/对象初始化。也就是说:“元类就是创建类的类”。

>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(bytes)
<class 'type'>

Python 的基础数据类型的类型都指向 type。在 Python中,内建的类type是元类。

Python中 定义新的元类是通过向类定义提供关键字参数 metaclass 就可以使用这个新的元类。就像下面一样定义与使用元类:

class NewType(type):
    pass
    
class A(metaclass=NewType):
    pass

所以在 Python 中我们一定要区分 objecttype。两者不是一个东西却又有着千丝万缕的联系。

下面可以看一下 wiki 中的一个很有意思的例子:

r = object
c = type
class M(c): pass

class A(metaclass=M): pass

class B(A): pass

b = B()

最重要的就是:

  • object 类是所有类的祖先。
  • type 元类是所有元类的祖先。

也就是说所有对象(包括 type )都是继承自 object 。所有对象(包括 object )的类型都源与 type (元类)。如下代码所示:

>>> type(object)
<class 'type'>
>>> isinstance(object, type)
True

最后再贴一张图理解一下这两者的关系:

image.png

图片源自 differences-between-python-types-and-objects,有兴趣的小伙伴可以查看原文对应的解释。这里就不过多的讨论。

2. 元类的作用

元类可以干预类的创建。 比如 Python 标准库库中就有一个元类 abc.ABCMeta,该元类的作用可以定义抽象类,类似 Java 中 abstract class。下面我们就看看它的使用:

class Base(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def read(self):
        pass

    @abc.abstractmethod
    def write(self):
        pass

class Http(Base, abc.ABC):
    pass

我们测试一下代码:

>>> Base()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: Can't instantiate abstract class Base with abstract methods read, write
>>> Http()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: Can't instantiate abstract class Http with abstract methods read, write

发现抽象类是不能实例化的。子类必须实现抽象方法才能对类进行实例化,我们修改子类代码:

>>> class Http(Base, abc.ABC):
... 
...     def read(self):
...         pass
... 
...     def write(self):
...         pass
... 
...     def open(self):
...         print(" open method ")
...         
>>> Http().open()
 open method 

可以看到 Http 继承 Base 并且实现抽象方法是没有任何问题的。

3. 自定义元类

如下在实现对类对象的缓存的功能。

创建一个类的对象时,如果之前使用同样参数创建过这个对象,那就返回它的缓存引用。

import weakref

class Cached(type):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__cache = weakref.WeakValueDictionary()

    def __call__(self, *args):
        if args in self.__cache:
            return self.__cache[args]
        else:
            obj = super().__call__(*args)
            self.__cache[args] = obj
            return obj

# Example
class Spam(metaclass=Cached):
    def __init__(self, name):
        print('Creating Spam({!r})'.format(name))
        self.name = name

验证一下功能:

>>> a = Spam('Guido')
Creating Spam('Guido')
>>> b = Spam('Diana')
Creating Spam('Diana')
>>> c = Spam('Guido') # Cached
>>> a is b
False
>>> a is c # Cached value returned
True

4. 注意事项

引用 Python 界的领袖 Tim Peters 一句话:

元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。 虽然元类的功能以及作用很强大,它可以改变类创建时的行为。但是不建议新手在代码中过多的使用元类,它会使你的代码变得复杂。更多的时候可以使用类装饰器,实现元类的功能。

上一步中实现缓存类对象的功能我们可以用简单的函数去实现,下面看一下普通方式是如何实现的:

import weakref
_spam_cache = weakref.WeakValueDictionary()

class Spam:
    def __init__(self, name):
        self.name = name

def get_spam(name):
    if name not in _spam_cache:
        s = Spam(name)
        _spam_cache[name] = s
    else:
        s = _spam_cache[name]
    return s

同样的我们验证结果:

>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> a is b
False
>>> c = get_spam('foo')
>>> a is c
True

其实很简单的一个函数就可以实现,只是元类的方式代码更优雅。

元类在 Python 中的实践,能够很好的反映动态语言的魅力,用元类的方式改变对象的一些属性,在程序执行阶段动态的改变对象。

abc.ABCMeta

其实关于 abc.ABCMeta 的实现很早之前就看过,但是一直不太明白。最近突然看到一篇文章,便豁然开朗了,下面就分析一下具体实现。

python abc 元类的定义

首先看一下在定义抽象类的时候用到的两个工具

  • @abc.abstractmethod 源码。
def abstractmethod(funcobj):
    """A decorator indicating abstract methods.

    Requires that the metaclass is ABCMeta or derived from it.  A
    class that has a metaclass derived from ABCMeta cannot be
    instantiated unless all of its abstract methods are overridden.
    The abstract methods can be called using any of the normal
    'super' call mechanisms.

    Usage:

        class C(metaclass=ABCMeta):
            @abstractmethod
            def my_abstract_method(self, ...):
                ...
    """
    funcobj.__isabstractmethod__ = True
    return funcobj

这个装饰器很简单就是给被装饰函数新增 __isabstractmethod__ 字段。

  • metaclass=abc.ABCMeta 源码,这里只截取用到的代码块。
class ABCMeta(type):

    _abc_invalidation_counter = 0

    def __new__(mcls, name, bases, namespace, **kwargs):
        cls = super().__new__(mcls, name, bases, namespace, **kwargs)
        # Compute set of abstract method names
        abstracts = {name
                     for name, value in namespace.items()
                     if getattr(value, "__isabstractmethod__", False)}
        for base in bases:
            for name in getattr(base, "__abstractmethods__", set()):
                value = getattr(cls, name, None)
                if getattr(value, "__isabstractmethod__", False):
                    abstracts.add(name)
        cls.__abstractmethods__ = frozenset(abstracts)
        # Set up inheritance registry
        cls._abc_registry = WeakSet()
        cls._abc_cache = WeakSet()
        cls._abc_negative_cache = WeakSet()
        cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
        return cls

ABCMeta 的初始化就是缓存抽象方法到cls.__abstractmethods__

cpython 对象的初始化

下面就是重点在 Python 对象初始化的时候会校验方法的参数,具体查看 cpython-typeobject.c ,下面截取 object_new 源码:

static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    if (excess_args(args, kwds)) {
        if (type->tp_new != object_new) {
            PyErr_SetString(PyExc_TypeError,
                            "object.__new__() takes exactly one argument (the type to instantiate)");
            return NULL;
        }
        if (type->tp_init == object_init) {
            PyErr_Format(PyExc_TypeError, "%.200s() takes no arguments",
                         type->tp_name);
            return NULL;
        }
    }

    if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
        PyObject *abstract_methods;
        PyObject *sorted_methods;
        PyObject *joined;
        PyObject *comma;
        _Py_static_string(comma_id, ", ");
        Py_ssize_t method_count;

        /* Compute ", ".join(sorted(type.__abstractmethods__))
           into joined. */
        abstract_methods = type_abstractmethods(type, NULL);
        if (abstract_methods == NULL)
            return NULL;
        sorted_methods = PySequence_List(abstract_methods);
        Py_DECREF(abstract_methods);
        if (sorted_methods == NULL)
            return NULL;
        if (PyList_Sort(sorted_methods)) {
            Py_DECREF(sorted_methods);
            return NULL;
        }
        comma = _PyUnicode_FromId(&comma_id);
        if (comma == NULL) {
            Py_DECREF(sorted_methods);
            return NULL;
        }
        joined = PyUnicode_Join(comma, sorted_methods);
        method_count = PyObject_Length(sorted_methods);
        Py_DECREF(sorted_methods);
        if (joined == NULL)
            return NULL;
        if (method_count == -1)
            return NULL;

        PyErr_Format(PyExc_TypeError,
                     "Can't instantiate abstract class %s "
                     "with abstract method%s %U",
                     type->tp_name,
                     method_count > 1 ? "s" : "",
                     joined);
        Py_DECREF(joined);
        return NULL;
    }
    return type->tp_alloc(type, 0);
}

这里看不懂 C 的代码没有关系,找到跟 Python 中对应的关键字 abstract_methods 以及判断语句type->tp_flags & Py_TPFLAGS_IS_ABSTRACT

盲猜就是对对象初始化的校验,再注意抛出的异常 PyErr_Format(PyExc_TypeError, "Can't instantiate abstract class %s " "with abstract method%s %U", type->tp_name, method_count > 1 ? "s" : "", joined);

虽说我也看不太懂,大致的逻辑应该就是 Cpython 会根据在 Python 中对方法设置的参数干预新对象的创建。

参考文档