
Saya pikir kita perlahan-lahan mulai terbiasa dengan fakta bahwa Python memiliki tipe anotasi: mereka membawa dua rilis kembali (3.5) dalam anotasi fungsi dan metode ( PEP 484 ), dan dalam rilis terakhir (3.6) ke variabel ( PEP 526 ).
Karena kedua PEP ini diinspirasi oleh MyPy , saya akan memberi tahu Anda apa kegembiraan duniawi dan disonansi kognitif yang menunggu saya saat menggunakan penganalisa statis ini, serta sistem pengetikan secara keseluruhan.
Disclamer: Saya tidak mengajukan pertanyaan tentang perlunya atau bahaya mengetik statis dengan Python. Saya hanya berbicara tentang jebakan yang saya temui saat bekerja dalam konteks yang diketik secara statis.
Generik (mengetik. Umum)
Sangat menyenangkan menggunakan sesuatu seperti List[int]
, Callable[[int, str], None]
dalam anotasi.
Sangat bagus ketika analis menyoroti kode berikut:
T = ty.TypeVar('T') class A(ty.Generic[T]): value: T A[int]().value = 'str'
Namun, bagaimana jika kita menulis perpustakaan, dan programmer yang menggunakannya tidak akan menggunakan penganalisa statis?
Memaksa pengguna untuk menginisialisasi kelas dengan nilai, lalu menyimpan tipenya?
T = ty.TypeVar('T') class Gen(Generic[T]): value: T ref: Type[T] def __init__(self, value: T) -> None: self.value = value self.ref = type(value)
Entah bagaimana tidak ramah pengguna.
Tetapi bagaimana jika Anda ingin melakukannya?
b = Gen[A](B())
Mencari jawaban untuk pertanyaan ini, saya pergi typing
sedikit, dan terjun ke dunia pabrik.

Faktanya adalah bahwa setelah menginisialisasi instance dari kelas Generic, ia mendapatkan atribut __origin_class__
, yang memiliki atribut __args__
, yang merupakan tipe tuple. Namun, akses ke sana dari __init__
, serta dari __new__
, tidak. Juga tidak ada dalam __call__
metaclass. Dan triknya adalah bahwa pada saat inisialisasi dari subkelas Generic
ia berubah menjadi metaclass lain _GenericAlias
, yang menetapkan tipe final, baik setelah objek diinisialisasi, termasuk semua metode metaclassnya, atau pada saat __getithem__
untuknya. Jadi, tidak ada cara untuk mendapatkan tipe generik saat membangun objek.
Kami membuang sampah ini, menjanjikan solusi yang lebih universal.Oleh karena itu, saya menulis sendiri deskriptor kecil yang memecahkan masalah ini:
def _init_obj_ref(obj: 'Gen[T]') -> None: """Set object ref attribute if not one to initialized arg.""" if not hasattr(obj, 'ref'): obj.ref = obj.__orig_class__.__args__[0]
Tentu saja, sebagai konsekuensinya, perlu menulis ulang untuk penggunaan yang lebih universal, tetapi esensinya jelas.
[UPD]: Di pagi hari saya memutuskan untuk mencoba melakukan hal yang sama seperti pada modul typing
itu sendiri, tetapi lebih sederhana:
import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):
[UPD]: Pengembang typing
Ivan Levinsky mengatakan kedua opsi bisa rusak secara tak terduga.
Bagaimanapun, Anda dapat menggunakan cara apa pun. Mungkin __class_getitem__
bahkan sedikit lebih baik, setidaknya __class_getitem__
adalah metode khusus yang terdokumentasi (walaupun perilakunya untuk obat generik tidak).
Fungsi dan Alias
Ya, obat generik sama sekali tidak mudah:
Sebagai contoh, jika kita di suatu tempat menerima suatu fungsi sebagai argumen, maka anotasinya secara otomatis berubah dari kovarian menjadi kontravarian:
class A: pass class B(A): pass def foo(arg: 'A') -> None:
Dan pada prinsipnya, saya tidak memiliki keluhan tentang logika, hanya saja ini harus diselesaikan melalui alias umum:
TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:
Secara umum, bagian tentang variabilitas jenis harus dibaca dengan cermat, dan tidak sekaligus.
Kompatibilitas mundur
Ini tidak begitu panas: dari versi 3.7 Generic
adalah subclass dari ABCMeta
, yang sangat nyaman dan bagus. Sangat buruk bahwa kode ini rusak jika dijalankan pada 3.6.
Warisan Struktural (Stuctural Suptyping)
Awalnya saya sangat senang: antarmuka telah dikirim! Peran antarmuka dilakukan oleh kelas Protocol
dari modul typing_extensions
, yang dikombinasikan dengan dekorator @runtime
, memungkinkan Anda untuk memeriksa apakah kelas mengimplementasikan antarmuka tanpa pewarisan langsung. MyPy juga disorot di tingkat yang lebih dalam.
Namun, saya tidak melihat banyak manfaat praktis dalam runtime dibandingkan dengan multiple inheritance.
Tampaknya dekorator hanya memeriksa keberadaan metode dengan nama yang diperlukan, bahkan tanpa memeriksa jumlah argumen, belum lagi mengetik:
import typing as ty import typing_extensions as te @te.runtime class IntStackP(te.Protocol): _list: ty.List[int] def push(self, val: int) -> None: ... class IntStack: def __init__(self) -> None: self._list: ty.List[int] = list() def push(self, val: int) -> None: if not isinstance(val, int): raise TypeError('wrong pushued val type') self._list.append(val) class StrStack: def __init__(self) -> None: self._list: ty.List[str] = list() def push(self, val: str, weather: ty.Any=None) -> None: if not isinstance(val, str): raise TypeError('wrong pushued val type') self._list.append(val) def push_func(stack: IntStackP, value: int): if not isinstance(stack, IntStackP): raise TypeError('is not IntStackP') stack.push(value) a = IntStack() b = StrStack() c: ty.List[int] = list() push_func(a, 1) push_func(b, 1)
Di sisi lain, MyPy, pada gilirannya, berperilaku lebih pintar, dan menyoroti ketidakcocokan jenis:
push_func(a, 1) push_func(b, 1)
Operator Kelebihan
Topik yang sangat segar, karena saat operator kelebihan beban dengan keamanan tipe penuh, semua kesenangan menghilang. Pertanyaan ini telah berulang kali muncul di pelacak bug MyPy, tetapi masih bersumpah di beberapa tempat, dan Anda dapat mematikannya dengan aman.
Saya jelaskan situasinya:
class A: def __add__(self, other) -> int: return 3 def __iadd__(self, other) -> 'A': if isinstance(other, int): return NotImplemented return A() var = A() var += 3
Jika metode penetapan majemuk mengembalikan NotImplemented
, Python pertama mencari __radd__
, kemudian menggunakan __add__
, dan voila.
Hal yang sama berlaku untuk kelebihan metode subclass dari formulir:
class A: def __add__(self, x : 'A') -> 'A': ... class B(A): @overload def __add__(self, x : 'A') -> 'A': ... @overload def __add__(self, x : 'B') -> 'B' : ...
Di beberapa tempat, peringatan sudah pindah ke dokumentasi, di beberapa tempat mereka masih bekerja pada prod. Tetapi kesimpulan umum dari para kontributor adalah membiarkan kelebihan tersebut dapat diterima.