Skip to content

Latest commit

 

History

History
905 lines (673 loc) · 33.2 KB

File metadata and controls

905 lines (673 loc) · 33.2 KB

八、元类——使类(而非实例)更智能

前面的章节已经向我们展示了如何使用 decorator 修改类和函数。但这不是修改或扩展类的唯一选项。在创建类之前修改类的一种更高级的技术是使用元类。这个名字已经暗示了它可能是什么;元类是包含关于类的元信息的类。

元类的基本前提是在定义时为您生成另一个类的类,因此通常您不会使用它来更改类实例,而只更改类定义。通过更改类定义,可以自动向类添加一些属性、验证是否设置了某些属性、更改继承、在管理器中自动注册类以及执行许多其他操作。

虽然元类通常被认为是一种比(类)装饰器更强大的技术,但实际上它们在可能性上并没有太大差异。选择通常归结为便利或个人偏好。

本章涵盖以下主题:

  • 基本动态类创建
  • 带参数的元类
  • 类创建的内部结构、操作顺序
  • 抽象基类、示例和内部工作
  • 使用元类的自动插件系统
  • 存储类属性的定义顺序

动态创建类

元类是在 Python 中创建新类的工厂。事实上,尽管您可能不知道,但每当您创建一个类时,Python 总是会执行type元类。

以过程方式创建类时,type元类用作函数。此函数接受三个参数:namebasesdict。名称将成为__name__属性,bases是继承的基类列表,将存储在__bases__中,dict是包含所有变量的名称空间字典,将存储在__dict__中。

需要注意的是,type()函数还有另一个用途。根据前面记录的参数,它将根据这些规范创建一个类。给定一个带有类实例的参数,它也将从实例返回类。你的下一个问题可能是,“如果我对类定义而不是类实例调用type()会发生什么?”那么,这将返回类的元类,默认情况下是type

让我们用几个例子来说明这一点:

>>> class Spam(object):
>>>     eggs = 'my eggs'

>>> Spam = type('Spam', (object,), dict(eggs='my eggs'))

Spam的前两种定义完全相同;它们都创建了一个以实例化属性eggsobject为基的类。让我们测试一下这是否真的像您预期的那样有效:

>>> class Spam(object):
...     eggs = 'my eggs'

>>> spam = Spam()
>>> spam.eggs
'my eggs'
>>> type(spam)
<class '…Spam'>
>>> type(Spam)
<class 'type'>

>>> Spam = type('Spam', (object,), dict(eggs='my eggs'))

>>> spam = Spam()
>>> spam.eggs
'my eggs'
>>> type(spam)
<class '...Spam'>
>>> type(Spam)
<class 'type'>

正如预期的那样,这两种方法的结果是相同的。在创建类时,Python 会无声地添加type元类,custom元类只是继承type的类。一个简单的类定义有一个静默的元类,它生成一个简单的定义,例如:

class Spam(object):
 pass

基本相同于:

class Spam(object, metaclass=type):
 pass

这就提出了一个问题:如果每个类都是由一个(静默的)元类创建的,那么type的元类是什么?这实际上是一个递归定义;type的元类是type。这就是自定义元类的本质:一个继承类型以允许修改类而不需要修改类定义本身的类。

一个基本元类

因为元类可以修改任何类属性,所以您完全可以做任何您想做的事情。在继续使用更高级的元类之前,让我们看一个基本示例:

# The metaclass definition, note the inheritance of type instead
# of object
>>> class MetaSpam(type):
...
...     # Notice how the __new__ method has the same arguments
...     # as the type function we used earlier?
...     def __new__(metaclass, name, bases, namespace):
...         name = 'SpamCreatedByMeta'
...         bases = (int,) + bases
...         namespace['eggs'] = 1
...         return type.__new__(metaclass, name, bases, namespace)

# First, the regular Spam:
>>> class Spam(object):
...     pass

>>> Spam.__name__
'Spam'
>>> issubclass(Spam, int)
False
>>> Spam.eggs
Traceback (most recent call last):
 ...
