Bagaimana cara menentukan bahwa tipe pengembalian metode sama dengan kelas itu sendiri?

410

Saya memiliki kode berikut di 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)

Tetapi editor saya (PyCharm) mengatakan bahwa Posisi referensi tidak dapat diselesaikan (dalam __add__metode). Bagaimana saya menentukan bahwa saya mengharapkan tipe pengembalian menjadi tipe Position?

Sunting: Saya pikir ini sebenarnya masalah PyCharm. Ini benar-benar menggunakan informasi dalam peringatannya, dan penyelesaian kode

Tetapi koreksi saya jika saya salah, dan perlu menggunakan beberapa sintaks lainnya.

Michael van Gerwen
sumber

Jawaban:

575

TL; DR : jika Anda menggunakan Python 4.0 itu hanya berfungsi. Mulai hari ini (2019) dalam 3,7+ Anda harus mengaktifkan fitur ini menggunakan pernyataan masa depan (from __future__ import annotations ) - untuk Python 3.6 atau di bawahnya menggunakan string.

Saya kira Anda mendapatkan pengecualian ini:

NameError: name 'Position' is not defined

Hal ini karena Position harus ditentukan sebelum Anda dapat menggunakannya dalam anotasi kecuali Anda menggunakan Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 memperkenalkan PEP 563: evaluasi anotasi yang ditunda . Modul yang menggunakan pernyataan di masa mendatang from __future__ import annotationsakan menyimpan anotasi sebagai string secara otomatis:

from __future__ import annotations

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

Ini dijadwalkan menjadi default di Python 4.0. Karena Python masih merupakan bahasa yang diketik secara dinamis sehingga tidak ada pemeriksaan tipe yang dilakukan pada saat runtime, mengetik anotasi seharusnya tidak memiliki dampak kinerja, bukan? Salah! Sebelum python 3.7, modul pengetikan digunakan untuk menjadi salah satu modul python paling lambat di inti jadi jika import typingAnda akan melihat hingga 7 kali peningkatan kinerja ketika Anda meningkatkan ke 3,7.

Python <3.7: gunakan string

Menurut PEP 484 , Anda harus menggunakan string daripada kelas itu sendiri:

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

Jika Anda menggunakan kerangka Django ini mungkin akrab karena model Django juga menggunakan string untuk referensi ke depan (definisi kunci asing di mana model asing adalah selfatau belum dideklarasikan). Ini harus bekerja dengan Pycharm dan alat-alat lainnya.

Sumber

Bagian-bagian PEP 484 dan PEP 563 yang relevan , untuk membantu Anda melakukan perjalanan:

Referensi penerusan

Ketika petunjuk jenis berisi nama-nama yang belum didefinisikan, definisi tersebut dapat dinyatakan sebagai string literal, untuk diselesaikan nanti.

Situasi di mana hal ini terjadi umumnya adalah definisi kelas kontainer, di mana kelas yang didefinisikan terjadi dalam tanda tangan beberapa metode. Sebagai contoh, kode berikut (awal implementasi pohon biner sederhana) tidak berfungsi:

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

Untuk mengatasi ini, kami menulis:

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

Literal string harus berisi ekspresi Python yang valid (yaitu, kompilasi (lit, '', 'eval') harus menjadi objek kode yang valid) dan harus mengevaluasi tanpa kesalahan setelah modul telah dimuat penuh. Namespace lokal dan global yang dievaluasi harus ruang nama yang sama di mana argumen default untuk fungsi yang sama akan dievaluasi.

dan PEP 563:

Dalam Python 4.0, anotasi fungsi dan variabel tidak akan lagi dievaluasi pada waktu definisi. Sebagai gantinya, bentuk string akan disimpan dalam __annotations__kamus masing-masing . Pemeriksa tipe statis tidak akan melihat perbedaan dalam perilaku, sedangkan alat yang menggunakan anotasi saat runtime harus melakukan evaluasi yang ditunda.

...

Fungsi yang dijelaskan di atas dapat diaktifkan mulai dari Python 3.7 menggunakan impor khusus berikut:

from __future__ import annotations

Hal-hal yang mungkin tergoda untuk Anda lakukan

A. Definisikan boneka Position

Sebelum definisi kelas, tempatkan definisi dummy:

class Position(object):
    pass


class Position(object):
    ...

Ini akan menghilangkan NameErrordan bahkan mungkin terlihat OK:

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

Tetapi apakah itu?

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

B. Kera-tambalan untuk menambahkan anotasi:

Anda mungkin ingin mencoba beberapa sihir pemrograman meta Python dan menulis dekorator ke monyet-patch definisi kelas untuk menambahkan anotasi:

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

Dekorator harus bertanggung jawab atas hal yang setara ini:

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

Setidaknya sepertinya benar:

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

Mungkin terlalu banyak kesulitan.

Kesimpulan

