我们继续谈论Python中的元编程。 如果使用正确,它可以使您快速而优雅地实现复杂的设计模式。 在本文的最后一部分 ,我们展示了如何使用元类来更改实例和类的属性。

现在,让我们看看如何更改方法调用。 您可以在“ 高级Python”课程中了解有关元编程选项的更多信息。
调试和跟踪调用
正如您已经了解的那样,使用元类,任何类都可以转换为无法识别的形式。 例如,将所有类方法替换为其他类方法,或将任意修饰符应用于每个方法。 您可以使用此想法来调试应用程序性能。
以下元类衡量类及其实例中每个方法的执行时间,以及实例本身的创建时间:
from contextlib import contextmanager import logging import time import wrapt @contextmanager def timing_context(operation_name): """ """ start_time = time.time() try: yield finally: logging.info('Operation "%s" completed in %0.2f seconds', operation_name, time.time() - start_time) @wrapt.decorator def timing(func, instance, args, kwargs): """ . https://wrapt.readthedocs.io/en/latest/ """ with timing_context(func.__name__): return func(*args, **kwargs) class DebugMeta(type): def __new__(mcs, name, bases, attrs): for attr, method in attrs.items(): if not attr.startswith('_'):
让我们看一下实际的调试:
class User(metaclass=DebugMeta): def __init__(self, name): self.name = name time.sleep(.7) def login(self): time.sleep(1) def logout(self): time.sleep(2) @classmethod def create(cls): time.sleep(.5) user = User('Michael') user.login() user.logout() user.create()
尝试DebugMeta
扩展DebugMeta
并记录方法的签名及其堆栈跟踪。
独来独往的模式和禁止继承
现在,让我们继续探讨在Python项目中使用元类的特殊情况。
当然,您中的许多人肯定会使用常规的Python模块来实现单例设计模式(也称为Singleton),因为它比编写适当的元类要方便和快捷得多。 但是,出于学术兴趣,让我们编写其实现之一:
class Singleton(type): instance = None def __call__(cls, *args, **kwargs): if cls.instance is None: cls.instance = super().__call__(*args, **kwargs) return cls.instance class User(metaclass=Singleton): def __init__(self, name): self.name = name def __repr__(self): return f'<User: {self.name}>' u1 = User('Pavel')
此实现有一个有趣的细微差别-由于第二次不调用类构造函数,因此您可能会犯一个错误,并且无法在其中传递必要的参数,并且如果实例已经创建,则在运行时将不会发生任何事情。 例如:
>>> User('Roman') <User: Roman> >>> User('Alexey', 'Petrovich', 66)
现在让我们看一个更奇特的选择:禁止从特定类继承。
class FinalMeta(type): def __new__(mcs, name, bases, attrs): for cls in bases: if isinstance(cls, FinalMeta): raise TypeError(f"Can't inherit {name} class from final {cls.__name__}") return super().__new__(mcs, name, bases, attrs) class A(metaclass=FinalMeta): """ !""" pass class B(A): pass
在前面的示例中,我们使用元类来自定义类的创建,但是您可以走得更远并开始参数化元类的行为。
例如,可以在声明类时将函数传递给metaclass参数,并根据某些条件从其中返回不同的元类实例,例如:
def get_meta(name, bases, attrs): if SOME_SETTING: return MetaClass1(name, bases, attrs) else: return MetaClass2(name, bases, attrs) class A(metaclass=get_meta): pass
但是一个更有趣的示例是在声明类时使用extra_kwargs
参数。 假设您要使用元类来更改类中某些方法的行为,并且每个类对于这些方法可能具有不同的名称。 怎么办 这是什么
在我看来,它非常优雅! 您可以想出很多使用此参数化的模式,但请记住主要规则-一切都适中。
__prepare__
方法__prepare__
最后,我将讨论__prepare__
方法的可能用法。 如上所述,此方法应返回一个字典对象,解释器在解析类主体时填充该对象,例如,如果__prepare__
返回对象d = dict()
,则在读取以下类时:
class A: x = 12 y = 'abc' z = {1: 2}
解释器将执行以下操作:
d['x'] = 12 d['y'] = 'abc' d['z'] = {1: 2}
此功能有多种可能的用途。 它们都具有不同程度的效用,因此:
- 在Python = <3.5版本中,如果我们需要保持类中声明方法的顺序,则可以返回
__prepare__
方法中的__prepare__
,在较旧的版本中,内置词典已经保留了添加键的顺序,因此不再需要OrderedDict
。 enum
标准库模块使用定制的类似dict的对象来确定在声明时何时复制类属性。 该代码可以在这里找到。- 根本不是生产就绪的代码,但是一个很好的例子是对参数多态性的支持。
例如,考虑以下类,它具有单个多态方法的三种实现:
class Terminator: def terminate(self, x: int): print(f'Terminating INTEGER {x}') def terminate(self, x: str): print(f'Terminating STRING {x}') def terminate(self, x: dict): print(f'Terminating DICTIONARY {x}') t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'})
显然,最后一个声明的terminate
方法将覆盖前两个方法的实现,并且我们需要根据传递的参数的类型来选择方法。 为此,我们编写了两个附加的包装器对象:
class PolyDict(dict): """ , PolyMethod. """ def __setitem__(self, key: str, func): if not key.startswith('_'): if key not in self: super().__setitem__(key, PolyMethod()) self[key].add_implementation(func) return None return super().__setitem__(key, func) class PolyMethod: """ , . , : instance method, staticmethod, classmethod. """ def __init__(self): self.implementations = {} self.instance = None self.cls = None def __get__(self, instance, cls): self.instance = instance self.cls = cls return self def _get_callable_func(self, impl):
上面的代码中最有趣的是PolyMethod
对象,该对象存储具有相同方法实现的注册表,具体取决于传递给此方法的参数的类型。 我们将从__prepare__
方法返回PolyDict
对象,从而保存具有相同名称terminate
的方法的不同实现。 重要的一点-读取类的主体并创建attrs
对象时,解释器将所谓的unbound
函数放在此处,这些函数尚不知道将在哪个类或实例上调用它们。 为了在函数调用期间定义上下文并传递self
或cls
作为第一个参数,或者如果调用staticmethod
传递任何东西,我们必须实现描述符协议 。
结果,我们将看到以下魔术:
class PolyMeta(type): @classmethod def __prepare__(mcs, name, bases): return PolyDict() class Terminator(metaclass=PolyMeta): ... t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'})
如果您知道__prepare__
方法有任何其他有趣的用途,请在评论中写下。
结论
元编程是我在Advanced Python中讨论的众多主题之一。 作为课程的一部分,我还将告诉您如何在大型Python项目的开发中有效地使用SOLID和GRASP原理,设计应用程序体系结构以及编写高性能和高质量的代码。 我将很高兴在Binary District的墙壁上见到您!