بهترین شیوه‌های حاشیه‌نویسی

نویسنده:

لری هیستینگز

دسترسی به دیکشنری حاشیه‌نویسی‌های یک شیء در پایتون 3.10 و جدیدتر

پایتون 3.10 یک تابع جدید به کتابخانه استاندارد می‌افزاید: inspect.get_annotations(). در نسخه‌های 3.10 تا 3.13 پایتون، فراخوانی این تابع بهترین روش برای دسترسی به دیکشنری حاشیه‌نویسی‌های هر شیء‌ای است که از حاشیه‌نویسی پشتیبانی می‌کند. این تابع همچنین می‌تواند حاشیه‌نویسی‌های رشته‌ای‌شده را برای شما از حالت رشته‌ای خارج کند (un-stringize).

در پایتون 3.14، ماژول جدید annotationlib با قابلیت‌هایی برای کار با حاشیه‌نویسی‌هاوجود دارد. این ماژول شامل یک تابع annotationlib.get_annotations() است که جایگزین inspect.get_annotations() شده است.

اگر به هر دلیلی inspect.get_annotations() برای مورد استفاده شما قابل‌استفاده نیست، می‌توانید به‌صورت دستی به عضو داده‌ای __annotations__ دسترسی پیدا کنید. بهترین رویه برای این کار نیز در Python 3.10 تغییر کرد: از Python 3.10 به بعد، تضمین شده است که o.__annotations__ همیشه برای توابع، کلاس‌ها و ماژول‌های پایتون کار می‌کند. اگر مطمئن هستید شیء مورد بررسی شما یکی از این سه شیء خاص است، می‌توانید به‌سادگی از o.__annotations__ برای دسترسی به دیکشنری حاشیه‌نویسی‌های شیء استفاده کنید.

با این حال، سایر انواع اشیای فراخوانی‌پذیر—برای مثال، آن‌هایی که توسط functools.partial() ایجاد شده‌اند—ممکن است ویژگی __annotations__ تعریف‌شده‌ای نداشته باشند. هنگام دسترسی به __annotations__ شیء‌ای که ممکن است ناشناخته باشد، بهترین روش در نسخه‌های 3.10 و جدیدتر پایتون این است که getattr() را با ۳ آرگومان فراخوانی کنید، برای مثال getattr(o, '__annotations__', None).

پیش از پایتون 3.10، دسترسی به __annotations__ در کلاسی که هیچ حاشیه‌نویسی‌ایتعریف نمی‌کند، اما کلاس والد آن حاشیه‌نویسی دارد، __annotations__ کلاس والد را برمی‌گرداند. در پایتون 3.10 و نسخه‌های جدیدتر، حاشیه‌نویسی‌های کلاس فرزند در عوض یک دیکشنری خالی خواهد بود.

دسترسی به دیکشنری حاشیه‌نویسی‌هامتعلق به یک شیء در پایتون 3.9 و قدیمی‌تر

در پایتون 3.9 و نسخه‌های قدیمی‌تر، دسترسی به دیکشنری حاشیه‌نویسی‌ها برای یک شیء، بسیار پیچیده‌تر از نسخه‌های جدیدتر است. این مشکل یک نقص طراحی در این نسخه‌های قدیمی‌تر پایتون است، به‌ویژه مربوط به حاشیه‌نویسی‌های کلاس (class annotations).

بهترین روش برای دسترسی به دیکشنری حاشیه‌نویسی‌هادر اشیای دیگر--توابع، سایر اشیای فراخوانی‌پذیر و ماژول‌ها--همان بهترین روش برای 3.10 است، با فرض اینکه شما inspect.get_annotations() را فراخوانی نمی‌کنید: باید برای دسترسی به ویژگی __annotations__ شیء، از getattr() با سه آرگومان استفاده کنید.

متأسفانه، این بهترین روش برای کلاس‌ها نیست. مشکل این است که از آن‌جا که __annotations__ در کلاس‌ها اختیاری است و چون کلاس‌ها می‌توانند ویژگی‌ها را از کلاس‌های پایه خود به ارث ببرند، دسترسی به ویژگی __annotations__ یک کلاس ممکن است ناخواسته دیکشنری حاشیه‌نویسی‌های یک کلاس پایه را برگرداند. به‌عنوان مثال:

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

این، دیکشنری حاشیه‌نویسی را از Base چاپ می‌کند، نه Derived.

اگر شیئی که بررسی می‌کنید یک کلاس باشد (isinstance(o, type))، کد شما باید مسیر کد جداگانه‌ای داشته باشد. در این صورت، بهترین روش به یک جزئیات پیاده‌سازی در پایتون 3.9 و نسخه‌های پیش از آن متکی است: اگر برای کلاسی حاشیه‌نویسی‌هاییتعریف شده باشند، آن‌ها در دیکشنری __dict__ کلاس ذخیره می‌شوند. از آن‌جا که ممکن است برای کلاس حاشیه‌نویسی‌هاییتعریف شده باشند یا نشده باشند، بهترین روش این است که متد get() را روی دیکشنری کلاس فراخوانی کنید.

