Como especifico que o tipo de retorno de um método é igual à própria classe?

410

Eu tenho o seguinte código em python 3:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: Position) -> Position:
        return Position(self.x + other.x, self.y + other.y)

Mas meu editor (PyCharm) diz que a posição de referência não pode ser resolvida (no __add__método). Como devo especificar que espero que o tipo de retorno seja do tipo Position?

Edit: Eu acho que isso é realmente um problema de PyCharm. Na verdade, ele usa as informações em seus avisos e a conclusão de código

Mas corrija-me se estiver errado e preciso usar outra sintaxe.

Michael van Gerwen
fonte

Respostas:

575

TL; DR : se você estiver usando o Python 4.0, ele simplesmente funciona. A partir de hoje (2019) em 3.7+, você deve ativar esse recurso usando uma instrução futura ( from __future__ import annotations) - para Python 3.6 ou abaixo, use uma string.

Eu acho que você recebeu essa exceção:

NameError: name 'Position' is not defined

Isso ocorre porque Positiondeve ser definido antes que você possa usá-lo em uma anotação, a menos que esteja usando o Python 4.

Python 3.7+: from __future__ import annotations

O Python 3.7 introduz o PEP 563: avaliação adiada de anotações . Um módulo que usa a instrução futura from __future__ import annotationsarmazenará anotações como seqüências de caracteres automaticamente:

from __future__ import annotations

class Position:
    def __add__(self, other: Position) -> Position:
        ...

Isso está programado para se tornar o padrão no Python 4.0. Como o Python ainda é uma linguagem de tipo dinâmico, portanto, nenhuma verificação de tipo é feita em tempo de execução, as anotações de digitação não devem ter impacto no desempenho, certo? Errado! Antes de python 3.7 do módulo de digitação costumava ser um dos mais lentos módulos python no núcleo assim se você import typingvocê vai ver até 7 vezes aumento no desempenho quando você atualizar para 3.7.

Python <3.7: use uma string

De acordo com o PEP 484 , você deve usar uma string em vez da própria classe:

class Position:
    ...
    def __add__(self, other: 'Position') -> 'Position':
       ...

Se você usa a estrutura do Django, isso pode ser familiar, pois os modelos do Django também usam strings para referências futuras (definições de chave estrangeira em que o modelo estrangeiro é self ou ainda não foi declarado). Isso deve funcionar com Pycharm e outras ferramentas.

Fontes

As partes relevantes do PEP 484 e PEP 563 , para poupar a viagem:

Encaminhar referências

Quando uma dica de tipo contém nomes que ainda não foram definidos, essa definição pode ser expressa como uma string literal, a ser resolvida posteriormente.

Uma situação em que isso ocorre geralmente é a definição de uma classe de contêiner, em que a classe que está sendo definida ocorre na assinatura de alguns dos métodos. Por exemplo, o código a seguir (o início de uma implementação simples de árvore binária) não funciona:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

Para resolver isso, escrevemos:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

O literal da string deve conter uma expressão Python válida (ou seja, compile (lit, '', 'eval') deve ser um objeto de código válido) e deve ser avaliada sem erros quando o módulo estiver totalmente carregado. O espaço para nome local e global em que é avaliado deve ser o mesmo espaço para nome no qual os argumentos padrão para a mesma função seriam avaliados.

e PEP 563:

No Python 4.0, as anotações de função e variável não serão mais avaliadas no momento da definição. Em vez disso, um formulário de string será preservado no respectivo __annotations__dicionário. Os verificadores de tipo estático não verão diferença no comportamento, enquanto as ferramentas que usam anotações em tempo de execução terão que realizar uma avaliação adiada.

...

A funcionalidade descrita acima pode ser ativada a partir do Python 3.7 usando a seguinte importação especial:

from __future__ import annotations

Coisas que você pode se sentir tentado a fazer

A. Definir um manequim Position

Antes da definição da classe, coloque uma definição fictícia:

class Position(object):
    pass


class Position(object):
    ...

Isso vai se livrar do NameErrore pode até parecer OK:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

Mas é isso?

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: False
other is Position: False

B. Patch de macaco para adicionar as anotações:

Você pode tentar um pouco da mágica de programação de Python e escrever um decorador para corrigir a definição de classe com o objetivo de adicionar anotações:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

O decorador deve ser responsável pelo equivalente a isso:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

Pelo menos parece certo:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Provavelmente demais.

Conclusão

