Fluent Python - Part21类元编程

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

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

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

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

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

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

类工厂函数

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

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

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

def record_factory(cls_name, field_names):
    try:
        field_names = field_names.replace(',', ' ').split()
    except AttributeError:
        pass
    field_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 i
                            in 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 = description
        self.weight = weight
        self.price = price

    def 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 是其自身的实例。

在这里插入图片描述

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 类继承了构建类的能力。

image.png

我们要抓住的重点是,所有类都是 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 = description
       self.weight = weight
       self.price = price

   def subtotal(self):
       return self.weight * self.price

class 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  collections

class EntityMeta(type):
    """元类,用于创建带有验证字段的业务实体"""

    @classmethod
    def __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):
    """带有验证字段的业务实体"""
    
    @classmethod
    def 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__ 中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类解析方法顺序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值