En Python, ¿cómo indico que estoy anulando un método?

173

En Java, por ejemplo, la @Overrideanotación no solo proporciona la verificación en tiempo de compilación de una anulación, sino que es un excelente código de autodocumentación.

Solo estoy buscando documentación (aunque si es un indicador para algún corrector como pylint, es una ventaja). Puedo agregar un comentario o una cadena de documentos en alguna parte, pero ¿cuál es la forma idiomática de indicar una anulación en Python?

Bluu
fuente
13
En otras palabras, ¿nunca indicas que estás anulando un método? ¿Dejar que el lector se dé cuenta por sí mismo?
Bluu
2
Sí, sé que parece una situación propensa a errores proveniente de un lenguaje compilado, pero solo tienes que aceptarlo. En la práctica, no he encontrado que sea un gran problema (Ruby en mi caso, no Python, sino la misma idea)
Ed S.
Claro, listo. Tanto la respuesta de Triptych como la de mkorpela son simples, me gusta, pero el espíritu explícito sobre-implícito de este último y la prevención inteligible de errores gana.
Bluu
1
No es directamente lo mismo, pero las clases base abstractas verifican si todos los métodos abstractos han sido anulados por una subclase. Por supuesto, esto no ayuda si anula métodos concretos.
letmaik

Respuestas:

208

Basado en esto y en la respuesta de fwc: s, creé un paquete pip instalable https://github.com/mkorpela/overrides

De vez en cuando termino aquí mirando esta pregunta. Principalmente esto sucede después de (nuevamente) ver el mismo error en nuestra base de código: alguien ha olvidado alguna clase de implementación de "interfaz" al cambiar el nombre de un método en la "interfaz".

Bueno, Python no es Java, pero Python tiene poder, y explícito es mejor que implícito, y hay casos concretos reales en el mundo real en los que esto me habría ayudado.

Así que aquí hay un boceto de decoraciones de anulaciones. Esto verificará que la clase dada como parámetro tenga el mismo nombre de método (o algo) que el método que se está decorando.

Si puede pensar en una mejor solución, ¡publíquela aquí!

def overrides(interface_class):
    def overrider(method):
        assert(method.__name__ in dir(interface_class))
        return method
    return overrider

Funciona de la siguiente manera:

class MySuperInterface(object):
    def my_method(self):
        print 'hello world!'


class ConcreteImplementer(MySuperInterface):
    @overrides(MySuperInterface)
    def my_method(self):
        print 'hello kitty!'

y si haces una versión defectuosa, generará un error de aserción durante la carga de la clase:

class ConcreteFaultyImplementer(MySuperInterface):
    @overrides(MySuperInterface)
    def your_method(self):
        print 'bye bye!'

>> AssertionError!!!!!!!
mkorpela
fuente
18
Increíble. Esto detectó un error de ortografía la primera vez que lo probé. Prestigio.
Christopher Bruns el
77
mfbutner: no se llama cada vez que se ejecuta el método, solo cuando se crea el método.
mkorpela
3
¡Esto también es bueno para las cadenas de documentos! overridespodría copiar la cadena de documentación del método anulado si el método de anulación no tiene uno propio.
letmaik
55
@mkorpela, Heh, este código tuyo debería estar en el sistema lib predeterminado de Python. ¿Por qué no pones esto en el sistema pip? : P
55
@mkorpela: Ah, y sugiero informar a los desarrolladores de Python Core acerca de este paquete, es posible que quieran considerar agregar un decorador overide al sistema Python Core. :)
30

Aquí hay una implementación que no requiere la especificación del nombre de interface_class.

import inspect
import re

