
أعتقد أننا اعتدنا ببطء على حقيقة أن بيثون لديه تعليقات توضيحية من النوع: لقد أعيد إصدارين إلى الوراء (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'
ومع ذلك ، ماذا لو كتبنا مكتبة ، والمبرمج الذي يستخدمها لن يستخدم محلل ثابت؟
هل يجبر المستخدم على تهيئة الفصل بقيمة ، ثم تخزين نوعه؟
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]
بالطبع ، نتيجة لذلك ، سيكون من الضروري إعادة الكتابة للاستخدام العالمي ، لكن الجوهر واضح.
[UPD]: في الصباح ، قررت أن أفعل نفس الشيء كما في وحدة typing
نفسها ، ولكن أبسط:
import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):
[محدث]: قال مطور typing
إيفان ليفينسكي إن كلا الخيارين يمكن أن ينكسرا بشكل غير متوقع.
على أي حال ، يمكنك استخدام أي طريقة. ربما __class_getitem__
أفضل قليلاً ، على الأقل __class_getitem__
هي طريقة خاصة موثقة (على الرغم من أن سلوكها بالنسبة للأدوية ليست كذلك).
وظائف والأسماء المستعارة
نعم ، ليست الأدوية الشائعة سهلة على الإطلاق:
على سبيل المثال ، إذا قبلنا دالة في مكان ما كوسيطة ، فسيتحول تعليقها تلقائيًا من المتغير إلى المخالف:
class A: pass class B(A): pass def foo(arg: 'A') -> None:
ومن حيث المبدأ ، ليس لدي أي شكاوى حول المنطق ، فقط يجب حل هذا من خلال الأسماء المستعارة العامة:
TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:
بشكل عام ، يجب قراءة القسم الخاص بتباين الأنواع بعناية ، وليس مرة واحدة.
التوافق الخلفي
هذه ليست ساخنة: من الإصدار 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)
من ناحية أخرى ، MyPy ، بدوره ، يتصرف بشكل أكثر ذكاء ، ويسلط الضوء على عدم توافق الأنواع:
push_func(a, 1) push_func(b, 1)
مشغل الزائد
موضوع جديد جدا ، لأنه عند التحميل الزائد للمشغلين بأمان كامل ، تختفي كل المتعة. لقد ظهر هذا السؤال مرارًا وتكرارًا في جهاز تتبع الأخطاء 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
إذا كانت طريقة التعيين المركب تُرجع __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' : ...
في بعض الأماكن ، انتقلت التحذيرات بالفعل إلى الوثائق ، وفي بعض الأماكن لا تزال تعمل على التحفيز. لكن الاستنتاج العام للمساهمين هو ترك مثل هذه الأحمال الزائدة مقبولة.