Se você estiver usando o 3.6 ou abaixo, use uma string literal contendo o nome da classe, no 3.7 use from __future__ import annotationse ele simplesmente funcionará.

Paulo Scardine
fonte
2
Certo, isso é menos um problema do PyCharm e mais um problema do Python 3.5 PEP 484. Eu suspeito que você receba o mesmo aviso se o executar pela ferramenta do tipo mypy.
Paul Everitt #
23
> se você estiver usando o Python 4.0, ele simplesmente funciona a propósito, você viu Sarah Connor? :)
scrutari
@JoelBerkeley Acabei de testar e digitar os parâmetros trabalhados para mim na versão 3.6, mas não se esqueça de importar, typingpois qualquer tipo que você usar deve estar no escopo quando a string for avaliada.
Paulo Scardine 20/08/19
ah, meu erro, eu tinha apenas colocar ''em volta da classe, e não os parâmetros de tipo
joelb
5
Nota importante para quem from __future__ import annotationsestiver usando - isso deve ser importado antes de todas as outras importações.
Artur
16

A especificação do tipo como string é boa, mas sempre me irrita um pouco que basicamente estamos contornando o analisador. Portanto, é melhor você não digitar incorretamente nenhuma dessas strings literais:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Uma pequena variação é usar uma typevar vinculada; pelo menos, você deve escrever a string apenas uma vez ao declarar a typevar:

from typing import TypeVar

T = TypeVar('T', bound='Position')

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: T) -> T:
        return Position(self.x + other.x, self.y + other.y)
vbraun
fonte
8
Eu gostaria que o Python tivesse typing.Selfque especificar isso explicitamente.
Alexander Huszagh
2
Eu vim aqui olhando para ver se algo como o seu typing.Selfexistia. O retorno de uma sequência codificada não falha ao retornar o tipo correto ao alavancar o polimorfismo. No meu caso, eu queria implementar um método de classe desserializado . Decidi devolver um ditado (kwargs) e ligar some_class(**some_class.deserialize(raw_data)).
Scott P.
As anotações de tipo usadas aqui são apropriadas ao implementar isso corretamente para usar subclasses. No entanto, a implementação retorna Position, e não a classe, portanto o exemplo acima é tecnicamente incorreto. A implementação deve substituir Position(por algo como self.__class__(.
Sam Bull
Além disso, as anotações dizem que o tipo de retorno depende other, mas provavelmente depende self. Portanto, você precisará colocar a anotação selfpara descrever o comportamento correto (e talvez otherdeva apenas Positionmostrar que ele não está vinculado ao tipo de retorno). Isso também pode ser usado para casos em que você está trabalhando apenas self. por exemplodef __aenter__(self: T) -> T:
Sam Bull
15

O nome 'Posição' não está disponível no momento em que o corpo da classe é analisado. Não sei como você está usando as declarações de tipo, mas o PEP 484 do Python - que é o que a maioria dos modos deve usar se usar essas dicas de digitação diz que você pode simplesmente colocar o nome como uma string neste momento:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Verifique https://www.python.org/dev/peps/pep-0484/#forward-references - ferramentas em conformidade com as que saberão desembrulhar o nome da classe a partir daí e utilizá-lo (é sempre importante ter lembre-se de que a própria linguagem Python não faz nada dessas anotações - elas geralmente são destinadas à análise de código estático, ou é possível ter uma biblioteca / estrutura para verificação de tipo em tempo de execução - mas você deve definir isso explicitamente).

update Além disso, no Python 3.8, marque pep-563 - no Python 3.8 é possível escrever from __future__ import annotationspara adiar a avaliação das anotações - as classes de referência direta devem funcionar de maneira direta.

jsbueno
fonte
9

Quando uma dica de tipo baseada em string é aceitável, o __qualname__item também pode ser usado. Ele contém o nome da classe e está disponível no corpo da definição da classe.

class MyClass:
    @classmethod
    def make_new(cls) -> __qualname__:
        return cls()

Ao fazer isso, renomear a classe não implica modificar as dicas de tipo. Mas, pessoalmente, eu não esperaria que editores de código inteligentes lidassem bem com esse formulário.

Yvon DUTAPIS
fonte
11
Isso é especialmente útil porque não codifica o nome da classe, por isso continua trabalhando nas subclasses.
Florian Brucker
Não tenho certeza se isso funcionará com a avaliação adiada de anotações (PEP 563), então fiz uma pergunta para isso .
Florian Brucker