Come faccio a specificare che il tipo restituito di un metodo è uguale alla classe stessa?


410

Ho il seguente codice in 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)

Ma il mio editor (PyCharm) afferma che la posizione di riferimento non può essere risolta (nel __add__metodo). Come devo specificare che mi aspetto che il tipo restituito sia di tipo Position?

Modifica: penso che questo sia in realtà un problema di PyCharm. In realtà utilizza le informazioni nei suoi avvisi e il completamento del codice

Ma correggimi se sbaglio e ho bisogno di usare qualche altra sintassi.

Risposte:


576

TL; DR : se stai usando Python 4.0 funziona. A partire da oggi (2019) in 3.7+ devi attivare questa funzione usando un'istruzione futura ( from __future__ import annotations) - per Python 3.6 o versioni precedenti usa una stringa.

Immagino tu abbia questa eccezione:

NameError: name 'Position' is not defined

Questo perché Positiondeve essere definito prima di poterlo utilizzare in un'annotazione a meno che non si stia utilizzando Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 introduce PEP 563: valutazione posticipata delle annotazioni . Un modulo che utilizza la dichiarazione futura from __future__ import annotationsmemorizzerà automaticamente le annotazioni come stringhe:

from __future__ import annotations

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

Questo è programmato per diventare il predefinito in Python 4.0. Poiché Python è ancora un linguaggio tipizzato in modo dinamico, quindi non viene eseguito alcun controllo del tipo in fase di esecuzione, le annotazioni di digitazione non dovrebbero avere alcun impatto sulle prestazioni, giusto? Sbagliato! Prima di Python 3.7 il modulo di digitazione era uno dei moduli Python più lenti nel core, quindi se import typingvedrai un aumento delle prestazioni fino a 7 volte quando esegui l'aggiornamento a 3.7.

Python <3.7: usa una stringa

Secondo PEP 484 , è necessario utilizzare una stringa anziché la classe stessa:

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

Se si utilizza il framework Django, ciò può essere familiare in quanto i modelli Django usano anche le stringhe per riferimenti in avanti (definizioni di chiave esterna in cui il modello esterno è selfo non è ancora stato dichiarato). Questo dovrebbe funzionare con Pycharm e altri strumenti.

fonti

Le parti rilevanti di PEP 484 e PEP 563 , per risparmiare il viaggio:

Riferimenti in avanti

Quando un suggerimento sul tipo contiene nomi che non sono stati ancora definiti, tale definizione può essere espressa come una stringa letterale, da risolvere in seguito.

Una situazione in cui ciò si verifica comunemente è la definizione di una classe contenitore, in cui la classe da definire si verifica nella firma di alcuni dei metodi. Ad esempio, il seguente codice (l'inizio di una semplice implementazione dell'albero binario) non funziona:

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

Per affrontare questo, scriviamo:

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

Il valore letterale stringa deve contenere un'espressione Python valida (ovvero compilare (acceso, '', 'eval') dovrebbe essere un oggetto codice valido) e dovrebbe valutare senza errori una volta che il modulo è stato caricato completamente. Lo spazio dei nomi locale e globale in cui viene valutato dovrebbe essere lo stesso spazio dei nomi in cui verrebbero valutati gli argomenti predefiniti della stessa funzione.

e PEP 563:

In Python 4.0, le annotazioni di funzioni e variabili non saranno più valutate al momento della definizione. Invece, una forma di stringa verrà conservata nel rispettivo __annotations__dizionario. I controllori di tipi statici non vedranno alcuna differenza nel comportamento, mentre gli strumenti che utilizzano le annotazioni in fase di esecuzione dovranno eseguire una valutazione posticipata.

...

Le funzionalità sopra descritte possono essere abilitate a partire da Python 3.7 utilizzando la seguente importazione speciale:

from __future__ import annotations

Cose che potresti essere tentato di fare invece

A. Definisci un manichino Position

Prima della definizione della classe, inserisci una definizione fittizia:

class Position(object):
    pass


class Position(object):
    ...

Questo eliminerà il NameErrore potrebbe anche sembrare OK:

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

Ma è?

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

B. Monkey-patch per aggiungere le annotazioni:

Potresti provare un po 'di magia della meta-programmazione Python e scrivere un decoratore per correggere la definizione della classe in modo da aggiungere annotazioni:

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

Il decoratore dovrebbe essere responsabile dell'equivalente di questo:

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

Almeno sembra giusto:

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

