元类背景
我们先看 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 中我们一定要区分 object 与 type。两者不是一个东西却又有着千丝万缕的联系。
下面可以看一下 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
最后再贴一张图理解一下这两者的关系:
图片源自 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 中对方法设置的参数干预新对象的创建。