AttributeError: type object 'Spam' has no attribute 'eggs'

# Now the meta-Spam
>>> class Spam(object, metaclass=MetaSpam):
...     pass

>>> Spam.__name__
'SpamCreatedByMeta'
>>> issubclass(Spam, int)
True
>>> Spam.eggs
1

正如您所看到的,关于类定义的所有内容都可以使用元类轻松地修改。这使得它既是一个非常强大的工具,也是一个非常危险的工具,因为您很容易导致非常意外的行为。

元类的参数

向元类添加参数的可能性是一个鲜为人知的特性,但仍然非常有用。在许多情况下,只需向类定义中添加属性或方法就足以检测要做什么,但在某些情况下,更具体一些是有用的。

>>> class MetaWithArguments(type):
...     def __init__(metaclass, name, bases, namespace, **kwargs):
...         # The kwargs should not be passed on to the
...         # type.__init__
...         type.__init__(metaclass, name, bases, namespace)
...
...     def __new__(metaclass, name, bases, namespace, **kwargs):
...         for k, v in kwargs.items():
...             namespace.setdefault(k, v)
...
...         return type.__new__(metaclass, name, bases, namespace)

>>> class WithArgument(metaclass=MetaWithArguments, spam='eggs'):
...     pass

>>> with_argument = WithArgument()
>>> with_argument.spam
'eggs'

这个简单的示例可能没有用处,但可能性很大。您唯一需要记住的是__new____init__方法都需要扩展才能工作。

通过类访问元类属性

当使用元类时,可能会让感到困惑。请注意,类实际上不仅仅是构造类,它实际上在创建过程中继承了类。举例说明:

>>> class Meta(type):
...
...     @property
...     def spam(cls):
...         return 'Spam property of %r' % cls
...
...     def eggs(self):
...         return 'Eggs method of %r' % self

>>> class SomeClass(metaclass=Meta):
...     pass

>>> SomeClass.spam
"Spam property of <class '...SomeClass'>"
>>> SomeClass().spam
Traceback (most recent call last):
 ...
AttributeError: 'SomeClass' object has no attribute 'spam'

>>> SomeClass.eggs()
"Eggs method of <class '...SomeClass'>"
>>> SomeClass().eggs()
Traceback (most recent call last):
 ...
AttributeError: 'SomeClass' object has no attribute 'eggs'

如前例所示,这些方法仅适用于class对象,而不适用于实例。spam属性和方法不能通过实例访问,但可以通过类访问。我个人没有看到任何关于这种行为的有用案例,但它绝对值得注意。

使用 collections.abc 的抽象类

抽象的基类模块是 Python 中最有用的和最常用的元类示例之一,因为它可以轻松确保一个类遵守某个接口,而无需进行大量手动检查。在前面的章节中,我们已经看到了一些抽象基类的例子,但现在我们将看看这些抽象基类的内部工作原理以及更高级的特性,例如自定义 ABC。

抽象类的内部工作

首先,让我们演示常规抽象基类的用法:

>>> import abc

>>> class Spam(metaclass=abc.ABCMeta):
...
...     @abc.abstractmethod
...     def some_method(self):
...         raise NotImplemented()

>>> class Eggs(Spam):
...     def some_new_method(self):
...         pass

>>> eggs = Eggs()
Traceback (most recent call last):
 ...
TypeError: Can't instantiate abstract class Eggs with abstract
methods some_method

>>> class Bacon(Spam):
...     def some_method():
...         pass

>>> bacon = Bacon()

如您所见,抽象基类阻止我们实例化这些类,直到继承了所有抽象方法。除常规方法外,还支持propertystaticmethodclassmethod

>>> import abc

>>> class Spam(object, metaclass=abc.ABCMeta):
...     @property
...     @abc.abstractmethod
...     def some_property(self):
...         raise NotImplemented()
...
...     @classmethod
...     @abc.abstractmethod
...     def some_classmethod(cls):
...         raise NotImplemented()
...
...     @staticmethod
...     @abc.abstractmethod
...     def some_staticmethod():
...         raise NotImplemented()
...
...     @abc.abstractmethod
...     def some_method():
...         raise NotImplemented()

