No Python, como indico que estou substituindo um método?

173

Em Java, por exemplo, a @Overrideanotação não apenas fornece a verificação em tempo de compilação de uma substituição, mas também oferece um excelente código de auto-documentação.

Estou apenas procurando por documentação (embora seja um indicador para algum verificador como o pylint, isso é um bônus). Posso adicionar um comentário ou uma documentação em algum lugar, mas qual é a maneira idiomática de indicar uma substituição no Python?

Bluu
fonte
13
Em outras palavras, você nunca indica que está substituindo um método? Deixa para o leitor descobrir isso sozinho?
Bluu
2
Sim, eu sei que parece uma situação propensa a erros vindo de uma linguagem compilada, mas você só precisa aceitá-la. Na prática, eu não tê-lo encontrado para ser um grande problema (Ruby no meu caso, não Python, mas mesma idéia)
Ed S.
Claro, pronto. Tanto a resposta de Triptych quanto a de mkorpela são simples, eu gosto disso, mas o espírito explícito sobre implícito deste último e inteligentemente impedindo erros vencem.
Bluu
1
Não é diretamente o mesmo, mas as classes base abstratas verificam se todos os métodos abstratos foram substituídos por uma subclasse. Claro que isso não ajuda se você estiver substituindo métodos concretos.
Letmaik

Respostas:

208

Com base nisso e na resposta do fwc: s, criei um pacote instalável pip https://github.com/mkorpela/overrides

De tempos em tempos, acabo aqui olhando para essa pergunta. Principalmente isso ocorre depois (novamente) da mesma falha em nossa base de código: alguém esqueceu uma classe de implementação de "interface" enquanto renomeia um método na "interface".

Bem, o Python não é Java, mas o Python tem poder - e explícito é melhor do que implícito - e há casos concretos reais no mundo real em que isso poderia me ajudar.

Então, aqui está um esboço do decorador de substituições. Isso verificará se a classe fornecida como parâmetro tem o mesmo nome de método (ou algo assim) que o método que está sendo decorado.

Se você pode pensar em uma solução melhor, por favor poste aqui!

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

Funciona da seguinte maneira:

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


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

e se você fizer uma versão defeituosa, ocorrerá um erro de asserção durante o carregamento da classe:

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

>> AssertionError!!!!!!!
mkorpela
fonte
18
Impressionante. Isso pegou um erro de ortografia incorreta na primeira vez que tentei. Parabéns.
Christopher Bruns
7
mfbutner: não é chamado toda vez que o método é executado - somente quando o método é criado.
Mkorpela
3
Isso também é bom para strings de documentos! overridespoderia copiar a docstring do método substituído se o método de substituição não tiver um próprio.
Letmaik
5
@mkorpela, Heh, esse código deve estar no sistema lib padrão do python. Por que você não coloca isso no sistema pip? : P
5
@mkorpela: Ah, e sugiro informar os desenvolvedores principais do python sobre este pacote, eles podem querer considerar adicionar o decorador de substituição ao sistema python principal. :)
30

Aqui está uma implementação que não requer especificação do nome 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
fonte
2
Um pouco mágico, mas facilita muito o uso típico. Você pode incluir exemplos de uso?
Bluu
Quais são os custos médios e de pior caso do uso desse decorador, talvez expressos como uma comparação com um decorador incorporado como @classmethod ou @property?
larham1
4
@ larham1 Este decorador é executado uma vez, quando a definição de classe é analisada, não em cada chamada. Portanto, o custo de execução é irrelevante quando comparado ao tempo de execução do programa.
quer
Isso será muito melhor no Python 3.6, graças ao PEP 487 .
19416 Neil G
Para obter uma melhor mensagem de erro: afirme qualquer (hasattr (cls, método .__ name__) para cls em base_classes), 'O método Overriden "{}" não foi encontrado na classe base.'. Format (method .__ name__)
Ivan Kovtun
14

Se você desejar isso apenas para fins de documentação, poderá definir seu próprio decorador de substituição:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

Na verdade, isso não passa de um colírio para os olhos, a menos que você crie a substituição (f) de uma maneira que realmente verifique a substituição.

Mas então, este é Python, por que escrevê-lo como se fosse Java?

Ber
fonte
2
Pode-se adicionar validação real via inspeção ao overridedecorador.
Erik Kaplun 29/07
70
Mas então, este é Python, por que escrevê-lo como se fosse Java? Como algumas idéias em Java são boas e vale a pena estender para outras linguagens?
Piotr Dobrogost
9
Porque quando você renomeia um método em uma superclasse, seria bom saber que alguns níveis da subclasse 2 estavam substituindo-o. Claro, é fácil verificar, mas uma pequena ajuda do analisador de idiomas não faria mal.
Abgan
4
Porque é uma boa ideia. O fato de várias outras línguas terem esse recurso não é um argumento - a favor ou contra.
sfkleach
6

Python não é Java. Obviamente, não existe algo como verificação em tempo de compilação.

Eu acho que um comentário na doutrina é suficiente. Isso permite que qualquer usuário do seu método digite help(obj.method)e veja que o método é uma substituição.

Você também pode estender explicitamente uma interface com class Foo(Interface), o que permitirá aos usuários digitar help(Interface.method)para ter uma idéia da funcionalidade que seu método se destina a fornecer.

