Clase con registro de métodos basados ​​en decoradores.

Tengo una clase que tiene varios métodos, cada uno con ciertas propiedades (en el sentido de calidad). Me gustaría que estos métodos estén disponibles en una lista dentro de la clase para que puedan ejecutarse a la vez. Tenga en cuenta que las propiedades pueden ser intercambiables, por lo que esto no se puede resolver utilizando otras clases que heredarían de la original. En un mundo ideal se vería algo así:

class MyClass: def __init__(): red_rules = set() blue_rules = set() hard_rules = set() soft_rules = set() @red def rule_one(self): return 1 @blue @hard def rule_two(self): return 2 @hard def rule_three(self): return 3 @blue @soft def rule_four(self): return 4 

Cuando se crea una instancia de la clase, debería ser fácil ejecutar todas las reglas rojas y suaves combinando los conjuntos y ejecutando los métodos. Sin embargo, los decoradores para esto son difíciles ya que un decorador de registro regular puede completar un objeto global pero no el atributo de clase:

 def red(fn): red_rules.add(fn) return fn 

¿Cómo hago para implementar algo como esto?

Puedes set subclases y darle un método decorador:

 class MySet(set): def register(self, method): self.add(method) return method class MyClass: red_rules = MySet() blue_rules = MySet() hard_rules = MySet() soft_rules = MySet() @red_rules.register def rule_one(self): return 1 @blue_rules.register @hard_rules.register def rule_two(self): return 2 @hard_rules.register def rule_three(self): return 3 @blue_rules.register @soft_rules.register def rule_four(self): return 4 

O si encuentra que el uso del método .register es feo, siempre puede definir el método __call__ para usar el conjunto como decorador:

 class MySet(set): def __call__(self, method): """Use set as a decorator to add elements to it.""" self.add(method) return method class MyClass: red_rules = MySet() ... @red_rules def rule_one(self): return 1 ... 

Esto se ve mejor, pero es menos explícito, por lo que para otros colaboradores (o para el futuro) podría ser más difícil comprender lo que está sucediendo aquí.


Para llamar a las funciones almacenadas, puede pasar el conjunto que desee y pasar la instancia como argumento self :

 my_instance = MyClass() for rule in MyClass.red_rules: rule(my_instance) 

También puede crear una función de utilidad para hacer esto por usted, por ejemplo, puede crear un método MySet.invoke() :

 class MySet(set): ... def invoke(self, obj): for rule in self: rule(obj) 

Y ahora solo llame:

 MyClass.red_rules.invoke(my_instance) 

O podrías hacer que MyClass maneje esto en su lugar:

 class MyClass: ... def invoke_rules(self, rules): for rule in rules: rule(self) 

Y luego llame a esto en una instancia de MyClass :

 my_instance.invoke_rules(MyClass.red_rules) 

Los decoradores se aplican cuando se define la función ; en una clase que es cuando se define la clase. En este momento no hay instancias todavía!

Tienes tres opciones:

  1. Registre sus decoradores a nivel de clase. Esto no es tan limpio como puede sonar; tienes que pasar explícitamente objetos adicionales a tus decoradores ( red_rules = set() , luego @red(red_rules) para que la fábrica de decoradores pueda agregar la función a la ubicación correcta), o tienes que usar algún tipo de inicializador de clase para recoger funciones especialmente marcadas; puede hacer esto con una clase base que define el método de clase __init_subclass__ , en cuyo punto puede iterar sobre el espacio de nombres y encontrar esos marcadores (atributos establecidos por los decoradores).

  2. Haga que su método __init__ (o un método __new__ ) __new__ todos los métodos de la clase y busque los atributos especiales que los decoradores han puesto allí.

    El decorador solo tendría que agregar un _rule_name o un atributo similar a los métodos decorados, y {getattr(self, name) for for name in dir(self) if getattr(getattr(self, name), '_rule_name', None) == rule_name} recogería cualquier método que tenga el nombre de regla correcto definido en rule_name .

  3. Haz que tus decoradores produzcan nuevos objetos descriptores ; los descriptores tienen su __set_name__() llamado cuando se crea el objeto de clase. Esto le da acceso a la clase y, por lo tanto, puede agregar atributos a esa clase.

Tenga en cuenta que __init_subclass__ y __set_name__ requieren Python 3.6 o más reciente; tendría que recurrir a una metaclase para lograr una funcionalidad similar en versiones anteriores.