那么 Python 在内部做什么呢?当然,你可以阅读abc.py源代码,但我认为一个简单的解释会更好。

首先,abc.abstractmethod将函数的__isabstractmethod__属性设置为True。因此,如果您不想使用 decorator,您可以简单地通过执行以下操作来模拟该行为:

some_method.__isabstractmethod__ = True

之后,abc.ABCMeta元类遍历名称空间中的所有项,并查找__isabstractmethod__属性计算结果为True的对象。除此之外,它还会遍历所有基类并检查每个基类的__abstractmethods__集,以防该类继承abstract类。__isabstractmethod__仍然计算为True的所有项目都将添加到__abstractmethods__集合中,该集合作为frozenset存储在类中。

请注意,我们不使用abc.abstractpropertyabc.abstractclassmethodabc.abstractstaticmethod。由于 Python3.3 已经被弃用为classmethodstaticmethodproperty修饰符,因此abc.abstractmethod可以识别简单的property修饰符和abc.abstractmethod修饰符。订购装饰师时要小心;abc.abstractmethod必须是最里面的装饰师才能正常工作。

下一个问题是关于实际支票的来源;检查类是否完全实现。这实际上是通过一些 Python 内部构件实现的:

>>> class AbstractMeta(type):
...     def __new__(metaclass, name, bases, namespace):
...         cls = super().__new__(metaclass, name, bases, namespace)
...         cls.__abstractmethods__ = frozenset(('something',))
...         return cls

>>> class Spam(metaclass=AbstractMeta):
...     pass

>>> eggs = Spam()
Traceback (most recent call last):
 ...
TypeError: Can't instantiate abstract class Spam with ...

我们可以很容易地模仿metaclass自己的相同行为,但应该注意的是abc.ABCMeta实际上做得更多,我们将在下一节中演示。要模拟内置抽象基类支持的行为,请查看以下示例:

>>> import functools

>>> class AbstractMeta(type):
...     def __new__(metaclass, name, bases, namespace):
...         # Create the class instance
...         cls = super().__new__(metaclass, name, bases, namespace)
...
...         # Collect all local methods marked as abstract
...         abstracts = set()
...         for k, v in namespace.items():
...             if getattr(v, '__abstract__', False):
...                 abstracts.add(k)
...
...         # Look for abstract methods in the base classes and add
...         # them to the list of abstracts
...         for base in bases:
...             for k in getattr(base, '__abstracts__', ()):
...                 v = getattr(cls, k, None)
...                 if getattr(v, '__abstract__', False):
...                     abstracts.add(k)
...
...         # store the abstracts in a frozenset so they cannot be
...         # modified
...         cls.__abstracts__ = frozenset(abstracts)
...
...         # Decorate the __new__ function to check if all abstract
...         # functions were implemented
...         original_new = cls.__new__
...         @functools.wraps(original_new)
...         def new(self, *args, **kwargs):
...             for k in self.__abstracts__:
...                 v = getattr(self, k)
...                 if getattr(v, '__abstract__', False):
...                     raise RuntimeError(
...                         '%r is not implemented' % k)
...
...             return original_new(self, *args, **kwargs)
...
...         cls.__new__ = new
...         return cls

>>> def abstractmethod(function):
...     function.__abstract__ = True
...     return function

>>> class Spam(metaclass=AbstractMeta):
...     @abstractmethod
...     def some_method(self):
...         pass

# Instantiating the function, we can see that it functions as the
# regular ABCMeta does
>>> eggs = Spam()
Traceback (most recent call last):
 ...
RuntimeError: 'some_method' is not implemented

实际的实现有点复杂,因为它仍然需要处理旧式类和propertyclassmethodstaticmethod类型的方法。此外,它还具有缓存功能,但这段代码涵盖了实现中最有用的部分。这里需要注意的最重要的技巧之一是,实际检查是通过修饰实际类的__new__函数来执行的。这个方法在一个类中只执行一次,因此我们可以避免对多个实例化执行这些检查的开销。