def overrides(method):
    # actually can't do this because a method is really just a function while inside a class def'n  
    #assert(inspect.ismethod(method))

    stack = inspect.stack()
    base_classes = re.search(r'class.+\((.+)\)\s*\:', stack[2][4][0]).group(1)

    # handle multiple inheritance
    base_classes = [s.strip() for s in base_classes.split(',')]
    if not base_classes:
        raise ValueError('overrides decorator: unable to determine base class') 

    # stack[0]=overrides, stack[1]=inside class def'n, stack[2]=outside class def'n
    derived_class_locals = stack[2][0].f_locals

    # replace each class name in base_classes with the actual class type
    for i, base_class in enumerate(base_classes):

        if '.' not in base_class:
            base_classes[i] = derived_class_locals[base_class]

        else:
            components = base_class.split('.')

            # obj is either a module or a class
            obj = derived_class_locals[components[0]]

            for c in components[1:]:
                assert(inspect.ismodule(obj) or inspect.isclass(obj))
                obj = getattr(obj, c)

            base_classes[i] = obj


    assert( any( hasattr(cls, method.__name__) for cls in base_classes ) )
    return method
fwc
fuente
2
Un poco mágico pero hace que el uso típico sea mucho más fácil. ¿Puedes incluir ejemplos de uso?
Bluu
¿Cuáles son los costos promedio y en el peor de los casos de usar este decorador, tal vez expresado como una comparación con un decorador incorporado como @classmethod o @property?
larham1
44
@ larham1 Este decorador se ejecuta una vez, cuando se analiza la definición de clase, no en cada llamada. Por lo tanto, su costo de ejecución es irrelevante, en comparación con el tiempo de ejecución del programa.
Abgan
Esto será mucho mejor en Python 3.6 gracias a PEP 487 .
Neil G
Para obtener un mejor mensaje de error: afirme any (hasattr (cls, method .__ name__) para cls en base_classes), 'El método Overriden "{}" no se encontró en la clase base.'. Format (method .__ name__)
Ivan Kovtun
14

Si desea esto solo con fines de documentación, puede definir su propio decorador de anulación:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

Esto no es más que un placer visual, a menos que cree una anulación (f) de tal manera que realmente se compruebe si hay una anulación.

Pero entonces, esto es Python, ¿por qué escribirlo como si fuera Java?

Ber
fuente
2
Se podría agregar validación real mediante inspección al overridedecorador.
Erik Kaplun
70
Pero entonces, esto es Python, ¿por qué escribirlo como si fuera Java? ¿Porque algunas ideas en Java son buenas y vale la pena extenderlas a otros idiomas?
Piotr Dobrogost
9
Porque cuando cambias el nombre de un método en una superclase, sería bueno saber que algunos niveles de subclase 2 hacia abajo lo anulaban. Claro, es fácil de verificar, pero un poco de ayuda del analizador de idiomas no estaría de más.
Abgan
44
Porque es una buena idea. El hecho de que una variedad de otros idiomas tengan la característica no es un argumento, ni a favor ni en contra.
sfkleach
6

Python no es Java. Por supuesto, no existe tal cosa como la verificación en tiempo de compilación.

Creo que un comentario en la cadena de documentación es suficiente. Esto permite que cualquier usuario de su método escriba help(obj.method)y vea que el método es una anulación.

También puede ampliar explícitamente una interfaz con class Foo(Interface), lo que permitirá a los usuarios escribir help(Interface.method)para tener una idea sobre la funcionalidad que su método está destinado a proporcionar.

Tríptico
fuente
57
El punto real de @OverrideJava no es documentar: es detectar un error cuando pretendía anular un método, pero terminó definiendo uno nuevo (por ejemplo, porque escribió mal un nombre; en Java, también puede suceder porque usó la firma incorrecta, pero esto no es un problema en Python, pero el error de ortografía todavía lo es).
Pavel Minaev
2
@ Pavel Minaev: Cierto, pero aún es conveniente tener documentación, especialmente si está utilizando un editor IDE / texto que no tiene indicadores automáticos para anulaciones (JDT de Eclipse los muestra claramente junto a los números de línea, por ejemplo).
Tuukka Mustonen
2
@PavelMinaev Incorrecto. Uno de los puntos principales de la @Overridedocumentación es además de la verificación del tiempo de compilación.
siamii
66
@siamii Creo que una ayuda para la documentación es excelente, pero en toda la documentación oficial de Java que veo, solo indican la importancia de las comprobaciones de tiempo de compilación. Justifique su afirmación de que Pavel está "equivocado".
Andrew Mellinger
5

