类元编程指在运行时创建或定制类的技艺。在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,无须使用 class 关键字。类装饰器也是函数,不过能够审查、修改,甚至把被装饰的类替换成另一个类。最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类,例如前面讲过的抽象基类。
defparse_identifiers(names: FieldNames) -> tuple[str, ...]: ifisinstance(names, str): names = names.replace(',', ' ').split() ifnotall(s.isidentifier() for s in names): raise ValueError('names must all be valid identifiers') returntuple(names)
用户可以使用一整个字符串或者产出字符串的可迭代对象提供字段名称
使用属性名构建一个元组,这将成为新建类的 __slots__ 属性的值
组建类属性字典后,调用 type 构造函数,构建新类,然后将其返回
这里的返回类型注解为 type[tuple],表示返回的是一个类型(即 type 类型的实例,即某个类),而且返回的类是 tuple 的子类。
__init__subclass__
之前我们学习过,借助 typing.NamedTuple 和 @dataclass,程序员可以使用 class 语句为新类指定属性,然后由类构建器自动添加 __init__、__repr__、__eq__ 等基本的方法,增强新类的功能。这两个类构建器均读取用户在 class 语句中添加的类型提示,以增强类的功能。静态类型检查工具还能通过那些类型提示验证用于设置或获取属性的代码。然而,NamedTuple 和 @dataclass 在运行时不能利用类型提示验证属性。
我们将创建一个 Checked 类型,使用方法类似于 typing.NamedTuple:
1 2 3 4 5 6 7 8 9 10
>>> classMovie(Checked): ... title: str ... year: int ... box_office: float ... >>> movie = Movie(title='The Godfather', year=1972, box_office=137) >>> movie.title 'The Godfather' >>> movie Movie(title='The Godfather', year=1972, box_office=137.0)
from collections.abc importCallable from typing importAny, NoReturn, get_type_hints
classField: def__init__(self, name: str, constructor: Callable) -> None: ifnotcallable(constructor) or constructor istype(None): raise TypeError(f'{name!r} type hint must be callable') self.name = name self.constructor = constructor
def__set__(self, instance: Any, value: Any) -> None: if value is ...: value = self.constructor() else: try: value = self.constructor(value) except (TypeError, ValueError) as e: type_name = self.constructor.__name__ msg = f'{value!r} is not compatible with {self.name}:{type_name}' raise TypeError(msg) from e instance.__dict__[self.name] = value
使用内置函数 callable 做运行时检查,判断 constructor 是否可调用
如果 Checked.__init__ 把 value 设为 ...(内置对象 Ellipsis),就调用无参数的 constructor
def__init_subclass__(subclass) -> None: super().__init_subclass__() for name, constructor in subclass._fields().items(): setattr(subclass, name, Field(name, constructor))
def__init__(self, **kwargs: Any) -> None: for name in self._fields(): value = kwargs.pop(name, ...) setattr(self, name, value) if kwargs: self.__flag_unknown_attrs(*kwargs)
def__setattr__(self, name: str, value: Any) -> None: if name in self._fields(): cls = self.__class__
>>> import evaldemo @ builderlib module start @ Builder body @ Descriptor body @ builderlib module end # evaldemo module start # Klass body @ Descriptor.__init__(<Descriptor instance>) # Klass body end @ Descriptor.__set_name__(<Descriptor instance>, <class'evaldemo.Klass'>, 'attr') @ Builder.__init_subclass__(<class'evaldemo.Klass'>) @ deco(<class'evaldemo.Klass'>) # evaldemo module end
在开始定义 main 函数前,Python 内置的 type.__new__ 方法已经创建 Klass 类型对象,这就可以在描述符类的各个实例上调用 __set_name__ ,把 Klass 传给 owner 参数
然后,type.__new__ 在 Klass的超类上调用 __init_subclass__,传入的第一个参数是 Klass
type.__new__ 返回类对象后,Python 应用装饰器。在这个示例中,deco 返回的类绑定到模块命名空间中的 Klass 上
以上操作都是在 导入时 触发的。如果直接运行 evaldemo.py 脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@ builderlib module start @ Builder body @ Descriptor body @ builderlib module end # evaldemo module start # Klass body @ Descriptor.__init__(<Descriptor instance>) # Klass body end @ Descriptor.__set_name__(<Descriptor instance>, <class'__main__.Klass'>, 'attr') @ Builder.__init_subclass__(<class'__main__.Klass'>) @ deco(<class'__main__.Klass'>) @ Builder.__init__(<Klass instance>) # Klass.__init__(<Klass instance>) @ SuperA.__init_subclass__:inner_0(<Klass instance>) @ deco:inner_1(<Klass instance>) @ Descriptor.__set__(<Descriptor instance>, <Klass instance>, 999) # evaldemo module end
def__init__(self, **kwargs): for name, default in defaults.items(): setattr(self, name, kwargs.pop(name, default)) if kwargs: extra = ', '.join(kwargs) raise AttributeError(f'No slots left for: {extra!r}')
def__repr__(self): rep = ', '.join(f'{name}={value!r}' for name, default in defaults.items() if (value := getattr(self, name)) != default) returnf'{cls_name}({rep})'
for name, value in cls_dict.items(): if name.startswith('__') and name.endswith('__'): if name in new_dict: raise AttributeError(f"Can't set {name!r} in {cls_name!r}") new_dict[name] = value else: new_dict['__slots__'].append(name) defaults[name] = value returnsuper().__new__(meta_cls, cls_name, bases, new_dict)
classField: def__init__(self, name: str, constructor: Callable) -> None: ifnotcallable(constructor) or constructor istype(None): raise TypeError(f'{name!r} type hint must be callable') self.name = name self.storage_name = '_' + name self.constructor = constructor
def__get__(self, instance, owner=None): if instance isNone: return self returngetattr(instance, self.storage_name)
def__set__(self, instance: Any, value: Any) -> None: if value is ...: value = self.constructor() else: try: value = self.constructor(value) except (TypeError, ValueError) as e: type_name = self.constructor.__name__ msg = f'{value!r} is not compatible with {self.name}:{type_name}' raise TypeError(msg) from e setattr(instance, self.storage_name, value)
在前面的示例中,各个 Field 描述符实例把值存在托管实例的同名属性中。例如,在 Movie 类中,title 描述符把字段的值存在托管实例的 title 属性中。因此,Field 不能提供 __get__ 方法(在 __get__ 方法内直接使用 instance.__dict__ 访问同名实例属性是否可行?)
然而,使用 __slots__ 之后,对于 Movie 这样的类,类属性和实例属性不能同名。每个描述符实例都是一个类属性,而为了让各个实例单独存储属性,选择在托管实例的属性名前加上 _ 作为前缀
元类的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
classCheckedMeta(type):
def__new__(meta_cls, cls_name, bases, cls_dict): if'__slots__'notin cls_dict: slots = [] type_hints = cls_dict.get('__annotations__', {}) for name, constructor in type_hints.items(): field = Field(name, constructor) cls_dict[name] = field slots.append(field.storage_name)
def__init__(self, **kwargs: Any) -> None: for name in self._fields(): value = kwargs.pop(name, ...) setattr(self, name, value) if kwargs: self.__flag_unknown_attrs(*kwargs)
def__flag_unknown_attrs(self, *names: str) -> NoReturn: plural = 's'iflen(names) > 1else'' extra = ', '.join(f'{name!r}'for name in names) cls_name = repr(self.__class__.__name__) raise AttributeError(f'{cls_name} object has no attribute{plural}{extra}')
def_asdict(self) -> dict[str, Any]: return { name: getattr(self, name) for name, attr in self.__class__.__dict__.items() ifisinstance(attr, Field) }
def__repr__(self) -> str: kwargs = ', '.join( f'{key}={value!r}'for key, value in self._asdict().items() ) returnf'{self.__class__.__name__}({kwargs})'