通过在以下文件中查找 Python 源代码中的__isabstractmethod__Objects/descrobject.cObjects/funcobject.cObjects/object.c可以找到抽象方法的实际实现。实现的 Python 部分可以在Lib/abc.py中找到。

自定义类型检查

当然,使用抽象基类定义自己的接口非常好。但是,告诉 Python 您的类实际上类似于什么以及哪些类型类似也非常方便。为此,abc.ABCMeta提供了一个寄存器函数,允许您指定哪些类型相似。例如,将列表类型视为类似的自定义列表:

>>> import abc

>>> class CustomList(abc.ABC):
...     'This class implements a list-like interface'
...     pass

>>> CustomList.register(list)
<class 'list'>

>>> issubclass(list, CustomList)
True
>>> isinstance([], CustomList)
True
>>> issubclass(CustomList, list)
False
>>> isinstance(CustomList(), list)
False

如最后四行所示,这是一种单向关系。另一种方法通常很容易通过继承列表来实现,但在这种情况下不起作用。abc.ABCMeta拒绝创建继承周期。

>>> import abc

>>> class CustomList(abc.ABC, list):
...     'This class implements a list-like interface'
...     pass

>>> CustomList.register(list)
Traceback (most recent call last):
 ...
RuntimeError: Refusing to create an inheritance cycle

为了能够处理这样的情况,abc.ABCMeta中还有一个有用的功能。当子类化abc.ABCMeta时,可以扩展__subclasshook__方法来定制issubclass的行为,并以此定制isinstance

>>> import abc

>>> class UniversalClass(abc.ABC):
...    @classmethod
...    def __subclasshook__(cls, subclass):
...        return True

>>> issubclass(list, UniversalClass)
True
>>> issubclass(bool, UniversalClass)
True
>>> isinstance(True, UniversalClass)
True
>>> issubclass(UniversalClass, bool)
False

__subclasshook__应该返回TrueFalseNotImplemented,这将导致issubclass返回TrueFalseNotImplemented被提升时的通常行为。

在 Python 3.4 之前使用 abc.abc

我们在这一段中使用的abc.ABC类在 Python 版本 3.4 及更高版本中仅可用,但在较旧版本中实现它却很简单。对于metaclass=abc.ABCMeta来说,它只不过是语法上的糖分。要自己实现它,只需使用以下代码段:

import abc

class ABC(metaclass=abc.ABCMeta):
    pass

自动注册插件系统

元类最常见的用途之一是让类自动注册为插件/处理程序。这些例子可以在许多项目中看到,比如 web 框架。但是,这些代码库太广泛,无法在这里进行有效的解释。因此,我们将展示一个简单的示例,展示元类作为自注册plugin系统的强大功能:

>>> import abc

>>> class Plugins(abc.ABCMeta):
...     plugins = dict()
...
...     def __new__(metaclass, name, bases, namespace):
...         cls = abc.ABCMeta.__new__(metaclass, name, bases,
...                                   namespace)
...         if isinstance(cls.name, str):
...             metaclass.plugins[cls.name] = cls
...         return cls
...
...     @classmethod
...     def get(cls, name):
...         return cls.plugins[name]

>>> class PluginBase(metaclass=Plugins):
...     @property
...     @abc.abstractmethod
...     def name(self):
...         raise NotImplemented()

>>> class SpamPlugin(PluginBase):
...     name = 'spam'

>>> class EggsPlugin(PluginBase):
...     name = 'eggs'

>>> Plugins.get('spam')
<class '...SpamPlugin'>
>>> Plugins.plugins
{'spam': <class '...SpamPlugin'>,
 'eggs': <class '...EggsPlugin'>}

这个例子当然有点简单,但它是许多插件系统的基础。这是在实施此类系统时需要注意的一件非常重要的事情;但是,虽然元类在定义时运行,但仍然需要导入模块才能工作。有几种方法可以做到这一点;我赞成通过get方法按需加载,因为如果不使用插件,也不会增加加载时间。

