Python: metaprogramação em produção. Parte dois

Continuamos a falar sobre metaprogramação em Python. Quando usado corretamente, permite implementar rápida e elegantemente padrões de design complexos. Na última parte deste artigo, mostramos como as metaclasses podem ser usadas para alterar os atributos de instâncias e classes.



Agora vamos ver como você pode alterar as chamadas de método. Você pode aprender mais sobre as opções de metaprogramação no curso Advanced Python .


Depuração e rastreamento de chamadas


Como você já entendeu, usando uma metaclasse, qualquer classe pode ser transformada além do reconhecimento. Por exemplo, substitua todos os métodos de classe por outros ou aplique um decorador arbitrário a cada método. Você pode usar essa ideia para depurar o desempenho do aplicativo.


A metaclasse a seguir mede o tempo de execução de cada método na classe e suas instâncias, bem como o tempo de criação da própria instância:


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('_'): #     attrs[attr] = timing(method) return super().__new__(mcs, name, bases, attrs) def __call__(cls, *args, **kwargs): with timing_context(f'{cls.__name__} instance creation'): #      return super().__call__(*args, **kwargs) 

Vejamos a depuração em ação:


 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() #   INFO:__main__:Operation "User instance creation" completed in 0.70 seconds INFO:__main__:Operation "login" completed in 1.00 seconds INFO:__main__:Operation "logout" completed in 2.00 seconds INFO:__main__:Operation "create" completed in 0.50 seconds 

Tente expandir o DebugMeta e DebugMeta a assinatura dos métodos e seu rastreamento de pilha.


O padrão solitário e a proibição de herança


E agora vamos para casos exóticos de uso de metaclasses em projetos Python.


Certamente muitos de vocês usam o módulo Python usual para implementar um padrão de design singleton (também conhecido como Singleton), porque é muito mais conveniente e mais rápido do que escrever a metaclasse apropriada. No entanto, vamos escrever uma de suas implementações em prol do interesse acadêmico:


 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') #         u2 = User('Stepan') >>> id(u1) == id(u2) True >>> u2 <User: Pavel> >>> User.instance <User: Pavel> #   , ? >>> u1.instance.instance.instance.instance <User: Pavel> 

Essa implementação tem uma nuance interessante - como o construtor de classe não é chamado pela segunda vez, você pode cometer um erro e não passar o parâmetro necessário lá, e nada acontecerá em tempo de execução se a instância já tiver sido criada. Por exemplo:


 >>> User('Roman') <User: Roman> >>> User('Alexey', 'Petrovich', 66) #     ! <User: Roman> #     User       #    TypeError! 

Agora, vamos dar uma olhada em uma opção ainda mais exótica: a proibição de herança de uma classe específica.


 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 # TypeError: Can't inherit B class from final A #    ! 

Parametrização de Metaclasse


Nos exemplos anteriores, usamos metaclasses para personalizar a criação de classes, mas você pode ir ainda mais longe e começar a parametrizar o comportamento das metaclasses.


Por exemplo, você pode passar uma função para o parâmetro metaclass ao declarar uma classe e retornar diferentes instâncias de metaclasses, dependendo de algumas condições, por exemplo:


 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 

Mas um exemplo mais interessante é o uso de parâmetros extra_kwargs ao declarar classes. Suponha que você queira usar a metaclasse para alterar o comportamento de certos métodos em uma classe, e cada classe pode ter nomes diferentes para esses métodos. O que fazer? E aqui está o que


 #   `DebugMeta`     class DebugMetaParametrized(type): def __new__(mcs, name, bases, attrs, **extra_kwargs): debug_methods = extra_kwargs.get('debug_methods', ()) for attr, value in attrs.items(): #      ,   #    `debug_methods`: if attr in debug_methods: attrs[attr] = timing(value) return super().__new__(mcs, name, bases, attrs) class User(metaclass=DebugMetaParametrized, debug_methods=('login', 'create')): ... user = User('Oleg') user.login() #  "logout"   . user.logout() user.create() 

