Beberapa perangkap statis mengetik Python


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' # error: Incompatible types in assignment # (expression has type "str", variable has type "int") 

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] # type: ignore class ValueHandler(Generic[T]): """Handle object _value attribute, asserting it's type.""" def __get__(self, obj: 'Gen[T]', cls: Type['Gen[T]'] ) -> Union[T, 'ValueHandler[T]']: if not obj: return self _init_obj_ref(obj) if not obj._value: obj._value = obj.ref() return obj._value def __set__(self, obj: 'Gen[T]', val: T) -> None: _init_obj_ref(obj) if not isinstance(val, obj.ref): raise TypeError(f'has to be of type {obj.ref}, pasted {val}') obj._value = val class Gen(Generic[T]): _value: T ref: Type[T] value = ValueHandler[T]() def __init__(self, value: T) -> None: self._value = value class A: pass class B(A): pass b = Gen[A](B()) b.value = A() b.value = int() # TypeError: has to be of type <class '__main__.A'>, pasted 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]): # __args are unique every instantiation __args: ty.Optional[ty.Tuple[ty.Type[T]]] = None value: T def __init__(self, value: ty.Optional[T]=None) -> None: """Get actual type of generic and initizalize it's value.""" cls = ty.cast(A, self.__class__) if cls.__args: self.ref = cls.__args[0] else: self.ref = type(value) if value: self.value = value else: self.value = self.ref() cls.__args = None def __class_getitem__(cls, *args: ty.Union[ty.Type[int], ty.Type[str]] ) -> ty.Type['A']: """Recive type args, if passed any before initialization.""" cls.__args = ty.cast(ty.Tuple[ty.Type[T]], args) return super().__class_getitem__(*args, **kwargs) # type: ignore a = A[int]() b = A(int()) c = A[str]() print([a.value, b.value, c.value]) # [0, 0, ''] 

[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: #   A  B ... def bar(f: Callable[['A'], None]): #       A ... 

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: #   B   ... def bar(f: Callable[['TA'], None]): #     A  B ... 

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) # TypeError: wrong pushued val type push_func(c, 1) # TypeError: is not IntStackP 

Di sisi lain, MyPy, pada gilirannya, berperilaku lebih pintar, dan menyoroti ketidakcocokan jenis:


 push_func(a, 1) push_func(b, 1) # Argument 1 to "push_func" has incompatible type "StrStack"; # expected "IntStackP" # Following member(s) of "StrStack" have conflicts: # _list: expected "List[int]", got "List[str]" # Expected: # def push(self, val: int) -> None # Got: # def push(self, val: str, weather: Optional[Any] = ...) -> None 

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 # Inferred type is 'A', but runtime type is 'int'? 

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.

Source: https://habr.com/ru/post/id437018/


All Articles