Tríptico
fonte
57
O objetivo real de @OverrideJava não é documentar - é cometer um erro quando você pretende substituir um método, mas acabou definindo um novo (por exemplo, porque você digitou um nome incorretamente; em Java, isso também pode acontecer porque você usou a assinatura errada, mas isso não é um problema no Python - mas o erro de ortografia ainda é).
Pavel Minaev 22/07/2009
2
@ Pavel Minaev: Verdade, mas ainda é conveniente ter para documentação, especialmente se você estiver usando um editor de IDE / texto que não possui indicadores automáticos para substituições (o JDT do Eclipse os mostra perfeitamente ao lado dos números das linhas, por exemplo).
Tuukka Mustonen
2
@PavelMinaev Wrong. Um dos principais pontos de @Overrideé a documentação, além da verificação do tempo de compilação.
siamii
6
@siamii Eu acho que uma ajuda para a documentação é ótima, mas em toda a documentação oficial do Java, eles indicam apenas a importância das verificações do tempo de compilação. Por favor, fundamentar sua alegação de que Pavel está "errado".
Andrew Mellinger
5

Improvisando a ótima resposta do @mkorpela , aqui está uma versão com

verificações, nomes e objetos de erro mais 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


Aqui está o que parece na prática:

NotImplementedError" não implementado na classe base "

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

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

resulta em NotImplementedErrorerro mais descritivo

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

pilha completa

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

resulta em NotImplementedErrorerro mais descritivo

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

pilha completa

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'>




A grande coisa sobre a resposta @mkorpela é que a verificação acontece durante alguma fase de inicialização. A verificação não precisa ser "executada". Referindo-se aos exemplos anteriores, class Bnunca é inicializado ( B()), ainda NotImplementedErrorassim o valor aumentará. Isso significa que os overrideserros são detectados antes.

JamesThomasMoon1979
fonte
Ei! Isso parece interessante. Você poderia considerar fazer uma solicitação pull no meu projeto ipromise? Eu adicionei uma resposta.
21719 Neil G
@ NeilG Bifurquei o projeto ipromise e codifiquei um pouco. Parece que você basicamente implementou isso dentro overrides.py. Não tenho certeza do que mais posso melhorar significativamente, exceto para alterar os tipos de exceção de TypeErrorpara NotImplementedError.
JamesThomasMoon1979 15/03/19
Ei! Obrigado, não tenho verificações de que o objeto substituído realmente tenha um tipo types.MethodType. Essa foi uma boa ideia na sua resposta.
31519 Neil G
2

Como outros já disseram, diferentemente do Java, não existe a tag @Overide, mas acima você pode criar seus próprios decoradores. No entanto, sugiro usar o método global getattrib () em vez de usar o dict interno para obter algo como o seguinte:

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

Se você quiser, você pode pegar o getattr () em sua própria tentativa, tente aumentar o seu próprio erro, mas acho que o método getattr é melhor nesse caso.

Isso também captura todos os itens vinculados a uma classe, incluindo métodos de classe e variáveis

Bicker x 2
fonte
2

Baseado na ótima resposta do @ mkorpela, escrevi um pacote semelhante ( ipromise pypi github ) que faz muito mais verificações:

Suponha que Aherda Be C, Bherda C.

O módulo ipromise verifica se:

  • Se A.fsubstitui B.f, B.fdeve existir e Adeve herdar de B. (Esta é a verificação do pacote de substituições).

  • Você não tem o padrão A.fque declara que substitui B.f, que declara que substitui C.f. Adeve dizer que substitui a partir de C.fpois Bpode decidir parar de substituir esse método e isso não deve resultar em atualizações posteriores.

  • Você não tem o padrão A.fdeclara que ele substitui C.f, mas B.fnão declara sua substituição.

  • Você não tem o padrão A.fque declara que substitui C.f, mas B.fdeclara que substitui de algumas D.f.

Ele também possui vários recursos para marcar e verificar a implementação de um método abstrato.

Neil G
fonte
0

O Hear é mais simples e funciona sob o Jython com classes 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()
user3034016
fonte
0

O decorador que fiz não apenas verificou se o nome do atributo de substituição é qualquer superclasse da classe em que o atributo está sem precisar especificar uma superclasse, mas também decorou também para garantir que o atributo de substituição seja do mesmo tipo que o de substituição atributo. Os métodos de classe são tratados como métodos e os métodos estáticos são tratados como funções. Este decorador trabalha para chamadas, métodos de classe, métodos estáticos e propriedades.

Para obter o código-fonte, consulte: https://github.com/fireuser909/override

Esse decorador funciona apenas para classes que são instâncias de substituição.OverridesMeta, mas se sua classe for uma instância de uma metaclasse personalizada, use a função create_custom_overrides_meta para criar uma metaclasse compatível com o decorador de substituição. Para testes, execute o módulo override .__ init__.

Michael
fonte
0

No Python 2.6+ e no Python 3.2+ você pode fazê-lo (na verdade, simule , o Python não suporta sobrecarga de funções e a classe filho substitui automaticamente o método dos pais). Podemos usar decoradores para isso. Mas primeiro, observe que Python @decoratorse Java @Annotationssão coisas totalmente diferentes. O anterior é um invólucro com código concreto, enquanto mais tarde é um sinalizador para o compilador.

Para isso, primeiro faça 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)

resultado:

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

Observe que você deve usar o decorador aqui entre parênteses

Uma coisa a lembrar é que, como o Python não possui sobrecarga de função diretamente, mesmo que a Classe B não herda da Classe A, mas precise de todos esses foos, você também precisará usar o @Override (embora o apelido 'Overload' seja parecido) melhor nesse caso)

mradul
fonte