以下示例将使用以下文件结构来获得可复制的结果。所有文件都将包含在插件目录中。

__init__.py文件用于创建快捷方式,因此简单的导入插件将导致plugins.Plugins可用,而不需要显式导入plugins.base

# plugins/__init__.py
from .base import Plugin
from .base import Plugins

__all__ = ['Plugin', 'Plugins']

包含Plugins集合和Plugin基类的base.py文件:

# plugins/base.py
import abc

class Plugins(abc.ABCMeta):
    plugins = dict()

    def __new__(metaclass, name, bases, namespace):
        cls = abc.ABCMeta.__new__(
            metaclass, name, bases, namespace)
        if isinstance(cls.name, str):
            metaclass.plugins[cls.name] = cls
        return cls

    @classmethod
    def get(cls, name):
        return cls.plugins[name]

class Plugin(metaclass=Plugins):
    @property
    @abc.abstractmethod
    def name(self):
        raise NotImplemented()

还有两个简单的插件,spam.py

from . import base

class Spam(base.Plugin):
    name = 'spam'

eggs.py

from . import base

class Eggs(base.Plugin):
    name = 'eggs'

按需导入插件

导入问题的第一个解决方案只是在Plugins元类的get方法中处理它。每当在注册表中找不到插件时,它应该自动从plugins目录加载模块。

这种方法的优点是,不仅插件不需要显式地预加载,而且插件只在需要时才加载。不接触未使用的插件,因此此方法有助于减少应用的加载时间。

缺点是代码不会运行或测试,因此它可能会被完全破坏,直到最后加载为止,您不会知道它。此问题的解决方案将在测试章节、第 10 章测试和日志记录–准备 bug中介绍。另一个问题是,如果代码自行注册到应用的其他部分,那么该代码也不会执行。

修改的Plugins.get方法,我们得到如下结果:

import abc
import importlib

class Plugins(abc.ABCMeta):
    plugins = dict()

    def __new__(metaclass, name, bases, namespace):
        cls = abc.ABCMeta.__new__(
            metaclass, name, bases, namespace)
        if isinstance(cls.name, str):
            metaclass.plugins[cls.name] = cls
        return cls

    @classmethod
    def get(cls, name):
        if name not in cls.plugins:
            print('Loading plugins from plugins.%s' % name)
            importlib.import_module('plugins.%s' % name)
        return cls.plugins[name]

执行时会产生以下结果:

>>> import plugins
>>> plugins.Plugins.get('spam')
Loading plugins from plugins.spam
<class 'plugins.spam.Spam'>

>>> plugins.Plugins.get('spam')
<class 'plugins.spam.Spam'>

如您所见,这种方法只会导致运行import一次。第二次,插件将在插件字典中可用,因此无需加载。

通过配置导入插件

虽然只加载所需的插件通常是一个更好的主意,但可以说可以预先加载您可能需要的插件。由于显式优于隐式,所以要加载的插件的显式列表通常是一个好的解决方案。这种方法的附加优点是,首先,您可以使注册更高级,因为您可以保证注册是运行的,其次,您可以从多个包加载插件。

get方法中不引入,而是增加load方法;导入所有给定模块名称的load方法:

import abc
import importlib

class Plugins(abc.ABCMeta):
    plugins = dict()

    def __new__(metaclass, name, bases, namespace):
        cls = abc.ABCMeta.__new__(
            metaclass, name, bases, namespace)
        if isinstance(cls.name, str):
            metaclass.plugins[cls.name] = cls
        return cls

    @classmethod
    def get(cls, name):
        return cls.plugins[name]

    @classmethod
    def load(cls, *plugin_modules):
        for plugin_module in plugin_modules:
            plugin = importlib.import_module(plugin_module)

可以使用以下代码调用:

>>> import plugins

>>> plugins.Plugins.load(
...     'plugins.spam',
...     'plugins.eggs',
... )

>>> plugins.Plugins.get('spam')
<class 'plugins.spam.Spam'>

