Estoy tratando de escribir una clase base abstracta A que tendrá un método abstracto run que se espera que el usuario / desarrollador sobrecargue. Me gustaría aplicar algún comportamiento 'after' para que se aplique automáticamente a la clase derivada B, de modo que después de que B.run() haya ejecutado su curso, se llamará a otro método estándar (en canalizaciones de datos esto podría, p. Ej. confirmar o deshacer transacciones). ¿Hay una manera de lograr esto?

Mi intento ingenuo fallido en esto es:

def do_the_other_thing(func): 
    def wrapper(): 
        func() 
        print('doing the other thing!')
    return wrapper 

class A: 
    @do_the_other_thing 
    def run(self): 
        pass     

class B(A): 
    def run(self): 
        print('running!') 

B().run() 
>>> 'running!' 
>>> #'doing the other thing!'     # <--- is there a way to get this?

Por supuesto, puedo implementar una solución creando un método abstracto diferente (por ejemplo, _run) que se llama desde un método no abstracto A.run, pero esto es menos elegante.

Puedo ver eso en 2007 PEP 3124 especificó exactamente esta funcionalidad pero no puedo encontrar ninguna referencia moderna a ella.

2
kd88 5 may. 2020 a las 17:48

2 respuestas

La mejor respuesta

En realidad, no puede hacer lo que quiere con un decorador de funciones solo si no espera que el usuario decore run ellos mismos . Puede usar decoradores de clase, __init_subclass__, o metaclasses.


Con una decoradora de clase,

class A:
    def run(self):
        pass

def do_the_other_thing(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print('doing the other thing!')
    return wrapper


def pipeline_thing(cls):
    cls.run = do_the_other_thing(cls.run)
    # do some other work
    return cls


@pipeline_thing
class B(A):

    def run(self):
        print("running!")

O con __init_subclass__

class A:
    def run(self):
        pass

    def __init_subclass__(cls):
        super().__init_subclass__()
        cls.run = do_the_other_thing(cls.run)
        # do some other work

class B(A):

    def run(self):
         print("running!")

O con metaclasses

class AMeta(type):

    def __init__(cls, name, bases, attrs, **kwargs):
        super().__init__(name, bases, attrs)
        cls.run = do_the_other_thing(cls.run)
        # do some other work

class A(metaclass=AMeta):
    def run(self):
        pass

class B(A):

    def run(self):
        print("running!")

Este ejemplo es excesivo para las metaclases (está utilizando metaclass.__init__, el método mágico menos poderoso en una metaclase y su comportamiento se puede hacer con __init_subclass__ (esto es el uso previsto de __init_subclass__). El uso de una metaclase de esta manera evitará que sus usuarios usen metaclases y complicará innecesariamente su código. Si necesita la tubería para hacer más magia, puede usarlos (por ejemplo, si necesita acceso a __new__).

Usaría __init_subclass__ o un decorador de clase (@pipe o algo así) que probablemente también mezcló B con A. Como alkasm mencionado, puede hacer que A herede de abc.ABC y decorar {{X6} } con abc.abstractmethod para garantizar que las subclases lo implementen.

2
modesitt 5 may. 2020 a las 15:25

No anule run; anular un método que run llama .

class A:
    def run(self):
        self.do_run()
        print('doing the other thing!')

    def do_run(self):
        pass


class B(A):
    def do_run(self):
        print('running!') 

Entonces

>>> B().run() 
running!
doing the other thing!
0
chepner 5 may. 2020 a las 15:13