Improvisando en @mkorpela gran respuesta , aquí hay una versión con

comprobaciones, nombres y objetos de error más precisos

def overrides(interface_class):
    """
    Function override annotation.
    Corollary to @abc.abstractmethod where the override is not of an
    abstractmethod.
    Modified from answer https://stackoverflow.com/a/8313042/471376
    """
    def confirm_override(method):
        if method.__name__ not in dir(interface_class):
            raise NotImplementedError('function "%s" is an @override but that'
                                      ' function is not implemented in base'
                                      ' class %s'
                                      % (method.__name__,
                                         interface_class)
                                      )

        def func():
            pass

        attr = getattr(interface_class, method.__name__)
        if type(attr) is not type(func):
            raise NotImplementedError('function "%s" is an @override'
                                      ' but that is implemented as type %s'
                                      ' in base class %s, expected implemented'
                                      ' type %s'
                                      % (method.__name__,
                                         type(attr),
                                         interface_class,
                                         type(func))
                                      )
        return method
    return confirm_override


Así es como se ve en la práctica:

NotImplementedError" no implementado en la clase base "

class A(object):
    # ERROR: `a` is not a implemented!
    pass

class B(A):
    @overrides(A)
    def a(self):
        pass

da como resultado un NotImplementedErrorerror más descriptivo

function "a" is an @override but that function is not implemented in base class <class '__main__.A'>

completa pila

Traceback (most recent call last):
  
  File "C:/Users/user1/project.py", line 135, in <module>
    class B(A):
  File "C:/Users/user1/project.py", line 136, in B
    @overrides(A)
  File "C:/Users/user1/project.py", line 110, in confirm_override
    interface_class)
NotImplementedError: function "a" is an @override but that function is not implemented in base class <class '__main__.A'>


NotImplementedError" tipo implementado esperado "

class A(object):
    # ERROR: `a` is not a function!
    a = ''

class B(A):
    @overrides(A)
    def a(self):
        pass

da como resultado un NotImplementedErrorerror más descriptivo

function "a" is an @override but that is implemented as type <class 'str'> in base class <class '__main__.A'>, expected implemented type <class 'function'>

completa pila

Traceback (most recent call last):
  
  File "C:/Users/user1/project.py", line 135, in <module>
    class B(A):
  File "C:/Users/user1/project.py", line 136, in B
    @overrides(A)
  File "C:/Users/user1/project.py", line 125, in confirm_override
    type(func))
NotImplementedError: function "a" is an @override but that is implemented as type <class 'str'> in base class <class '__main__.A'>, expected implemented type <class 'function'>




Lo mejor de la respuesta de @mkorpela es que la verificación ocurre durante alguna fase de inicialización. La verificación no necesita ser "ejecutada". Refiriéndose a los ejemplos anteriores, class Bnunca se inicializa ( B()), sin embargo NotImplementedError, aún se elevará. Esto significa que los overrideserrores se detectan antes.

JamesThomasMoon1979
fuente
¡Oye! Esto se ve interesante. ¿Podría considerar hacer una solicitud de extracción en mi proyecto ipromise? He agregado una respuesta.
Neil G
@NeilG Bifurqué el proyecto ipromise y lo codifiqué un poco. Parece que esencialmente has implementado esto dentro overrides.py. No estoy seguro de qué más puedo mejorar significativamente, excepto para cambiar los tipos de excepción de TypeErrora NotImplementedError.
JamesThomasMoon1979
¡Oye! Gracias, no tengo comprobaciones de que el objeto reemplazado realmente tenga tipo types.MethodType. Esa fue una buena idea en tu respuesta.
Neil G
2

Como otros han dicho, a diferencia de Java, no hay una etiqueta @Overide, sin embargo, anteriormente puede crear sus propios decoradores, sin embargo, sugeriría usar el método global getattrib () en lugar de usar el dict interno para obtener algo como lo siguiente:

