poo - Cómo obtener todos los métodos de una clase de python con decorador determinado
poo python 3 (3)
¿Cómo obtener todos los métodos de una determinada clase A que están decorados con @ decorator2?
class A():
def method_a(self):
pass
@decorator1
def method_b(self, b):
pass
@decorator2
def method_c(self, t=5):
pass
Método 1: Decorador de registro básico
Ya respondí esta pregunta aquí: Llamar funciones por índice de matriz en Python =)
Método 2: análisis de código fuente
Si no tienes control sobre la definición de clase , que es una interpretación de lo que te gustaría suponer, esto es imposible (sin código-lectura-reflexión), ya que por ejemplo el decorador podría ser un decorador no operativo (como en mi ejemplo vinculado) que simplemente devuelve la función sin modificaciones. (Sin embargo, si se permite envolver / redefinir los decoradores, consulte el Método 3: Convertir los decoradores en "autoconscientes" , luego encontrará una solución elegante)
Es un hack terriblemente terrible, pero podrías usar el módulo de inspect
para leer el código fuente y analizarlo. Esto no funcionará en un intérprete interactivo, porque el módulo de inspección se negará a dar el código fuente en modo interactivo. Sin embargo, a continuación hay una prueba de concepto.
#!/usr/bin/python3
import inspect
def deco(func):
return func
def deco2():
def wrapper(func):
pass
return wrapper
class Test(object):
@deco
def method(self):
pass
@deco2()
def method2(self):
pass
def methodsWithDecorator(cls, decoratorName):
sourcelines = inspect.getsourcelines(cls)[0]
for i,line in enumerate(sourcelines):
line = line.strip()
if line.split(''('')[0].strip() == ''@''+decoratorName: # leaving a bit out
nextLine = sourcelines[i+1]
name = nextLine.split(''def'')[1].split(''('')[0].strip()
yield(name)
¡Funciona!:
>>> print(list( methodsWithDecorator(Test, ''deco'') ))
[''method'']
Tenga en cuenta que hay que prestar atención al análisis sintáctico y la sintaxis de Python, por ejemplo, @deco
y @deco(...
son resultados válidos, pero @deco2
no debería devolverse si simplemente solicitamos ''deco''
. Notamos que de acuerdo con el la sintaxis oficial de python en http://docs.python.org/reference/compound_stmts.html decoradores es la siguiente:
decorator ::= "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE
Respiramos con alivio por no tener que lidiar con casos como @(deco)
. Pero tenga en cuenta que esto aún no lo ayuda si realmente tiene decoradores realmente complicados, como @getDecorator(...)
, por ejemplo
def getDecorator():
return deco
Por lo tanto, esta estrategia de código de análisis que mejor se puede hacer no puede detectar casos como este. Aunque si está utilizando este método, lo que realmente getDecorator
es lo que está escrito sobre el método en la definición, que en este caso es getDecorator
.
De acuerdo con la especificación, también es válido tener @foo1.bar2.baz3(...)
como decorador. Puede extender este método para trabajar con eso. También podría ampliar este método para devolver un <function object ...>
lugar del nombre de la función, con mucho esfuerzo. Este método sin embargo es hackish y terrible.
Método 3: Convertir los decoradores en "autoconscientes"
Si no tiene control sobre la definición del decorador (que es otra interpretación de lo que le gustaría), todos estos problemas desaparecerán porque tiene control sobre cómo se aplica el decorador. Por lo tanto, puede modificar el decorador envolviéndolo , crear su propio decorador y usarlo para decorar sus funciones. Permítanme decirlo una vez más: puede hacer que un decorador decore el decorador sobre el que no tiene control, "iluminándolo", lo que en nuestro caso lo hace hacer lo que estaba haciendo antes, pero también .decorator
una propiedad de metadato .decorator
al que se puede .decorator
regresa, lo que le permite realizar un seguimiento de "¿Estaba decorada o no esta función? ¡Comprobemos function.decorator!". Y luego puede iterar sobre los métodos de la clase, y simplemente verifique si el decorador tiene la propiedad .decorator
apropiada. =) Como se demuestra aquí:
def makeRegisteringDecorator(foreignDecorator):
"""
Returns a copy of foreignDecorator, which is identical in every
way(*), except also appends a .decorator property to the callable it
spits out.
"""
def newDecorator(func):
# Call to newDecorator(method)
# Exactly like old decorator, but output keeps track of what decorated it
R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done
R.decorator = newDecorator # keep track of decorator
#R.original = func # might as well keep track of everything!
return R
newDecorator.__name__ = foreignDecorator.__name__
newDecorator.__doc__ = foreignDecorator.__doc__
# (*)We can be somewhat "hygienic", but newDecorator still isn''t signature-preserving, i.e. you will not be able to get a runtime list of parameters. For that, you need hackish libraries...but in this case, the only argument is func, so it''s not a big issue
return newDecorator
Demostración para @decorator
:
deco = makeRegisteringDecorator(deco)
class Test2(object):
@deco
def method(self):
pass
@deco2()
def method2(self):
pass
def methodsWithDecorator(cls, decorator):
"""
Returns all methods in CLS with DECORATOR as the
outermost decorator.
DECORATOR must be a "registering decorator"; one
can make any decorator "registering" via the
makeRegisteringDecorator function.
"""
for maybeDecorated in cls.__dict__.values():
if hasattr(maybeDecorated, ''decorator''):
if maybeDecorated.decorator == decorator:
print(maybeDecorated)
yield maybeDecorated
¡Funciona!:
>>> print(list( methodsWithDecorator(Test2, deco) ))
[<function method at 0x7d62f8>]
Sin embargo, un "decorador registrado" debe ser el decorador externo , de lo contrario la anotación del atributo .decorator
se perderá. Por ejemplo, en un tren de
@decoOutermost
@deco
@decoInnermost
def func(): ...
solo puede ver los metadatos que expone decoOutermost
, a menos que tengamos referencias a los contenedores "más internos".
nota al margen: el método anterior también puede crear un .decorator
que realiza un seguimiento de toda la pila de decoradores aplicados y funciones de entrada y argumentos decorador-fábrica . =) Por ejemplo, si considera la línea R.original = func
, es posible utilizar un método como este para realizar un seguimiento de todas las capas de envoltura. Personalmente, esto es lo que haría si escribiera una biblioteca decoradora, porque permite una profunda introspección.
También hay una diferencia entre @foo
y @bar(...)
. Si bien ambos son "expressons del decorador" tal como se definen en la especificación, tenga en cuenta que foo
es un decorador, mientras que la bar(...)
devuelve un decorador creado dinámicamente, que luego se aplica. Por lo tanto, necesitaría una función separada makeRegisteringDecoratorFactory
, que es algo así como makeRegisteringDecorator
pero incluso MORE META:
def makeRegisteringDecoratorFactory(foreignDecoratorFactory):
def newDecoratorFactory(*args, **kw):
oldGeneratedDecorator = foreignDecoratorFactory(*args, **kw)
def newGeneratedDecorator(func):
modifiedFunc = oldGeneratedDecorator(func)
modifiedFunc.decorator = newDecoratorFactory # keep track of decorator
return modifiedFunc
return newGeneratedDecorator
newDecoratorFactory.__name__ = foreignDecoratorFactory.__name__
newDecoratorFactory.__doc__ = foreignDecoratorFactory.__doc__
return newDecoratorFactory
Demostración para @decorator(...)
:
def deco2():
def simpleDeco(func):
return func
return simpleDeco
deco2 = makeRegisteringDecoratorFactory(deco2)
print(deco2.__name__)
# RESULT: ''deco2''
@deco2()
def f():
pass
Este envoltorio de fábrica también funciona:
>>> print(f.decorator)
<function deco2 at 0x6a6408>
bonus Incluso intentemos lo siguiente con el Método n. ° 3:
def getDecorator(): # let''s do some dispatching!
return deco
class Test3(object):
@getDecorator()
def method(self):
pass
@deco2()
def method2(self):
pass
Resultado:
>>> print(list( methodsWithDecorator(Test3, deco) ))
[<function method at 0x7d62f8>]
Como puede ver, a diferencia de method2, @deco se reconoce correctamente aunque nunca se haya escrito explícitamente en la clase. A diferencia del método2, esto también funcionará si el método se agrega en tiempo de ejecución (manualmente, a través de una metaclase, etc.) o se hereda.
Tenga en cuenta que también puede decorar una clase, de modo que si "ilumina" un decorador que se utiliza para decorar métodos y clases, y luego escribe una clase dentro del cuerpo de la clase que desea analizar , entonces los methodsWithDecorator
devolverán las clases decoradas así como también métodos decorados. Se podría considerar esto una característica, pero se puede escribir fácilmente la lógica para ignorarlos examinando el argumento al decorador, es decir, .original
, para lograr la semántica deseada.
Para ampliar la excelente respuesta de @ ninjagecko en el Método 2: Análisis de código fuente, puede usar el módulo ast
presentado en Python 2.6 para realizar la autoinspección siempre que el módulo de inspección tenga acceso al código fuente.
def findDecorators(target):
import ast, inspect
res = {}
def visit_FunctionDef(node):
res[node.name] = [ast.dump(e) for e in node.decorator_list]
V = ast.NodeVisitor()
V.visit_FunctionDef = visit_FunctionDef
V.visit(compile(inspect.getsource(target), ''?'', ''exec'', ast.PyCF_ONLY_AST))
return res
Agregué un método decorado un poco más complicado:
@x.y.decorator2
def method_d(self, t=5): pass
Resultados:
> findDecorators(A)
{''method_a'': [],
''method_b'': ["Name(id=''decorator1'', ctx=Load())"],
''method_c'': ["Name(id=''decorator2'', ctx=Load())"],
''method_d'': ["Attribute(value=Attribute(value=Name(id=''x'', ctx=Load()), attr=''y'', ctx=Load()), attr=''decorator2'', ctx=Load())"]}
Tal vez, si los decoradores no son demasiado complejos (pero no sé si hay una forma menos hacky).
def decorator1(f):
def new_f():
print "Entering decorator1", f.__name__
f()
new_f.__name__ = f.__name__
return new_f
def decorator2(f):
def new_f():
print "Entering decorator2", f.__name__
f()
new_f.__name__ = f.__name__
return new_f
class A():
def method_a(self):
pass
@decorator1
def method_b(self, b):
pass
@decorator2
def method_c(self, t=5):
pass
print A.method_a.im_func.func_code.co_firstlineno
print A.method_b.im_func.func_code.co_firstlineno
print A.method_c.im_func.func_code.co_firstlineno