一个相当简单和直接的系统,根据设置加载插件,这可以很容易地与任何类型的设置系统结合,以填充load方法。

通过文件系统导入插件

只要有可能,最好避免让系统依赖于文件系统上模块的自动检测,因为这直接违反了PEP8。具体来说,“显性优于隐性”。虽然这些系统在特定情况下可以正常工作,但它们往往使调试变得更加困难。Django 类似的自动导入系统让我相当头疼,因为它们往往会混淆错误。话虽如此,基于插件目录中所有文件的自动插件加载仍然是一个值得演示的可能性。

import os
import re
import abc
import importlib

MODULE_NAME_RE = re.compile('[a-z][a-z0-9_]*', re.IGNORECASE)

class Plugins(abc.ABCMeta):
    plugins = dict()

    def __new__(metaclass, name, bases, namespace):
        cls = abc.ABCMeta.__new__(
            metaclass, name, bases, namespace)
        if isinstance(cls.name, str):
            metaclass.plugins[cls.name] = cls
        return cls

    @classmethod
    def get(cls, name):
        return cls.plugins[name]

    @classmethod
    def load_directory(cls, module, directory):
        for file_ in os.listdir(directory):
            name, ext = os.path.splitext(file_)
            full_path = os.path.join(directory, file_)
            import_path = [module]
            if os.path.isdir(full_path):
                import_path.append(file_)
            elif ext == '.py' and MODULE_NAME_RE.match(name):
                import_path.append(name)
            else:
                # Ignoring non-matching files/directories
                continue

            plugin = importlib.import_module('.'.join(import_path))

    @classmethod
    def load(cls, **plugin_directories):
        for module, directory in plugin_directories.items():
            cls.load_directory(module, directory)

如果可能的话,我会尽量避免使用全自动导入系统,因为它很容易出现意外错误,并且会使调试更加困难,更不用说导入订单无法通过这种方式轻松控制。为了使这个系统更加智能(甚至在 Python 路径之外导入包),您可以使用importlib.abc中的抽象基类创建插件加载器。请注意,您很可能仍然需要通过os.listdiros.walk列出目录。

实例化类时的操作顺序

当调试动态创建和/或修改的类时,类实例化期间的操作顺序非常重要。类的实例化按以下顺序进行。

寻找元类

元类来自类或bases上显式给出的元类,或者使用默认的type元类。

对于每个类、类本身和基,将使用以下项的第一个匹配:

  • 显式给定元类

  • 基于基的显式元类

  • type()

    请注意,如果没有找到作为所有候选元类的子类型的元类,将引发一个TypeError。这种情况不太可能发生,但在使用元类的多重继承/混合时肯定会发生。

准备名称空间

类名称空间是通过前面选择的元类准备的。如果元类有一个__prepare__方法,它将被调用namespace = metaclass__prepare__(names, bases, **kwargs),其中**kwargs来源于类定义。如果没有可用的__prepare__方法,则结果为namespace = dict()

请注意,实现自定义名称空间有多种方法,正如我们在上一段中所看到的,type()函数调用还采用了一个dict参数,该参数也可用于更改名称空间。

执行类主体

类的主体执行与正常代码执行非常相似,但有一个关键区别,即单独的名称空间。因为类有一个单独的名称空间,它不应该污染globals()/locals()名称空间,所以它是在该上下文中执行的。生成的调用类似于:exec(body, globals(), namespace)其中namespace是先前生成的名称空间。

创建类对象(非实例)

现在我们已经准备好了所有组件,就可以生成实际的类对象了。这是通过class_ = metaclass(name, bases, namespace, **kwargs)调用完成的。如您所见,这实际上与前面讨论的type()调用相同。**kwargs这里与前面传递给__prepare__方法的相同。

请注意,这也是将从无参数的super()调用中引用的对象,这可能很有用。

执行类装饰器

既然类对象实际上已经完成,那么将执行类装饰器。由于这仅在类对象中的所有其他内容都已构造好之后执行,因此很难修改类属性,例如哪些类正在被继承,以及类的名称。通过修改__class__对象,您仍然可以修改或覆盖这些内容,但这至少更加困难。