También tenga en cuenta que cuando registra funciones en el nivel de clase, debe vincularlas explícitamente con la function.__get__(self, type(cls)) para convertirlas en métodos, o puede pasar explícitamente en self cuando las llame. Puede automatizar esto haciendo una clase dedicada para mantener los conjuntos de reglas, y hacer de esta clase un descriptor también:

 import types from collections.abc import MutableSet class RulesSet(MutableSet): def __init__(self, values=(), rules=None, instance=None, owner=None): self._rules = rules or set() # can be a shared set! self._instance = instance self._owner = owner self |= values def __repr__(self): bound = '' if self._owner is not None: bound = f', instance={self._instance!r}, owner={self._owner!r}' rules = ', '.join([repr(v) for v in iter(self)]) return f'{type(self).__name__}({{{rules}}}{bound})' def __contains__(self, ob): try: if ob.__self__ is self._instance or ob.__self__ is self._owner: # test for the unbound function instead when both are bound, this requires staticmethod and classmethod to be unwrapped! ob = ob.__func__ return any(ob is getattr(f, '__func__', f) for f in self._rules) except AttributeError: # not a method-like object pass return ob in self._rules def __iter__(self): if self._owner is not None: return (f.__get__(self._instance, self._owner) for f in self._rules) return iter(self._rules) def __len__(self): return len(self._rules) def add(self, ob): while isinstance(ob, Rule): # remove any rule wrappers ob = ob._function assert isinstance(ob, (types.FunctionType, classmethod, staticmethod)) self._rules.add(ob) def discard(self, ob): self._rules.discard(ob) def __get__(self, instance, owner): # share the set with a new, bound instance. return type(self)(rules=self._rules, instance=instance, owner=owner) class Rule: @classmethod def make_decorator(cls, rulename): ruleset_name = f'{rulename}_rules' def decorator(f): return cls(f, ruleset_name) decorator.__name__ = rulename return decorator def __init__(self, function, ruleset_name): self._function = function self._ruleset_name = ruleset_name def __get__(self, *args): # this is mostly here just to make Python call __set_name__ return self._function.__get__(*args) def __set_name__(self, owner, name): # register, then replace the name with the original function # to avoid being a performance bottleneck ruleset = getattr(owner, self._ruleset_name, None) if ruleset is None: ruleset = RulesSet() setattr(owner, self._ruleset_name, ruleset) ruleset.add(self) # transfer controrol to any further rule objects if isinstance(self._function, Rule): self._function.__set_name__(owner, name) else: setattr(owner, name, self._function) red = Rule.make_decorator('red') blue = Rule.make_decorator('blue') hard = Rule.make_decorator('hard') soft = Rule.make_decorator('soft') 

Luego solo usa:

 class MyClass: @red def rule_one(self): return 1 @blue @hard def rule_two(self): return 2 @hard def rule_three(self): return 3 @blue @soft def rule_four(self): return 4 

y puede acceder a self.red_rules , etc. como un conjunto con métodos enlazados:

 >>> inst = MyClass() >>> inst.red_rules RulesSet({>}, instance=<__main__.MyClass object at 0x106fe7550>, owner=) >>> inst.blue_rules RulesSet({>, >}, instance=<__main__.MyClass object at 0x106fe7550>, owner=) >>> inst.hard_rules RulesSet({>, >}, instance=<__main__.MyClass object at 0x106fe7550>, owner=) >>> inst.soft_rules RulesSet({>}, instance=<__main__.MyClass object at 0x106fe7550>, owner=) >>> for rule in inst.hard_rules: ... rule() ... 2 3 

Las mismas reglas son accesibles en la clase; Las funciones normales permanecen sin unir:

 >>> MyClass.blue_rules RulesSet({, }, instance=None, owner=) >>> next(iter(MyClass.blue_rules))  

Las pruebas de contención funcionan como se espera:

 >>> inst.rule_two in inst.hard_rules True >>> inst.rule_two in inst.soft_rules False >>> MyClass.rule_two in MyClass.hard_rules True >>> MyClass.rule_two in inst.hard_rules True 

También puede usar estas reglas para registrar objetos de staticmethod classmethod y staticmethod :

 >>> class Foo: ... @hard ... @classmethod ... def rule_class(cls): ... return f'rule_class of {cls!r}' ... >>> Foo.hard_rules RulesSet({>}, instance=None, owner=) >>> next(iter(Foo.hard_rules))() "rule_class of " >>> Foo.rule_class in Foo.hard_rules True