Probabilmente troppi problemi.

Conclusione

Se stai usando 3.6 o sotto usa una stringa letterale contenente il nome della classe, in 3.7 usa from __future__ import annotationse funzionerà.


2
Bene, questo è meno un problema di PyCharm e più un problema di Python 3.5 PEP 484. Ho il sospetto che riceveresti lo stesso avvertimento se lo eseguissi attraverso lo strumento di tipo mypy.
Paul Everitt,

23
> se stai usando Python 4.0 funziona solo a proposito, hai visto Sarah Connor? :)
scrutari,

@JoelBerkeley L'ho appena testato e ho digitato i parametri per me su 3.6, ma non dimenticare di importare dal typingmomento che qualsiasi tipo che usi deve essere compreso nell'ambito della valutazione della stringa.
Paulo Scardine,

ah, errore mio, stavo solo mettendo in ''giro la classe, non i parametri del tipo
joelb

5
Nota importante per chiunque utilizzi from __future__ import annotations: questo deve essere importato prima di tutte le altre importazioni.
Artur

16

Specificare il tipo come stringa va bene, ma mi grida sempre un po 'che stiamo praticamente aggirando il parser. Quindi è meglio non sbagliare a scrivere una di queste stringhe letterali:

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

Una leggera variazione è usare un typevar associato, almeno allora devi scrivere la stringa una sola volta quando dichiari il 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)

8
Vorrei che Python avesse typing.Selfa specificarlo esplicitamente.
Alexander Huszagh,

2
Sono venuto qui per vedere se typing.Selfesisteva qualcosa come il tuo . Restituire una stringa codificata non riesce a restituire il tipo corretto quando si fa leva sul polimorfismo. Nel mio caso, volevo implementare un metodo di classe di deserializzazione . Decisi di restituire un dict (kwargs) e di chiamare some_class(**some_class.deserialize(raw_data)).
Scott P.

Le annotazioni sui tipi utilizzate qui sono appropriate quando vengono implementate correttamente per utilizzare le sottoclassi. Tuttavia, l'implementazione restituisce Positione non la classe, quindi l'esempio sopra è tecnicamente errato. L'implementazione dovrebbe sostituire Position(con qualcosa di simile self.__class__(.
Sam Bull,

Inoltre, le annotazioni affermano che il tipo di ritorno dipende other, ma molto probabilmente dipende in realtà self. Quindi, dovresti inserire l'annotazione selfper descrivere il comportamento corretto (e forse otherdovrebbe essere solo Positionper mostrare che non è legato al tipo restituito). Questo può essere usato anche per i casi in cui lavori solo self. ad es.def __aenter__(self: T) -> T:
Sam Bull,

15

Il nome "Posizione" non è disponibile al momento dell'analisi del corpo della classe stessa. Non so come stai usando le dichiarazioni di tipo, ma PEP 484 di Python - che è quello che la maggior parte delle modalità dovrebbe usare se usando questi suggerimenti per la digitazione dicono che puoi semplicemente mettere il nome come stringa a questo punto:

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

Controlla https://www.python.org/dev/peps/pep-0484/#forward-references - strumenti conformi a quello sapranno scartare il nome della classe da lì e farne uso (è sempre importante avere tenendo presente che il linguaggio Python stesso non fa nulla di queste annotazioni - di solito sono pensate per l'analisi del codice statico, oppure si potrebbe avere una libreria / framework per il controllo del tipo in fase di esecuzione - ma è necessario impostarlo esplicitamente).

aggiornamento Inoltre, a partire da Python 3.8, controllare pep-563 - a partire da Python 3.8 è possibile scrivere from __future__ import annotationsper rinviare la valutazione delle annotazioni - le classi di riferimento in avanti dovrebbero funzionare in modo semplice.


9

Quando un suggerimento di tipo basato su stringa è accettabile, è __qualname__possibile utilizzare anche l' elemento. Contiene il nome della classe ed è disponibile nel corpo della definizione della classe.

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

In questo modo, rinominare la classe non implica modificare i suggerimenti sul tipo. Ma personalmente non mi aspetto che gli editor di codici intelligenti gestiscano bene questo modulo.


1
Ciò è particolarmente utile perché non codifica il nome della classe, quindi continua a funzionare in sottoclassi.
Florian Brucker,

Non sono sicuro che funzionerà con la valutazione posticipata delle annotazioni (PEP 563), quindi ho fatto una domanda per questo .
Florian Brucker,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.