بعض المزالق من الكتابة الساكنة في بيثون


أعتقد أننا اعتدنا ببطء على حقيقة أن بيثون لديه تعليقات توضيحية من النوع: لقد أعيد إصدارين إلى الوراء (3.5) في التعليقات التوضيحية للوظائف والأساليب ( PEP 484 ) ، وفي الإصدار الأخير (3.6) إلى المتغيرات ( PEP 526 ).


نظرًا لأن كلا من PEPs مستوحاة من MyPy ، سأخبرك ما أفراح الدنيوية والتناقضات المعرفية التي تنتظرني عند استخدام هذا المحلل الثابت ، وكذلك نظام الكتابة ككل.


إخلاء المسئولية: أنا لا أثير مسألة ضرورة أو ضرر الكتابة الساكنة في بيثون. أنا أتحدث فقط عن المزالق التي صادفتها أثناء العمل في سياق مكتوب بشكل ثابت.

الوراثة (الكتابة. الجيل)


من الجيد استخدام شيء مثل List[int] و Callable[[int, str], None] في التعليقات التوضيحية.
إنه لأمر لطيف للغاية عندما يبرز المحلل الكود التالي:


 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") 

ومع ذلك ، ماذا لو كتبنا مكتبة ، والمبرمج الذي يستخدمها لن يستخدم محلل ثابت؟
هل يجبر المستخدم على تهيئة الفصل بقيمة ، ثم تخزين نوعه؟


 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) 

بطريقة ما ليست سهلة الاستعمال.
ولكن ماذا لو كنت تريد أن تفعل ذلك؟


 b = Gen[A](B()) 

بحثًا عن إجابة على هذا السؤال ، مررت typing قليلاً ، وتوغلت في عالم المصانع.

الحقيقة هي أنه بعد تهيئة مثيل الفئة Generic ، تحصل على السمة __origin_class__ ، التي تحتوي على السمة __args__ ، وهي نوع tuple. ومع ذلك ، فإن الوصول إليها من __init__ ، وكذلك من __new__ ، ليس كذلك. كما أنها ليست في __call__ metaclass. والخدعة هي أنه في وقت تهيئة الفئة الفرعية من Generic فإنها تتحول إلى ملف _GenericAlias آخر _GenericAlias ​​، والذي يحدد النوع النهائي ، إما بعد تهيئة الكائن ، بما في ذلك جميع أساليب ملف التعريف الخاص به ، أو في الوقت الذي __getithem__ عليه. وبالتالي ، لا توجد طريقة للحصول على أنواع عامة عند إنشاء كائن.


نحن رمي هذه القمامة ، وعدت بحل أكثر عالمية.

لذلك ، كتبت لنفسي وصفًا صغيرًا يحل هذه المشكلة:


 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 

بالطبع ، نتيجة لذلك ، سيكون من الضروري إعادة الكتابة للاستخدام العالمي ، لكن الجوهر واضح.


[UPD]: في الصباح ، قررت أن أفعل نفس الشيء كما في وحدة typing نفسها ، ولكن أبسط:


 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, ''] 

[محدث]: قال مطور typing إيفان ليفينسكي إن كلا الخيارين يمكن أن ينكسرا بشكل غير متوقع.


على أي حال ، يمكنك استخدام أي طريقة. ربما __class_getitem__ أفضل قليلاً ، على الأقل __class_getitem__ هي طريقة خاصة موثقة (على الرغم من أن سلوكها بالنسبة للأدوية ليست كذلك).

وظائف والأسماء المستعارة


نعم ، ليست الأدوية الشائعة سهلة على الإطلاق:
على سبيل المثال ، إذا قبلنا دالة في مكان ما كوسيطة ، فسيتحول تعليقها تلقائيًا من المتغير إلى المخالف:


 class A: pass class B(A): pass def foo(arg: 'A') -> None: #   A  B ... def bar(f: Callable[['A'], None]): #       A ... 

ومن حيث المبدأ ، ليس لدي أي شكاوى حول المنطق ، فقط يجب حل هذا من خلال الأسماء المستعارة العامة:


 TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None: #   B   ... def bar(f: Callable[['TA'], None]): #     A  B ... 

بشكل عام ، يجب قراءة القسم الخاص بتباين الأنواع بعناية ، وليس مرة واحدة.


التوافق الخلفي


هذه ليست ساخنة: من الإصدار 3.7 Generic هي فئة فرعية من ABCMeta ، وهي مريحة وجيدة للغاية. إنه لأمر سيء أن يكسر هذا الرمز إذا كان يعمل على 3.6.


الميراث الهيكلي


في البداية كنت سعيدًا جدًا: تم تسليم الواجهات! يتم تنفيذ دور الواجهات بواسطة فئة Protocol من الوحدة النمطية typing_extensions ، والتي تتيح لك ، بالاقتران مع @runtime ، التحقق مما إذا كانت الفئة تنفذ الواجهة دون وراثة مباشرة. يتم تمييز MyPy أيضًا على مستوى أعمق.


ومع ذلك ، لم ألاحظ الكثير من الفوائد العملية في وقت التشغيل مقارنةً بالميراث المتعدد.
يبدو أن الديكور يتحقق فقط من وجود طريقة بالاسم المطلوب ، دون حتى التحقق من عدد الوسائط ، ناهيك عن الكتابة:


 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 

من ناحية أخرى ، MyPy ، بدوره ، يتصرف بشكل أكثر ذكاء ، ويسلط الضوء على عدم توافق الأنواع:


 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 

مشغل الزائد


موضوع جديد جدا ، لأنه عند التحميل الزائد للمشغلين بأمان كامل ، تختفي كل المتعة. لقد ظهر هذا السؤال مرارًا وتكرارًا في جهاز تتبع الأخطاء MyPy ، لكنه لا يزال يقسم في بعض الأماكن ، ويمكنك إيقاف تشغيله بأمان.
أشرح الموقف:


 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'? 

إذا كانت طريقة التعيين المركب تُرجع __radd__ ، يبحث Python أولاً عن __radd__ ، ثم يستخدم __add__ ، و voila.


الأمر نفسه ينطبق على التحميل الزائد على أي طرق فرعية من النموذج:


 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' : ... 

في بعض الأماكن ، انتقلت التحذيرات بالفعل إلى الوثائق ، وفي بعض الأماكن لا تزال تعمل على التحفيز. لكن الاستنتاج العام للمساهمين هو ترك مثل هذه الأحمال الزائدة مقبولة.

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


All Articles