当前位置: 代码迷 >> 综合 >> Fluent Python - Part21类元编程
  详细解决方案

Fluent Python - Part21类元编程

热度:57   发布时间:2023-10-21 05:13:42.0

(元类) 是深奥的知识,99% 的用户都无需关注。如果你想知道是否需要使用元类,我告诉你,不需要 (真正需要使用元类的人确信他们需要,无需解释原因)。

--- Timsort 算法的发明者,活跃的 Python 贡献者

类元编程是指在运行时创建或定制类的技艺。在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,而无需使用 class 关键字。类装饰器也是函数,不过能够审查,修改,甚至把被装饰的类替换成其他类。最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类种,例如我们见过的抽象基类。

元类功能强大,但是难以掌握,类装饰器能使用更简单的方式解决很多问题。

本章还会谈及导入时和运行时的区别 — 这是有效使用 Python 元编程的重要基础。

首先本章探讨如何在运行时创建类。

类工厂函数

假设我在编写一个宠物店应用程序,我想把狗的数据当作简单的记录处理。编写下面的样板代码让人厌烦:

class Dog:def __init__(self, name, weight, owner):self.name = nameself.weight = weightself.owner = owner

各个字段名称出现了三次。写了这么多样板代码,甚至字符串表示形式都不友好。参考 collections.namedtuple,下面我们创建一个 record.factory 函数,即时创建简单的类(如 Dog)。

def record_factory(cls_name, field_names):try:field_names = field_names.replace(',', ' ').split()except AttributeError:passfield_names = tuple(field_names)def __init__(self, *args, **kwargs):attrs = dict(zip(self.__slots__, args))attrs.update(kwargs)for name, value in attrs.items():setattr(self, name, value)def __iter__(self):for name in self.__slots__:yield getattr(self, name)def __repr__(self):values = ', '.join('{}={!r}'.format(*i) for iin zip(self.__slots__, self))return '{}({})'.format(self.__class__.__name__, values)cls_attrs = dict(__slots__ = field_names,__init__ = __init__,__iter__ = __iter__,__repr__ = __repr__)return type(cls_name, (object, ), cls_attrs)Dog = record_factory('Dog', 'name weight owner')
rex = Dog('Rex', 30, 'Bob')
print(rex)
name, weight, _ = rex
print(name, weight)
rex.weight = 32
print(rex)
print(Dog.__mro__)""" output: Dog(name='Rex', weight=30, owner='Bob') Rex 30 Dog(name='Rex', weight=32, owner='Bob') (<class '__main__.Dog'>, <class 'object'>) """

定制描述符的类装饰器

20节中的 LineItem 示例还有个问题没有解决:储存属性的名称不具有描述性,即属性(如 weight) 的值存储在名为 _Quantity#0 的实例属性中,这样的名称有点不便于调试。

我们不能使用描述性的储存属性名称,因为实例化描述符时无法得知托管属性(即绑定到描述符上的类属性,例如前述示例的 weight)的名称。可是,一旦组建好整个类,而且把描述符绑定到类属性上之后,我们就可以审查类,并为描述符设置合理的储存属性名称。因此,我们在创建类时设置储存属性的名称。使用类装饰器或元类可以做到这一点。我们首先使用较简单的方式。

类装饰器与函数装饰器非常类似,是参数为类对象的函数,返回原来的类或修改后的类。

@model.entity
class LineItem:description = model.NonBlank()weight = model.Quantity()price = model.Quantity()def __init__(self, description, weight, price):self.description = descriptionself.weight = weightself.price = pricedef subtotal(self):return self.weight * self.price
def entity(cls):for key, attr in cls.__dict__.items():if isinstance(attr, Validated):type_name = type(attr).__name__attr.storage_name = '_{}#{}'.format(type_name, key)

类装饰器有个重大缺点:只对直接依附的类有效。这意味着,被装饰的类的子类可能继承也可能不继承装饰器所做的改动。如果想定制整个类层次结构,而不是一次只定制一个类,使用下一节介绍的元类更高效。

元类基础知识

元类是制造类的工厂,不过不是函数,而是类。

根据 Python 对象模型,类是对象,因此类肯定是另外某个列的实例。默认情况下,Python 中的类是 type 类的实例。也就是说, type 是大多数内置的类和用户定义的类的元类。为了避免无限回溯,type 是其自身的实例。