def Override(superClass):
    def method(func)
        getattr(superClass,method.__name__)
    return method

Si quisieras, podrías atrapar getattr () en tu propio intento, atrapar, generar tu propio error, pero creo que el método getattr es mejor en este caso.

Además, esto captura todos los elementos vinculados a una clase, incluidos los métodos de clase y las variables

Bicker x 2
fuente
2

Basado en la gran respuesta de @ mkorpela, he escrito un paquete similar ( ipromise pypi github ) que hace muchas más comprobaciones:

Supongamos que Ahereda de By C, Bhereda deC .

El módulo ipromise verifica que:

  • Si se A.fanula B.f, B.fdebe existir y Adebe heredar de B. (Este es el cheque del paquete de anulaciones).

  • No tiene el patrón A.fque declara que anula B.f, que luego declara que anula C.f. Adebería decir que se anula, C.fya que Bpodría decidir dejar de anular este método, y eso no debería dar lugar a actualizaciones posteriores.

  • No tiene el patrón A.fque declara que anula C.f, pero B.fno declara su anulación.

  • No tiene el patrón A.fque declara que anula C.f, pero B.fdeclara que anula a algunos D.f.

También tiene varias características para marcar y verificar la implementación de un método abstracto.

Neil G
fuente
0

Escuchar es más simple y funciona bajo Jython con clases Java:

class MyClass(SomeJavaClass):
     def __init__(self):
         setattr(self, "name_of_method_to_override", __method_override__)

     def __method_override__(self, some_args):
         some_thing_to_do()
usuario3034016
fuente
0

El decorador que hice no solo verificó si el nombre del atributo de anulación en es una superclase de la clase en la que se encuentra el atributo sin tener que especificar una superclase, este decorador también verifica para asegurarse de que el atributo de anulación debe ser del mismo tipo que el anulado atributo. Los métodos de clase se tratan como métodos y los métodos estáticos se tratan como funciones. Este decorador funciona para invocables, métodos de clase, métodos estáticos y propiedades.

Para el código fuente, consulte: https://github.com/fireuser909/override

Este decorador solo funciona para clases que son instancias de override. OverridesMeta pero si su clase es una instancia de una metaclase personalizada, use la función create_custom_overrides_meta para crear una metaclase que sea compatible con el decorador de reemplazo. Para las pruebas, ejecute el módulo de anulación .__ init__.

Miguel
fuente
0

En Python 2.6+ y Python 3.2+ puedes hacerlo (en realidad, simularlo , Python no admite la sobrecarga de funciones y la clase secundaria anula automáticamente el método de los padres). Podemos usar decoradores para esto. Pero primero, tenga en cuenta que Python @decoratorsy Java@Annotations son cosas totalmente diferentes. El anterior es un contenedor con código concreto, mientras que el otro es un indicador para el compilador.

Para esto, primero haz pip install multipledispatch

from multipledispatch import dispatch as Override
# using alias 'Override' just to give you some feel :)

class A:
    def foo(self):
        print('foo in A')

    # More methods here


class B(A):
    @Override()
    def foo(self):
        print('foo in B')
    
    @Override(int)
    def foo(self,a):
        print('foo in B; arg =',a)
        
    @Override(str,float)
    def foo(self,a,b):
        print('foo in B; arg =',(a,b))
        
a=A()
b=B()
a.foo()
b.foo()
b.foo(4)
b.foo('Wheee',3.14)

salida:

foo in A
foo in B
foo in B; arg = 4
foo in B; arg = ('Wheee', 3.14)

Tenga en cuenta que debe usar el decorador aquí con paréntesis

Una cosa para recordar es que, dado que Python no tiene sobrecarga de funciones directamente, por lo que incluso si la Clase B no hereda de la Clase A, pero necesita todas esas foo mensajes, también debe usar @Override (aunque se usará el alias 'Sobrecarga' mejor en ese caso)

mradul
fuente