创建类实例

从前面生成的类对象,我们现在终于可以创建实际的实例,就像您通常使用类一样。应该注意的是,与前面的步骤不同,此步骤和类装饰器步骤是每次实例化类时执行的唯一步骤。这两个步骤之前的步骤仅在每个类定义中执行一次。

示例

理论够了!让我们举例说明类对象的创建和实例化,以便检查操作的顺序:

>>> import functools

>>> def decorator(name):
...     def _decorator(cls):
...         @functools.wraps(cls)
...         def __decorator(*args, **kwargs):
...             print('decorator(%s)' % name)
...             return cls(*args, **kwargs)
...         return __decorator
...     return _decorator

>>> class SpamMeta(type):
...
...     @decorator('SpamMeta.__init__')
...     def __init__(self, name, bases, namespace, **kwargs):
...         print('SpamMeta.__init__()')
...         return type.__init__(self, name, bases, namespace)
...
...     @staticmethod
...     @decorator('SpamMeta.__new__')
...     def __new__(cls, name, bases, namespace, **kwargs):
...         print('SpamMeta.__new__()')
...         return type.__new__(cls, name, bases, namespace)
...
...     @classmethod
...     @decorator('SpamMeta.__prepare__')
...     def __prepare__(cls, names, bases, **kwargs):
...         print('SpamMeta.__prepare__()')
...         namespace = dict(spam=5)
...         return namespace

>>> @decorator('Spam')
... class Spam(metaclass=SpamMeta):
...
...     @decorator('Spam.__init__')
...     def __init__(self, eggs=10):
...         print('Spam.__init__()')
...         self.eggs = eggs
decorator(SpamMeta.__prepare__)
SpamMeta.__prepare__()
decorator(SpamMeta.__new__)
SpamMeta.__new__()
decorator(SpamMeta.__init__)
SpamMeta.__init__()

# Testing with the class object
>>> spam = Spam
>>> spam.spam
5
>>> spam.eggs
Traceback (most recent call last):
 ...
AttributeError: ... object has no attribute 'eggs'

# Testing with a class instance
>>> spam = Spam()
decorator(Spam)
decorator(Spam.__init__)
Spam.__init__()
>>> spam.spam
5
>>> spam.eggs
10