Fluent Python - Part21类元编程

object 类和 type 类之间的关系很独特,objecttype 的实例,而 typeobject 的子类。这种关系很神奇,无法用 Python 代码表述,因为定义其中一个之前另一个必须存在。

除了 type,标准库中海油一些别的元类,例如 ABCMetaEnumcollections.Iterable 所属的类是 abc.ABCMetaIterable 是抽象类,而 ABCMeta 不是,IterableABCMeta 的实例。

>>> import collections
>>> collections.Iterable.__class__
<class 'abc.ABCMeta'>
>>> import abc
>>> abc.ABCMeta.__class__
<class 'type'>
>>> abc.ABCMeta.__mro__
(<class 'abc.ABCMeta'>, <class 'type'>, <class 'object'>)

向上追溯,ABCMeta 最终所属的类也是 type.所有类都直接或间接地是 type 的实例,不过只有元类同时也是 type 的子类。若想理解元类,一定要知道这种关系:元类(如 ABCMeta) 从 type 类继承了构建类的能力。

Fluent Python - Part21类元编程

我们要抓住的重点是,所有类都是 type 的实例,但是元类还是 type 的子类,因此可以作为制造类的工厂。具体来说,元类可以通过实现 __init__ 方法定制实例。元类的 __init__ 方法可以做到类装饰器能做的任何事情,但是作用更大。

定制描述符的元类

class LineItem(model.Entity):description = model.NonBlank()weight = model.Quantity()price = model.Quantity()def __init__(self, description, weight, price):self.description = descriptionself.weight = weightself.price = pricedef subtotal(self):return self.weight * self.priceclass EntityMeta(type):"""元类,用于创建带有验证字段的业务实体"""def __init__(cls, name, bases, attr_dict):super().__init__(name, bases, attr_dict)for key, attr in attr_dict.items():if isinstance(attr, Validated):type_name = type(attr).__name__attr.storage_name = '_{}#{}'.format(type_name, key)class Entity(metaclass=EntityMeta):"""带有验证字段的业务实体"""

元类的特殊方法 __prepare__

在某些应用中,可能需要知道类的属性定义的顺序。 type 构造方法及元类的 __new____init__ 方法都会受到计算的类的定义体,形式是名称到属性的映射。然而在默认情况下,那个映射是字典;也就是说,元类或类装饰器获得映射时,属性在类定义体中的顺序已经丢失了。

这个问题的解决办法是,使用 Python3 引入的特殊方法 __prepare__.这个特殊方法只在元类中有用,而且必须声明为类方法(即,要使用 @classmethod 装饰器定义)。解释器调用元类的 __new__ 方法之前会先调用 __prepare__ 方法,使用类定义体中的属性创建映射。__prepare__ 方法第一个参数是元类,随后两个参数分别是要构建的类的名称和基类组成的元组,返回值必须是映射。元类构建新类时,__prepare__ 方法返回的映射会传给 __new__ 方法的最后一个参数,然后再传给 __init__ 方法。

import  collectionsclass EntityMeta(type):"""元类,用于创建带有验证字段的业务实体"""@classmethoddef __prepare__(metacls, name, bases):return collections.OrderedDict()def __init__(cls, name, bases, attr_dict):super().__init__(name, bases, attr_dict)cls._field_names = []for key, attr in attr_dict.items():if isinstance(attr, Validated):type_name = type(attr).__name__attr.storage_name = '_{}#{}'.format(type_name, key)cls._field_names.append(key)class Entity(metaclass=EntityMeta):"""带有验证字段的业务实体"""@classmethoddef field_names(cls):for name in cls._field_names:yield name

类作为对象

Python 数据模型为每个类定义了很多属性,其中三个属性在本书中已经见过多次: __mro__, __class____name__.此外,还有以下属性。

  • cls.__bases__: 由类的基类组成的元组。
  • cls.__qualname__: Python3.3 新引入的属性,其值是类或函数的限定名称,即从模块的全局作用域到类的点分路径。
  • cls.__subclasses__():这个方法返回一个列表,包含类的直接子类。这个方法的实现使用了弱引用,防止在超类和子类(子类在 __bases__ 属性中储存指向超类的强引用)之间出现循环引用。这个方法返回的列表中是内存里现存的子类。
  • cls.mro(): 构建类时,如果需要获取储存在类属性 __mro__ 中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类解析方法顺序。