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?


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

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

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?


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.


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.


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


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.


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()

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__.


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)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.