Na minha opinião, ficou muito elegante! Você pode criar muitos padrões para usar essa parametrização, mas lembre-se da regra principal - tudo é bom com moderação.


__prepare__ métodos __prepare__


Por fim, falarei sobre o possível uso do método __prepare__ . Como mencionado acima, esse método deve retornar um objeto de dicionário, que o intérprete preenche no momento em que analisa o corpo da classe, por exemplo, se __prepare__ retorna o objeto d = dict() , ao ler a seguinte classe:


 class A: x = 12 y = 'abc' z = {1: 2} 

O intérprete executará as seguintes operações:


 d['x'] = 12 d['y'] = 'abc' d['z'] = {1: 2} 

Existem vários usos possíveis para esse recurso. Eles são de vários graus de utilidade, portanto:


  1. Nas versões do Python = <3.5, se precisássemos manter a ordem dos métodos de declaração em uma classe, poderíamos retornar collections.OrderedDict do método __prepare__ , nas versões mais antigas, os dicionários OrderedDict já preservam a ordem de adição de chaves, portanto, OrderedDict não OrderedDict mais necessário.
  2. O módulo de biblioteca padrão da enum usa um objeto do tipo dict personalizado para determinar quando um atributo de classe é duplicado na declaração. O código pode ser encontrado aqui .
  3. Não é um código pronto para produção, mas um exemplo muito bom é o suporte ao polimorfismo paramétrico .

Por exemplo, considere a seguinte classe com três implementações de um único método polimórfico:


 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'}) #  Terminating DICTIONARY 10 Terminating DICTIONARY Hello, world! Terminating DICTIONARY {'hello': 'world'} 

Obviamente, o último método terminate declarado substitui as implementações dos dois primeiros, e precisamos que o método seja selecionado dependendo do tipo de argumento passado. Para conseguir isso, programamos alguns objetos adicionais do wrapper:


 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): # ""  classmethod/staticmethod return getattr(impl, '__func__', impl) def __call__(self, arg): impl = self.implementations[type(arg)] callable_func = self._get_callable_func(impl) if isinstance(impl, staticmethod): return callable_func(arg) elif self.cls and isinstance(impl, classmethod): return callable_func(self.cls, arg) else: return callable_func(self.instance, arg) def add_implementation(self, func): callable_func = self._get_callable_func(func) #   ,     1  arg_name, arg_type = list(callable_func.__annotations__.items())[0] self.implementations[arg_type] = func 

O mais interessante no código acima é o objeto PolyMethod , que armazena um registro com implementações do mesmo método, dependendo do tipo de argumento passado para esse método. Retornaremos o objeto __prepare__ método __prepare__ e, assim, salvaremos implementações diferentes dos métodos com o mesmo nome. Um ponto importante - ao ler o corpo da classe e ao criar o objeto attrs , o intérprete coloca ali as chamadas funções não unbound , essas funções ainda não sabem em qual classe ou instância serão chamadas. Tivemos que implementar um protocolo descritor para definir o contexto durante a chamada da função e passar self ou cls como o primeiro parâmetro ou não passar nada se o staticmethod chamado.


Como resultado, veremos a seguinte mágica:


 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'}) #  Terminating INTEGER 10 Terminating STRING Hello, world! Terminating DICTIONARY {'hello': 'world'} >>> t1000.terminate <__main__.PolyMethod object at 0xdeadcafe> 

Se você conhece outros usos interessantes do método __prepare__ , escreva nos comentários.


Conclusão


A metaprogramação é um dos muitos tópicos sobre os quais falei no Advanced Python . Como parte do curso, também mostrarei como usar efetivamente os princípios do SOLID e GRASP no desenvolvimento de grandes projetos Python, projetar a arquitetura do aplicativo e escrever código de alto desempenho e alta qualidade. Ficarei feliz em vê-lo nas paredes do distrito binário!

Source: https://habr.com/ru/post/pt422415/


All Articles