Jika Anda menggunakan 3,6 atau di bawah menggunakan string literal yang berisi nama kelas, gunakan 3,7 from __future__ import annotationsdan itu hanya akan berfungsi.

Paulo Scardine
sumber
2
Benar, ini bukan masalah PyCharm dan lebih masalah Python 3.5 PEP 484. Saya menduga Anda akan mendapatkan peringatan yang sama jika Anda menjalankannya melalui alat tipe mypy.
Paul Everitt
23
> jika Anda menggunakan Python 4.0 itu hanya berfungsi, apakah Anda melihat Sarah Connor? :)
scrutari
@ JoelBerkeley Saya baru saja mengujinya dan mengetik parameter bekerja untuk saya di 3.6, tapi jangan lupa untuk mengimpor dari typingkarena semua jenis yang Anda gunakan harus dalam lingkup ketika string dievaluasi.
Paulo Scardine
ah, kesalahan saya, saya hanya menempatkan ''sekeliling kelas, bukan parameter tipe
joelb
5
Catatan penting bagi siapa pun yang menggunakan from __future__ import annotations- ini harus diimpor sebelum semua impor lainnya.
Artur
16

Menentukan jenis sebagai string baik-baik saja, tetapi selalu memberi saya sedikit bahwa kita pada dasarnya menghindari parser. Jadi sebaiknya Anda tidak salah mengeja salah satu dari string literal ini:

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

Sedikit variasi adalah dengan menggunakan typevar terikat, setidaknya Anda harus menulis string hanya sekali ketika mendeklarasikan 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
sumber
8
Saya berharap Python harus typing.Selfmenentukan ini secara eksplisit.
Alexander Huszagh
2
Saya datang ke sini untuk melihat apakah sesuatu seperti Anda typing.Selfada. Mengembalikan string kode keras gagal mengembalikan jenis yang benar ketika memanfaatkan polimorfisme. Dalam kasus saya, saya ingin menerapkan metode deserialize class. Saya memutuskan untuk mengembalikan dikt (kwargs) dan menelepon some_class(**some_class.deserialize(raw_data)).
Scott P.
Jenis anotasi yang digunakan di sini sesuai ketika menerapkan ini dengan benar untuk menggunakan subclass. Namun, implementasinya kembali Position, dan bukan kelasnya, jadi contoh di atas secara teknis salah. Implementasinya harus diganti Position(dengan sesuatu seperti self.__class__(.
Sam Bull
Selain itu, anotasi mengatakan bahwa tipe pengembalian bergantung pada other, tetapi kemungkinan besar sebenarnya tergantung self. Jadi, Anda harus meletakkan anotasi selfuntuk menggambarkan perilaku yang benar (dan mungkin otherseharusnya hanya Positionuntuk menunjukkan bahwa itu tidak terkait dengan tipe pengembalian). Ini juga dapat digunakan untuk kasus-kasus ketika Anda hanya bekerja dengan self. misalnyadef __aenter__(self: T) -> T:
Sam Bull
15

Nama 'Posisi' tidak tersedia pada saat tubuh kelas itu sendiri diuraikan. Saya tidak tahu bagaimana Anda menggunakan deklarasi tipe, tetapi Python PEP 484 - yang merupakan mode yang paling banyak digunakan jika menggunakan petunjuk pengetikan ini mengatakan bahwa Anda dapat dengan mudah meletakkan nama sebagai string pada saat ini:

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

Periksa https://www.python.org/dev/peps/pep-0484/#forward-references - alat yang sesuai dengan itu akan tahu untuk membuka bungkusan nama kelas dari sana dan memanfaatkannya. (Selalu penting untuk memiliki perlu diingat bahwa bahasa Python sendiri tidak melakukan anotasi ini - mereka biasanya dimaksudkan untuk analisis kode-statis, atau orang dapat memiliki pustaka / kerangka kerja untuk memeriksa jenis dalam run-time - tetapi Anda harus secara eksplisit mengaturnya).

perbarui Juga, pada Python 3.8, periksa pep-563 - pada Python 3.8 dimungkinkan untuk menulis from __future__ import annotationsuntuk menunda evaluasi anotasi - kelas referensi maju harus bekerja secara langsung.

jsbueno
sumber
9

Ketika petunjuk tipe berbasis string dapat diterima, __qualname__item tersebut juga dapat digunakan. Itu memegang nama kelas, dan tersedia di tubuh definisi kelas.

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

Dengan melakukan ini, mengubah nama kelas tidak berarti memodifikasi petunjuk tipe. Tapi saya pribadi tidak akan mengharapkan editor kode pintar untuk menangani formulir ini dengan baik.

Yvon DUTAPIS
sumber
1
Ini sangat berguna karena ia tidak meng-hardcode nama kelas, jadi ia tetap berfungsi di dalam subclass.
Florian Brucker
Saya tidak yakin apakah ini akan berhasil dengan evaluasi anotasi yang ditunda (PEP 563), jadi saya telah mengajukan pertanyaan untuk itu .
Florian Brucker