برای جمع‌بندی همه موارد، در ادامه کد نمونه‌ای آمده است که به‌طور امن به ویژگی __annotations__ یک شیء دلخواه در پایتون 3.9 و پیش از آن دسترسی پیدا می‌کند:

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

پس از اجرای این کد، ann باید یا یک دیکشنری باشد یا None. توصیه می‌شود پیش از بررسی بیشتر، نوع ann را با استفاده از isinstance() دوباره بررسی کنید.

توجه داشته باشید که برخی اشیای نوع غیرعادی یا نقص‌دار ممکن است ویژگی __dict__ را نداشته باشند، بنابراین برای احتیاط بیشتر، ممکن است بخواهید برای دسترسی به __dict__ از getattr() نیز استفاده کنید.

غیررشته‌ای‌سازی دستی حاشیه‌نویسی‌های رشته‌ای‌شده

در موقعیت‌هایی که برخی حاشیه‌نویسی‌هاممکن است «رشته‌ای‌شده» (stringized) باشند و شما می‌خواهید آن رشته‌ها را ارزیابی کنید تا مقادیر پایتونی متناظر با آن‌ها تولید شوند، واقعاً بهترین کار این است که inspect.get_annotations() را فراخوانی کنید تا این کار را برای شما انجام دهد.

اگر از پایتون 3.9 یا نسخه‌های قدیمی‌تر استفاده می‌کنید، یا اگر به هر دلیلی نمی‌توانید از inspect.get_annotations() استفاده کنید، باید همان منطق را پیاده‌سازی کنید. به شما توصیه می‌شود پیاده‌سازی inspect.get_annotations() را در نسخه‌ی فعلی پایتون بررسی کنید و رویکرد مشابهی را دنبال کنید.

به‌طور خلاصه، اگر می‌خواهید یک حاشیه‌نویسی رشته‌شده (stringized annotation) را روی یک شیء دلخواه o ارزیابی کنید:

  • اگر o یک ماژول باشد، از o.__dict__ به‌عنوان globals هنگام فراخوانی eval() استفاده کنید.

  • اگر o یک کلاس باشد، هنگام فراخوانی eval()، از sys.modules[o.__module__].__dict__ به‌عنوان globals و از dict(vars(o)) به‌عنوان locals استفاده کنید.

  • اگر o یک فراخوانی‌پذیر پوششی‌شده با استفاده از functools.update_wrapper()، @functools.wraps یا functools.partial() باشد، آن را به‌صورت تکراری با دسترسی به o.__wrapped__ یا o.func بسته به مورد از پوشش خارج کنید، تا تابع ریشه‌ای بدون پوشش را پیدا کنید.

  • اگر o فراخوانی‌پذیر باشد (اما کلاس نباشد)، هنگام فراخوانی eval() از o.__globals__ به‌عنوان سراسری‌ها استفاده کنید.

با این حال، نمی‌توان همه‌ی مقادیر رشته‌ای را که به‌عنوان حاشیه‌نویسی استفاده می‌شوند، با موفقیت به‌وسیله‌ی eval() به مقادیر پایتون تبدیل کرد. مقادیر رشته‌ای از نظر تئوری می‌توانند شامل هر رشته‌ی معتبری باشند و در عمل موارد استفاده‌ی معتبری برای راهنمایی‌های نوع وجود دارد که مستلزم حاشیه‌نویسی با مقادیری رشته‌ای هستند که مشخصاً نمی‌توانند ارزیابی شوند. برای مثال:

  • انواع اجتماعی (union types) PEP 604 با استفاده از |، پیش از آنکه پشتیبانی از این قابلیت به Python 3.10 اضافه شود.

  • تعریف‌هایی که در ران‌تایم نیازی به آن‌ها نیست و فقط زمانی ایمپورت می‌شوند که typing.TYPE_CHECKING درست باشد.

اگر eval() سعی در ارزیابی چنین مقادیری داشته باشد، شکست خواهد خورد و استثنایی پرتاب خواهد کرد. بنابراین، هنگام طراحی API یک کتابخانه که با حاشیه‌نویسی‌ها کار می‌کند، توصیه می‌شود تنها زمانی برای ارزیابی مقادیر رشته‌ای اقدام کنید که فراخواننده به‌صراحت این کار را درخواست کرده باشد.