示例清楚地显示了类的创建顺序:

  1. 通过__prepare__准备名称空间。
  2. 使用__new__创建类主体。
  3. 使用__init__初始化元类(注意,这不是类__init__
  4. 通过类装饰器初始化类。
  5. 通过 class__init__函数初始化类。

我们可以从中注意到的一点是,每次实际实例化类时都会执行类装饰器,而不是在此之前。当然,这既有优点也有缺点,但是如果您希望构建所有子类的寄存器,那么使用元类肯定更方便,因为在实例化类之前,装饰器不会注册。

除此之外,在实际创建类对象(而不是实例)之前修改名称空间的功能也非常强大。例如,它可以方便地在多个类对象之间共享某个范围,或者方便地确保某些项在该范围内始终可用。

按定义顺序存储类属性

在某些情况下,定义顺序会产生影响。例如,假设我们正在创建一个表示 CSV(逗号分隔值)格式的类。CSV 格式要求字段具有特定顺序。在某些情况下,这将由标题指示,但保持一致的字段顺序仍然很有用。类似的系统在 ORM 系统(如 SQLAlchemy)中使用,以在 Django 中存储表定义的列顺序和表单中的输入字段顺序。

没有元类的经典解

存储字段顺序的一种简单方法是为字段实例提供一种特殊的__init__方法,该方法为每个定义递增,因此字段具有递增索引属性。这个解决方案可以被认为是经典的解决方案,因为它也适用于 Python2。

>>> import itertools

>>> class Field(object):
...     counter = itertools.count()
...
...     def __init__(self, name=None):
...         self.name = name
...         self.index = next(Field.counter)
...
...     def __repr__(self):
...         return '<%s[%d] %s>' % (
...             self.__class__.__name__,
...             self.index,
...             self.name,
...         )

>>> class FieldsMeta(type):
...     def __new__(metaclass, name, bases, namespace):
...         cls = type.__new__(metaclass, name, bases, namespace)
...         fields = []
...         for k, v in namespace.items():
...             if isinstance(v, Field):
...                 fields.append(v)
...                 v.name = v.name or k
...
...         cls.fields = sorted(fields, key=lambda f: f.index)
...         return cls

>>> class Fields(metaclass=FieldsMeta):
...     spam = Field()
...     eggs = Field()

>>> Fields.fields
[<Field[0] spam>, <Field[1] eggs>]

>>> fields = Fields()
>>> fields.eggs.index
1
>>> fields.spam.index
0
>>> fields.fields
[<Field[0] spam>, <Field[1] eggs>]

为了的方便,也为了让事情变得更漂亮,我们增加了FieldsMeta类。这里不严格要求,但如果需要,它会自动填充名称,并添加包含字段排序列表的fields列表。

使用元类获得排序的名称空间

前面的解决方案更简单,也支持 Python 2,但对于 Python 3,我们有更多的选择。正如您在前面的段落中看到的,自 Python 3 以来,我们有__prepare__方法,它返回名称空间。从前面的章节中,您可能还记得collections.OrderedDict,所以让我们看看当我们将它们组合在一起时会发生什么。

>>> import collections

>>> class Field(object):
...     def __init__(self, name=None):
...         self.name = name
...
...     def __repr__(self):
...         return '<%s %s>' % (
...             self.__class__.__name__,
...             self.name,
...         )

>>> class FieldsMeta(type):
...     @classmethod
...     def __prepare__(metaclass, name, bases):
...         return collections.OrderedDict()
...
...     def __new__(metaclass, name, bases, namespace):
...         cls = type.__new__(metaclass, name, bases, namespace)
...         cls.fields = []
...         for k, v in namespace.items():
...             if isinstance(v, Field):
...                 cls.fields.append(v)
...                 v.name = v.name or k
...
...         return cls

>>> class Fields(metaclass=FieldsMeta):
...     spam = Field()
...     eggs = Field()

>>> Fields.fields
[<Field spam>, <Field eggs>]
>>> fields = Fields()
>>> fields.fields
[<Field spam>, <Field eggs>]

正如您所看到的,字段确实是按照我们定义的顺序排列的。先Spameggs。由于类名称空间现在是一个collections.OrderedDict实例,我们知道顺序是有保证的。而不是 Pythondict的常规非预定顺序。这说明了元类以通用方式扩展类是多么方便。元类(而不是自定义的__init__方法)的另一大优势是,如果用户忘记调用父__init__方法,则不会丢失功能。元类将始终被执行,除非添加了不同的元类,即。

总结

Python 元类系统是每个 Python 程序员一直在使用的东西,可能甚至不知道它。每一个类都应该通过type的某个(子类)来创建,这允许无限的定制和魔法。现在,您可以像平常一样创建类,而不是静态地定义类,并在定义过程中动态地从类中添加、修改或删除属性;非常神奇但非常有用。然而,神奇的组件也是使用它时要格外小心的原因。虽然元类可以使您的生活更加轻松,但它们也是生成完全不可理解代码的最简单方法之一。

不管怎样,元类有一些很好的用例,许多库,比如SQLAlchemyDjango都使用元类来让代码工作得更简单、更好。实际上,使用这些库通常不需要理解内部使用的魔法,这使得案件可以辩护。现在的问题是,对于初学者来说,一个更好的体验是否值得一些内在的黑暗魔法,看看这些库的成功,在这种情况下,我会说是的。

总之,在考虑使用元类时,请记住 Tim Peters 曾经说过的话:“元类比 99%的用户应该担心的更神奇。如果你想知道是否需要它们,你就不需要。”

现在,我们将继续使用一个解决方案来消除元类产生的一些魔力:文档。下一章将向我们展示如何记录代码,如何测试文档,最重要的是,如何通过在文档中注释类型使文档变得更智能。