بهترین شیوه‌ها برای __annotations__ در هر نسخه‌ای از پایتون

  • باید از انتساب مستقیم به عضو __annotations__ شیء‌ها خودداری کنید. اجازه دهید پایتون تنظیم __annotations__ را مدیریت کند.

  • اگر مستقیماً به عضو __annotations__ یک شیء انتساب می‌دهید، باید همیشه آن را روی یک شیء dict تنظیم کنید.

  • باید از دسترسی مستقیم به __annotations__ روی هر شیء اجتناب کنید. به‌جای آن، از annotationlib.get_annotations() (پایتون 3.14+) یا inspect.get_annotations() (پایتون 3.10+) استفاده کنید.

  • اگر مستقیماً به عضو __annotations__ یک شیء دسترسی دارید، باید پیش از تلاش برای بررسی محتوای آن، اطمینان حاصل کنید که یک دیکشنری است.

  • شما باید از تغییر دیکشنری‌های __annotations__ خودداری کنید.

  • شما باید از حذف ویژگی __annotations__ یک شیء خودداری کنید.

رفتارهای عجیب __annotations__

در تمام نسخه‌های Python 3، اشیای تابع اگر هیچ‌یک از حاشیه‌نویسی‌ها برای آن‌ها تعریف نشده باشد، یک دیکشنری حاشیه‌نویسی‌ها را به‌صورت تنبل (lazy-create) ایجاد می‌کنند. شما می‌توانید ویژگی __annotations__ را با استفاده از del fn.__annotations__ حذف کنید، اما اگر سپس به fn.__annotations__ دسترسی پیدا کنید، شیء یک دیکشنری خالی جدید ایجاد می‌کند که آن را ذخیره می‌کند و به‌عنوان حاشیه‌نویسی‌های خود بازمی‌گرداند. حذف حاشیه‌نویسی‌های یک تابع پیش از آنکه دیکشنری حاشیه‌نویسی‌های خود را به‌صورت تنبل ایجاد کرده باشد، یک AttributeError را پرتاب می‌کند؛ تضمین می‌شود که استفاده از del fn.__annotations__ دو بار پشت سر هم، همیشه یک AttributeError را پرتاب کند.

همه موارد پاراگراف بالا، در پایتون 3.10 و نسخه‌های جدیدتر، برای اشیای کلاس و ماژول نیز اعمال می‌شود.

در همه نسخه‌های پایتون 3، می‌توانید __annotations__ را روی یک شیء تابع به None تنظیم کنید. با این حال، دسترسی بعدی به حاشیه‌نویسی‌های آن شیء با استفاده از fn.__annotations__، مطابق پاراگراف اول این بخش، یک دیکشنری خالی را به‌صورت تنبل ایجاد می‌کند. این موضوع در هیچ نسخه‌ای از پایتون برای ماژول‌ها و کلاس‌ها صادق نیست؛ آن اشیاء اجازه می‌دهند __annotations__ را به هر مقدار پایتون تنظیم کنید، و هر مقداری را که تنظیم شده باشد حفظ می‌کنند.

اگر پایتون حاشیه‌نویسی‌های شمارا برایتان به رشته تبدیل کند (با استفاده از from __future__ import annotations) و شما یک رشته را به‌عنوان حاشیه‌نویسی مشخص کنید، خود رشته داخل علامت نقل‌قول قرار خواهد گرفت. در عمل، حاشیه‌نویسی داخل علامت نقل‌قول قرار می‌گیرد، دو بار. برای مثال:

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

این {'a': "'str'"} را چاپ می‌کند. این واقعاً نباید یک «رفتار عجیب» تلقی شود؛ اینجا صرفاً به این دلیل ذکر شده است که ممکن است غافلگیرکننده باشد.

اگر از کلاسی با فراکلاس سفارشی استفاده کنید و به __annotations__ کلاس دسترسی پیدا کنید، ممکن است رفتار غیرمنتظره‌ای را مشاهده کنید؛ برای مشاهده چند مثال، 749 را ببینید. می‌توانید با استفاده از annotationlib.get_annotations() در پایتون 3.14+ یا inspect.get_annotations() در پایتون 3.10+ از این رفتارهای عجیب اجتناب کنید. در نسخه‌های پیشین پایتون، می‌توانید با دسترسی به حاشیه‌نویسی‌ها از طریق __dict__ کلاس، از این اشکال‌ها اجتناب کنید (برای مثال، cls.__dict__.get('__annotations__', None)).

در برخی نسخه‌های پایتون، نمونه‌های کلاس‌ها ممکن است دارای ویژگی __annotations__ باشند. با این حال، این یک قابلیت پشتیبانی‌شده نیست. اگر به حاشیه‌نویسی‌های یک نمونهنیاز دارید، می‌توانید از type() برای دسترسی به کلاس آن استفاده کنید (برای مثال، annotationlib.get_annotations(type(myinstance)) در پایتون 3.14+).