annotationlib --- امکاناتی برای درون‌نگری حاشیه‌نویسی‌ها

اضافه شده در نسخه‌ی 3.14.

کد منبع: Lib/annotationlib.py


ماژول annotationlib ابزارهایی را برای درون‌نگری حاشیه‌نویسی‌ها بر روی ماژول‌ها، کلاس‌ها و توابع فراهم می‌کند.

حاشیه‌گذاری‌ها به‌صورت تنبل ارزیابی می‌شوند و اغلب شامل ارجاع پیشرو به اشیایی هستند که هنوز در زمان ایجاد حاشیه‌گذاری تعریف نشده‌اند. این ماژول مجموعه‌ای از ابزارهای سطح پایین را فراهم می‌کند که می‌توان از آن‌ها برای بازیابی حاشیه‌گذاری‌ها به‌صورت قابل‌اعتماد استفاده کرد، حتی در صورت وجود ارجاع‌های پیشرو و سایر موارد مرزی.

این ماژول از بازیابی حاشیه‌نویسی‌ها در ۳ قالب اصلی پشتیبانی می‌کند (به Format مراجعه کنید)، که هر کدام برای موارد استفاده‌ی متفاوتی مناسب‌تر است:

  • VALUE حاشیه‌نویسی‌ها را ارزیابی می‌کند و مقدار آن‌ها را برمی‌گرداند. کار با این گزینه از همه ساده‌تر است، اما ممکن است خطاها را پرتاب کند، برای مثال اگر حاشیه‌نویسی‌ها شامل ارجاع به نام‌های تعریف‌نشده باشند.

  • FORWARDREF برای حاشیه‌نویسی‌هاییکه قابل حل نیستند، اشیای ForwardRef را برمی‌گرداند و به شما امکان می‌دهد حاشیه‌نویسی‌ها را بدون ارزیابی بررسی کنید. این حالت زمانی مفید است که نیاز داشته باشید با حاشیه‌نویسی‌هایی کار کنید که ممکن است حاوی ارجاع‌های پیشروی حل‌نشده باشند.

  • STRING حاشیه‌نویسی‌ها را به‌صورت یک رشته برمی‌گرداند، شبیه به آنچه در پرونده منبع ظاهر می‌شود. این برای مستندسازهایی که می‌خواهند حاشیه‌نویسی‌ها را به‌شکلی خوانا نمایش دهند، مفید است.

تابع get_annotations() نقطه ورود اصلی برای بازیابی حاشیه‌گذاری‌ها است. این تابع با دریافت یک تابع، کلاس یا ماژول، دیکشنری حاشیه‌گذاری‌ها را در قالب درخواستی بازمی‌گرداند. این ماژول همچنین برای کار مستقیم با annotate function که برای ارزیابی حاشیه‌گذاری‌ها استفاده می‌شود، قابلیت‌هایی فراهم می‌کند؛ مانند get_annotate_from_class_namespace() و call_annotate_function()، و همچنین تابع call_evaluate_function() برای کار با evaluate functions.

ملاحظه

بیشتر قابلیت‌های این ماژول می‌توانند کد دلخواه را اجرا کنند؛ برای اطلاعات بیشتر بخش امنیت را ببینید.

همچنین ملاحظه نمائید

PEP 649 مدل کنونی برای چگونگی کارکرد حاشیه‌نویسی‌هادر پایتون را پیشنهاد داد.

PEP 749 به تشریح جنبه‌های مختلفی از PEP 649 پرداخت و ماژول annotationlib را معرفی کرد.

بهترین شیوه‌های حاشیه‌نویسی بهترین شیوه‌ها را برای کار با حاشیه‌نویسی‌ها ارائه می‌دهد.

typing-extensions یک بک‌پورت از get_annotations() فراهم می‌کند که در نسخه‌های پیشین پایتون کار می‌کند.

معناشناسی حاشیه‌نویسی

روش ارزیابی حاشیه‌نویسی‌ها در طول تاریخ پایتون ۳ تغییر کرده است و در حال حاضر همچنان به یک future import وابسته است. مدل‌های اجرایی برای حاشیه‌نویسی‌ها وجود داشته‌اند:

  • معناشناسی استاندارد (پیش‌فرض در پایتون 3.0 تا 3.13؛ PEP 3107 و PEP 526 را ببینید): حاشیه‌نویسی‌هابه‌صورت فوری، به‌محض مواجهه در کد منبع ارزیابی می‌شوند.

  • حاشیه‌نویسی‌های رشته‌ای (با from __future__ import annotations در پایتون 3.7 و جدیدتر استفاده می‌شود؛ به PEP 563 مراجعه کنید): حاشیه‌نویسی‌ها فقط به‌صورت رشته ذخیره می‌شوند.

  • ارزیابی به‌تعویق‌افتاده (پیش‌فرض در پایتون 3.14 و نسخه‌های جدیدتر؛ PEP 649 و PEP 749 را ببینید): حاشیه‌گذاری‌ها به‌صورت تنبل ارزیابی می‌شوند، تنها زمانی که به آن‌ها دسترسی پیدا شود.

به‌عنوان مثال، برنامه زیر را در نظر بگیرید:

def func(a: Cls) -> None:
    print(a)

class Cls: pass

print(func.__annotations__)

این به‌صورت زیر رفتار خواهد کرد:

  • طبق معناشناسی استاندارد (پایتون 3.13 و نسخه‌های پیشین)، یک NameError در سطری که func تعریف شده است پرتاب می‌شود، زیرا Cls در آن نقطه یک نام تعریف‌نشده است.

  • در حالت حاشیه‌نویسی‌های رشته‌ای‌شده (اگر از from __future__ import annotations استفاده شود)، {'a': 'Cls', 'return': 'None'} چاپ خواهد شد.

  • در ارزیابی معوق (پایتون 3.14 و نسخه‌های بعد)، {'a': <class 'Cls'>, 'return': None} چاپ خواهد شد.

هنگامی که حاشیه‌نویسی‌های تابع برای نخستین بار در پایتون 3.0 (توسط PEP 3107) معرفی شدند، از معناشناسی پیش‌فرض استفاده شد، زیرا این ساده‌ترین و آشکارترین روش برای پیاده‌سازی حاشیه‌نویسی‌ها بود. هنگامی که حاشیه‌نویسی‌های متغیر در پایتون 3.6 (توسط PEP 526) معرفی شدند، از همان مدل اجرایی استفاده شد. با این حال، معناشناسی پیش‌فرض هنگام استفاده از حاشیه‌نویسی‌ها به‌عنوان راهنمایی‌های نوع مشکلاتی ایجاد می‌کرد، مانند نیاز به ارجاع به نام‌هایی که در زمان مواجهه با حاشیه‌نویسی هنوز تعریف‌نشده‌اند. علاوه بر این، مشکلات عملکردی در اجرای حاشیه‌نویسی‌ها در زمان ایمپورت ماژول وجود داشت. بنابراین، در پایتون 3.7، PEP 563 توانایی ذخیره کردن حاشیه‌نویسی‌ها به‌صورت رشته با استفاده از سینتکس from __future__ import annotations را معرفی کرد. برنامه در آن زمان این بود که در نهایت این رفتار به پیش‌فرض تبدیل شود، اما مشکلی پدیدار شد: پردازش حاشیه‌نویسی‌های رشته‌ای برای کسانی که حاشیه‌نویسی‌ها را در ران‌تایم درون‌نگری می‌کنند، دشوارتر است. یک پیشنهاد جایگزین، PEP 649، سومین مدل اجرایی، یعنی ارزیابی به‌تعویق‌افتاده را معرفی کرد و در پایتون 3.14 پیاده‌سازی شد. در صورت وجود from __future__ import annotations، همچنان از حاشیه‌نویسی‌های رشته‌ای استفاده می‌شود، اما این رفتار در نهایت حذف خواهد شد.

کلاس‌ها

class annotationlib.Format

یک IntEnum که قالب‌هایی را توصیف می‌کند که حاشیه‌نویسی‌ها می‌توانند در آن‌ها برگردانده شوند. اعضای enum، یا مقادیر صحیح معادل آن‌ها، می‌توانند به get_annotations() و سایر توابع این ماژول، و همچنین به توابع __annotate__ ارسال شوند.

VALUE = 1

مقادیر، نتیجه‌ی ارزیابی عبارت‌های حاشیه‌نویسی هستند.

VALUE_WITH_FAKE_GLOBALS = 2

مقدار ویژه‌ای که برای اعلام اینکه یک تابع annotate در یک محیط ویژه با متغیرهای سراسری جعلی ارزیابی می‌شود، به‌کار می‌رود. هنگامی که این مقدار به آن‌ها داده شود، توابع annotate باید یا همان مقداری را که برای قالب Format.VALUE برمی‌گردانند، برگردانند، یا NotImplementedError را پرتاب کنند تا نشان دهند که از اجرا در این محیط پشتیبانی نمی‌کنند. این قالب فقط به‌صورت داخلی استفاده می‌شود و نباید به توابع این ماژول داده شود.

FORWARDREF = 3

مقادیر، برای مقادیر تعریف‌شده، مقادیر واقعی حاشیه‌نویسی (بر اساس قالب Format.VALUE) هستند و برای مقادیر تعریف‌نشده، پراکسی‌های ForwardRef هستند. اشیاء واقعی ممکن است شامل ارجاع‌هایی به اشیاء پراکسی ForwardRef باشند.

STRING = 4

مقادیر، رشته‌ی متنی حاشیه‌نویسیهستند، به همان شکلی که در کد منبع ظاهر می‌شود، با در نظر گرفتن تغییراتی شامل، اما نه محدود به، نرمال‌سازی‌های فضای سفید و بهینه‌سازی‌های مقادیر ثابت.

مقادیر دقیق این رشته‌ها ممکن است در نسخه‌های آینده پایتون تغییر کند.

اضافه شده در نسخه‌ی 3.14.

class annotationlib.ForwardRef

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

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

__forward_arg__

رشته‌ای حاوی کدی که برای تولید ForwardRef ارزیابی‌شده است. این رشته ممکن است دقیقاً معادل کد منبع اصلی نباشد.

evaluate(*, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE)

ارجاع پیشرو را ارزیابی می‌کند و مقدار آن را برمی‌گرداند.

اگر آرگومان format برابر VALUE باشد (پیش‌فرض)، این متد ممکن است در صورتی استثنایی مانند NameError پرتاب کند که ارجاع پیشرو به نامی اشاره کند که نمی‌توان آن را حل کرد. می‌توان از آرگومان‌های این متد برای فراهم کردن پیوندها برای نام‌هایی استفاده کرد که در غیر این صورت تعریف‌نشده خواهند بود. اگر آرگومان format برابر FORWARDREF باشد، این متد هرگز استثنا پرتاب نخواهد کرد، اما ممکن است نمونه‌ای از ForwardRef بازگشت دهد. برای مثال، اگر شیء ارجاع پیشرو حاوی کد list[undefined] باشد، که در آن undefined نامی است که تعریف‌نشده است، ارزیابی آن با قالب FORWARDREF list[ForwardRef('undefined')] را بازگشت خواهد داد. اگر آرگومان format برابر STRING باشد، این متد __forward_arg__ را بازگشت خواهد داد.

پارامتر owner سازوکار ترجیحی برای انتقال اطلاعات محدوده به این متد را فراهم می‌کند. مالک یک ForwardRef آن شیء است که حاوی حاشیه‌نویسی ای است که ForwardRef از آن مشتق می‌شود، مانند شیء ماژول، شیء نوع، یا شیء تابع.

پارامترهای globals، locals و type_params سازوکار دقیق‌تری را برای تأثیرگذاری بر نام‌هایی که هنگام ارزیابی ForwardRef در دسترس هستند، فراهم می‌کنند. globals و locals به eval() ارسال می‌شوند و نشان‌دهنده‌ی فضای نام های سراسری و محلی هستند که نام در آن‌ها ارزیابی می‌شود. پارامتر type_params برای اشیایی که با استفاده از سینتکس بومی برای کلاس‌های عام و توابع عام ایجاد شده‌اند، مرتبط است. این، تاپلی از پارامترهای نوع است که در حین ارزیابی ارجاع پیشرو، در محدوده قرار دارند. برای مثال، اگر در حال ارزیابی یک ForwardRef هستید که از یک حاشیه‌نویسی یافت‌شده در فضای نام کلاسِ یک کلاس عام C بازیابی شده است، type_params باید روی C.__type_params__ تنظیم شود.

نمونه‌های ForwardRef که توسط get_annotations() بازگردانده می‌شوند، ارجاع‌هایی به اطلاعات مربوط به محدوده‌ای که از آن منشأ گرفته‌اند را حفظ می‌کنند، بنابراین فراخوانی این متد بدون آرگومان‌های بیشتر ممکن است برای ارزیابی چنین اشیایی کافی باشد. نمونه‌های ForwardRef که به روش‌های دیگر ایجاد شده‌اند ممکن است هیچ اطلاعاتی درباره محدوده خود نداشته باشند، بنابراین ارسال آرگومان‌ها به این متد ممکن است برای ارزیابی موفقیت‌آمیز آن‌ها ضروری باشد.

اگر هیچ‌کدام از owner، globals، locals یا type_params ارائه نشده باشند و ForwardRef حاوی اطلاعاتی درباره خاستگاه خود نباشد، از دیکشنری‌های خالی globals و locals استفاده می‌شود.

اضافه شده در نسخه‌ی 3.14.

توابع

annotationlib.annotations_to_string(annotations)

یک دیکشنری حاشیه‌نویسی‌ها را که حاوی مقادیر ران‌تایم است، به دیکشنری‌ای تبدیل می‌کند که فقط شامل رشته‌ها است. اگر مقادیر از قبل رشته نباشند، با استفاده از type_repr() تبدیل می‌شوند. این به‌عنوان یک کمک‌کننده برای توابع حاشیه‌نویسی ارائه‌شده توسط کاربر در نظر گرفته شده است که از قالب STRING پشتیبانی می‌کنند اما به کدی که حاشیه‌نویسی‌ها را ایجاد می‌کند دسترسی ندارند.

For example, this is used to implement the STRING format for typing.TypedDict classes created through the functional syntax:

>>> from typing import TypedDict
>>> Movie = TypedDict("movie", {"name": str, "year": int})
>>> get_annotations(Movie, format=Format.STRING)
{'name': 'str', 'year': 'int'}

اضافه شده در نسخه‌ی 3.14.

annotationlib.call_annotate_function(annotate, format, *, owner=None)

annotate function annotate را با format داده‌شده، عضوی از شمارشی (enum) Format، فراخوانی کنید و دیکشنری حاشیه‌نویسی‌ها تولیدشده توسط تابع را برگردانید.

این تابع کمکی مورد نیاز است، زیرا توابع annotate (annotate functions) تولیدشده توسط کامپایلر برای توابع، کلاس‌ها و ماژول‌ها، هنگام فراخوانی مستقیم تنها از قالب VALUE پشتیبانی می‌کنند. برای پشتیبانی از سایر قالب‌ها، این تابع، تابع annotate (annotate function) را در محیط ویژه‌ای فراخوانی می‌کند که به آن امکان می‌دهد حاشیه‌نویسی‌هارا در سایر قالب‌ها تولید کند. این یک جزء سازنده‌ی مفید در پیاده‌سازی قابلیتی است که نیاز دارد حاشیه‌نویسی‌ها را در حالی که یک کلاس در حال ساخته‌شدن است به‌طور جزئی ارزیابی کند.

owner شیءای است که تابع حاشیه‌نویسی‌ها به آن تعلق دارد و معمولاً یک تابع، کلاس یا ماژول است. در صورت ارائه، از آن در قالب FORWARDREF برای ایجاد یک شیء ForwardRef حاوی اطلاعات بیشتر استفاده می‌شود.

همچنین ملاحظه نمائید

PEP 649 شامل توضیحی درباره‌ی روش پیاده‌سازی به‌کاررفته در این تابع است.

اضافه شده در نسخه‌ی 3.14.

annotationlib.call_evaluate_function(evaluate, format, *, owner=None)

evaluate function یعنی evaluate را با format داده‌شده، که عضوی از enum Format است، فراخوانی می‌کند و مقدار تولیدشده توسط تابع را برمی‌گرداند. این تابع مشابه call_annotate_function() است، اما آن تابع همیشه دیکشنری‌ای را برمی‌گرداند که رشته‌ها را به annotationها نگاشت می‌کند، در حالی که این تابع یک مقدار واحد را برمی‌گرداند.

این برای استفاده با توابع ارزیابی تولیدشده برای عناصری که با تأخیر ارزیابی می‌شوند و به نام‌های مستعار نوع و پارامترهای نوع مربوط هستند، در نظر گرفته شده است:

owner شیءای است که تابع evaluate به آن تعلق دارد، مانند شیء نام مستعار نوع یا شیء متغیر نوع.

format می‌تواند برای کنترل قالبی که مقدار در آن برگردانده می‌شود، استفاده شود:

>>> type Alias = undefined
>>> call_evaluate_function(Alias.evaluate_value, Format.VALUE)
Traceback (most recent call last):
...
NameError: name 'undefined' is not defined
>>> call_evaluate_function(Alias.evaluate_value, Format.FORWARDREF)
ForwardRef('undefined')
>>> call_evaluate_function(Alias.evaluate_value, Format.STRING)
'undefined'

اضافه شده در نسخه‌ی 3.14.

annotationlib.get_annotate_from_class_namespace(namespace)

annotate function را از دیکشنری فضای نام کلاس namespace بازیابی می‌کند. اگر فضای نام شامل تابع annotate نباشد، None را بازمی‌گرداند. این موضوع عمدتاً پیش از آنکه کلاس به‌طور کامل ایجاد شده باشد (مثلاً در یک فراکلاس) مفید است؛ پس از ایجاد کلاس، می‌توان تابع annotate را با cls.__annotate__ بازیابی کرد. برای نمونه‌ای از استفاده از این تابع در فراکلاس، زیر را ببینید.

اضافه شده در نسخه‌ی 3.14.

annotationlib.get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)

دیکشنری حاشیه‌نویسی‌های یک شیء را محاسبه کنید.

obj می‌تواند یک شیء فراخوانی‌پذیر، کلاس، ماژول، یا شیء دیگری دارای ویژگی‌های __annotate__ یا __annotations__ باشد. اگر هر شیء دیگری داده شود، TypeError پرتاب می‌شود.

پارامتر format قالبی را که حاشیه‌نویسی‌ها در آن بازگردانده می‌شوند کنترل می‌کند، و باید عضوی از enum Format یا معادل عدد صحیح آن باشد. قالب‌های مختلف به‌صورت زیر عمل می‌کنند:

  • VALUE: ابتدا object.__annotations__ امتحان می‌شود؛ اگر وجود نداشته باشد، در صورت وجود داشتن تابع object.__annotate__ فراخوانی می‌شود.

  • FORWARDREF: اگر object.__annotations__ وجود داشته باشد و بتوان آن را با موفقیت ارزیابی کرد، از آن استفاده می‌شود؛ در غیر این صورت، تابع object.__annotate__ فراخوانی می‌شود. اگر این نیز وجود نداشته باشد، بار دیگر برای object.__annotations__ تلاش می‌شود و هر خطایی که از دسترسی به آن حاصل شود، دوباره پرتاب می‌شود.

    • هنگام فراخوانی object.__annotate__، ابتدا با FORWARDREF فراخوانی می‌شود. اگر این قالب پیاده‌سازی نشده باشد، سپس بررسی می‌شود که آیا VALUE_WITH_FAKE_GLOBALS پشتیبانی می‌شود یا خیر و در صورت پشتیبانی، از آن در محیط سراسری جعلی استفاده می‌شود. اگر هیچ‌کدام از این قالب‌ها پشتیبانی نشود، به استفاده از VALUE بازمی‌گردد. اگر VALUE با شکست مواجه شود، خطای حاصل از این فراخوانی پرتاب می‌شود.

  • STRING: اگر object.__annotate__ وجود داشته باشد، ابتدا فراخوانی می‌شود؛ در غیر این صورت، از object.__annotations__ استفاده می‌شود و با استفاده از annotations_to_string() به رشته تبدیل می‌شود.

    • هنگام فراخوانی object.__annotate__، ابتدا با STRING فراخوانی می‌شود. اگر این قالب پیاده‌سازی نشده باشد، سپس بررسی می‌شود که آیا VALUE_WITH_FAKE_GLOBALS پشتیبانی می‌شود یا خیر و در صورت پشتیبانی، از آن در محیط سراسری جعلی استفاده می‌شود. اگر هیچ‌کدام از این قالب‌ها پشتیبانی نشوند، به استفاده از VALUE بازمی‌گردد و نتیجه با استفاده از annotations_to_string() تبدیل می‌شود. اگر VALUE با شکست مواجه شود، خطای حاصل از این فراخوانی پرتاب می‌شود.

یک دیکشنری بازمی‌گرداند. get_annotations() هر بار که فراخوانی می‌شود یک دیکشنری جدید بازمی‌گرداند؛ دو بار فراخوانی آن روی یک شیء، دو دیکشنری متفاوت اما معادل بازمی‌گرداند.

این تابع چندین جزئیات را برای شما مدیریت می‌کند:

  • اگر eval_str برابر true باشد، مقادیر از نوع str با استفاده از eval() از حالت رشته‌ای خارج می‌شوند. این قابلیت برای استفاده با حاشیه‌نویسی‌های رشته‌ای‌شده (from __future__ import annotations) در نظر گرفته شده است. تنظیم eval_str روی true در قالب‌هایی غیر از Format.VALUE خطا است.

  • اگر obj دیکشنری annotations نداشته باشد، یک دیکشنری خالی برمی‌گرداند. (توابع و متدها همیشه دیکشنری annotations دارند؛ کلاس‌ها، ماژول‌ها و سایر انواع فراخوانی‌پذیر ممکن است نداشته باشند.)

  • حاشیه‌نویسی‌های موروثی کلاس‌هاو همچنین حاشیه‌نویسی‌های فراکلاس‌ها را نادیده می‌گیرد. اگر کلاسی دیکشنری حاشیه‌نویسی‌های خود را نداشته باشد، یک دیکشنری خالی برمی‌گرداند.

  • تمام دسترسی‌ها به اعضای شیء و مقادیر دیکشنری برای ایمنی با استفاده از getattr() و dict.get() انجام می‌شوند.

eval_str تعیین می‌کند که آیا مقادیر از نوع str با نتیجه‌ی فراخوانی eval() روی آن مقادیر جایگزین می‌شوند یا خیر:

  • اگر eval_str درست باشد، eval() روی مقادیری از نوع str فراخوانی می‌شود. (توجه داشته باشید که get_annotations() استثناها را مهار نمی‌کند؛ اگر eval() استثنایی پرتاب کند، باعث باز شدن پشته تا پس از فراخوانی get_annotations() خواهد شد.)

  • اگر eval_str نادرست باشد (پیش‌فرض)، مقادیر از نوع str بدون تغییر می‌مانند.

globals و locals به eval() ارسال می‌شوند؛ برای اطلاعات بیشتر، مستندات eval() را ببینید. اگر globals یا locals برابر None باشد، این تابع ممکن است، بسته به type(obj)، آن مقدار را با یک پیش‌فرض وابسته به زمینه جایگزین کند:

  • اگر obj یک ماژول باشد، globals به‌طور پیش‌فرض برابر با obj.__dict__ است.

  • اگر obj یک کلاس باشد، globals به‌طور پیش‌فرض sys.modules[obj.__module__].__dict__ است و locals به‌طور پیش‌فرض فضای نام کلاس obj است.

  • اگر obj فراخوانی‌پذیر باشد، مقدار پیش‌فرض globals برابر با obj.__globals__ است، هرچند اگر obj یک تابع پوشیده‌شده (با استفاده از functools.update_wrapper()) یا یک شیء functools.partial باشد، تا زمانی که یک تابع بدون پوشش پیدا شود، پوشش آن برداشته می‌شود.

فراخوانی get_annotations() بهترین روش برای دسترسی به دیکشنری حاشیه‌نویسی‌های هر شیء است. برای اطلاعات بیشتر درباره بهترین روش‌های حاشیه‌نویسی، بهترین شیوه‌های حاشیه‌نویسی را ببینید.

>>> def f(a: int, b: str) -> float:
...     pass
>>> get_annotations(f)
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}

اضافه شده در نسخه‌ی 3.14.

annotationlib.type_repr(value)

یک مقدار دلخواه پایتون را به قالبی مناسب برای استفاده توسط قالب STRING تبدیل می‌کند. این تابع برای بیشتر اشیاء، repr() را فراخوانی می‌کند، اما برای برخی اشیاء، مانند اشیاء نوع، مدیریت ویژه‌ای دارد.

این به‌عنوان یک ابزار کمکی برای توابع حاشیه‌نویسی ارائه‌شده توسط کاربر در نظر گرفته شده است که از قالب STRING پشتیبانی می‌کنند، اما به کدی که حاشیه‌نویسی‌هارا ایجاد می‌کند دسترسی ندارند. همچنین می‌توان از آن برای فراهم کردن یک نمایش رشته‌ای کاربرپسند برای اشیاء دیگری استفاده کرد که حاوی مقادیری هستند که معمولاً در حاشیه‌نویسی‌ها با آن‌ها مواجه می‌شوید.

اضافه شده در نسخه‌ی 3.14.

دستور پخت‌ها

استفاده از حاشیه‌نویسی‌ها در یک فراکلاس

ممکن است یک فراکلاس بخواهد در جریان ایجاد کلاس، حاشیه‌نویسی‌های موجود در بدنه کلاس را بررسی یا حتی اصلاح کند. این کار مستلزم بازیابی حاشیه‌نویسی‌ها از دیکشنری فضای نام کلاس است. برای کلاس‌هایی که با from __future__ import annotations ایجاد شده‌اند، حاشیه‌نویسی‌ها در کلید __annotations__ دیکشنری قرار خواهند داشت. برای سایر کلاس‌های دارای حاشیه‌نویسی، می‌توان از get_annotate_from_class_namespace() برای دریافت تابع حاشیه‌نویسی استفاده کرد و می‌توان از call_annotate_function() برای فراخوانی آن و بازیابی حاشیه‌نویسی‌ها استفاده کرد. استفاده از قالب FORWARDREF معمولاً بهترین حالت است، زیرا این امر به حاشیه‌نویسی‌ها اجازه می‌دهد به نام‌هایی ارجاع دهند که هنوز در زمان ایجاد کلاس قابل حل نیستند.

برای تغییر حاشیه‌نویسی‌ها، بهتر است یک تابع annotate پوششی ایجاد کنید که تابع annotate اصلی را فراخوانی کند، تنظیمات لازم را اعمال کند و نتیجه را برگرداند.

در زیر مثالی از یک فراکلاس آمده است که تمام حاشیه‌نویسی‌های typing.ClassVar را از کلاس جدا می‌کند و آن‌ها را در یک ویژگی جداگانه قرار می‌دهد:

import annotationlib
import typing

class ClassVarSeparator(type):
   def __new__(mcls, name, bases, ns):
      if "__annotations__" in ns:  # from __future__ import annotations
         annotations = ns["__annotations__"]
         classvar_keys = {
            key for key, value in annotations.items()
            # Use string comparison for simplicity; a more robust solution
            # could use annotationlib.ForwardRef.evaluate
            if value.startswith("ClassVar")
         }
         classvars = {key: annotations[key] for key in classvar_keys}
         ns["__annotations__"] = {
            key: value for key, value in annotations.items()
            if key not in classvar_keys
         }
         wrapped_annotate = None
      elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
         annotations = annotationlib.call_annotate_function(
            annotate, format=annotationlib.Format.FORWARDREF
         )
         classvar_keys = {
            key for key, value in annotations.items()
            if typing.get_origin(value) is typing.ClassVar
         }
         classvars = {key: annotations[key] for key in classvar_keys}

         def wrapped_annotate(format):
            annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
            return {key: value for key, value in annos.items() if key not in classvar_keys}

      else:  # no annotations
         classvars = {}
         wrapped_annotate = None
      typ = super().__new__(mcls, name, bases, ns)

      if wrapped_annotate is not None:
         # Wrap the original __annotate__ with a wrapper that removes ClassVars
         typ.__annotate__ = wrapped_annotate
      typ.classvars = classvars  # Store the ClassVars in a separate attribute
      return typ

ایجاد یک تابع حاشیه‌نویسی سفارشی و فراخوانی‌پذیر

توابع annotate سفارشی می‌توانند توابع واقعی (literal functions) باشند، مانند آن‌هایی که به‌طور خودکار برای توابع، کلاس‌ها و ماژول‌ها تولید می‌شوند. یا ممکن است بخواهید از کپسوله‌سازی فراهم‌شده توسط کلاس‌ها استفاده کنید، که در این صورت هر callable می‌تواند به‌عنوان یک annotate function استفاده شود.

برای ارائه مستقیم قالب‌های VALUE، STRING یا FORWARDREF، یک annotate function باید ویژگی زیر را فراهم کند:

  • یک __call__ فراخوانی‌پذیر با امضای __call__(format, /) -> dict، که هنگام فراخوانی با یک قالب پشتیبانی‌شده، NotImplementedError پرتاب نمی‌کند.

برای فراهم کردن قالب VALUE_WITH_FAKE_GLOBALS، که برای تولید خودکار STRING یا FORWARDREF در صورتی که به‌طور مستقیم پشتیبانی نشوند استفاده می‌شود، توابع annotate باید ویژگی‌های زیر را فراهم کنند:

  • یک __call__ فراخوانی‌پذیر با امضای __call__(format, /) -> dict، که هنگام فراخوانی با VALUE_WITH_FAKE_GLOBALS استثنای NotImplementedError را پرتاب نمی‌کند.

  • یک شیء کد __code__ که شامل کد کامپایل‌شده برای تابع annotate است.

  • اختیاری: یک تاپل از پیش‌فرض‌های جایگاهی تابع __kwdefaults__، اگر تابع نشان‌داده‌شده توسط __code__ از هرگونه پیش‌فرض جایگاهی استفاده کند.

  • اختیاری: یک دیکشنری از مقادیر پیش‌فرض کلیدواژه‌ای تابع __defaults__، اگر تابع نشان‌داده‌شده با __code__ از مقادیر پیش‌فرض کلیدواژه‌ای استفاده کند.

  • اختیاری: همه‌ی ویژگی‌های دیگر تابع.

class Annotate:
    called_formats = []

    def __call__(self, format=None, /, *, _self=None):
        # When called with fake globals, `_self` will be the
        # actual self value, and `self` will be the format.
        if _self is not None:
            self, format = _self, self

        self.called_formats.append(format)
        if format <= 2:  # VALUE or VALUE_WITH_FAKE_GLOBALS
            return {"x": MyType}
        raise NotImplementedError

    __code__ = __call__.__code__
    __defaults__ = (None,)
    __kwdefaults__ = property(lambda self: dict(_self=self))

    __globals__ = {}
    __builtins__ = {}
    __closure__ = None

سپس می‌توان آن را به این صورت فراخوانی کرد:

>>> from annotationlib import call_annotate_function, Format
>>> call_annotate_function(Annotate(), format=Format.STRING)
{'x': 'MyType'}

یا به‌عنوان تابع حاشیه‌نویسی برای یک شیء استفاده شود:

>>> from annotationlib import get_annotations, Format
>>> class C:
...   pass
>>> C.__annotate__ = Annotate()
>>> get_annotations(Annotate(), format=Format.STRING)
{'x': 'MyType'}

محدودیت‌های قالب STRING

قالب STRING برای تقریب کد منبع حاشیه‌نویسی در نظر گرفته شده است، اما راهبرد پیاده‌سازی به‌کاررفته باعث می‌شود که همیشه نتوان کد منبع دقیق را بازیابی کرد.

اول، رشته‌ساز (stringifier) البته نمی‌تواند هیچ‌گونه اطلاعاتی را که در کد کامپایل‌شده وجود ندارد، از جمله کامنت‌ها، فضای سفید، پرانتزگذاری و عملیاتی که توسط کامپایلر ساده‌سازی می‌شوند، بازیابی کند.

دوم، رشته‌ساز (stringifier) می‌تواند تقریباً تمام عملیاتی را که شامل نام‌های جستجوشده در محدوده‌ای هستند، رهگیری کند، اما نمی‌تواند عملیاتی را که به‌طور کامل روی ثابت‌ها عمل می‌کنند، رهگیری کند. به‌عنوان یک نتیجه، این همچنین به این معناست که درخواست قالب STRING برای کد غیرقابل‌اعتماد ایمن نیست: پایتون آن‌قدر قدرتمند است که حتی بدون دسترسی به هیچ‌یک از سراسری‌ها یا توکارها نیز می‌توان به اجرای کد دلخواه دست یافت. برای مثال:

>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
...
>>> annotationlib.get_annotations(f, format=annotationlib.Format.STRING)
Hello world
{'x': 'None'}

توجه

این مثال خاص در زمان نگارش کار می‌کند، اما به جزئیات پیاده‌سازی متکی است و تضمینی برای کار کردن آن در آینده وجود ندارد.

در میان انواع مختلف عبارت‌هایی که در پایتون وجود دارند و توسط ماژول ast نمایش داده می‌شوند، برخی از عبارت‌ها پشتیبانی می‌شوند، به این معنا که قالب STRING عموماً می‌تواند کد منبع اصلی را بازیابی کند؛ برخی دیگر پشتیبانی نمی‌شوند، به این معنا که ممکن است به خروجی نادرست یا خطا منجر شوند.

موارد زیر پشتیبانی می‌شوند (گاهی با ملاحظات):

موارد زیر پشتیبانی نمی‌شوند، اما هنگامی که رشته‌ساز (stringifier) با آن‌ها مواجه شود، خطای گویایی پرتاب می‌کنند:

  • ast.FormattedValue (اف‌استرینگ‌ها؛ در صورت استفاده از مشخص‌کننده‌های تبدیل مانند !r، خطا تشخیص داده نمی‌شود)

  • ast.JoinedStr (اف‌استرینگ‌ها)

موارد زیر پشتیبانی نمی‌شوند و منجر به خروجی نادرست می‌شوند:

موارد زیر در محدوده‌های حاشیه‌نویسی (annotation scopes) مجاز نیستند و بنابراین مرتبط نیستند:

محدودیت‌های قالب FORWARDREF

قالب FORWARDREF هدف دارد تا حد امکان مقادیر واقعی تولید کند، به‌طوری که هر چیزی که قابل حل نباشد، با اشیای ForwardRef جایگزین می‌شود. این قالب تقریباً همان محدودیت‌های قالب STRING را دارد: حاشیه‌نویسی‌هایی که عملیاتی را روی مقادیر لفظی انجام می‌دهند یا از انواع عبارت پشتیبانی‌نشده استفاده می‌کنند، ممکن است هنگام ارزیابی با قالب FORWARDREF استثنا پرتاب کنند.

در زیر چند مثال از رفتار در مواجهه با عبارت‌های پشتیبانی‌نشده آمده است:

>>> from annotationlib import get_annotations, Format
>>> def zerodiv(x: 1 / 0): ...
>>> get_annotations(zerodiv, format=Format.STRING)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> get_annotations(zerodiv, format=Format.FORWARDREF)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> def ifexp(x: 1 if y else 0): ...
>>> get_annotations(ifexp, format=Format.STRING)
{'x': '1'}

پیامدهای امنیتی درون‌نگری حاشیه‌نویسی‌ها

بخش زیادی از قابلیت‌های این ماژول شامل اجرای کد مرتبط با حاشیه‌نویسی‌ها می‌شود؛ کدی که سپس می‌تواند هر کار دلخواهی انجام دهد. برای مثال، get_annotations() ممکن است یک annotate function دلخواه را فراخوانی کند و ForwardRef.evaluate() ممکن است eval() را روی یک رشته دلخواه فراخوانی کند. کد موجود در یک حاشیه‌نویسی ممکن است فراخوانی‌های سیستمی دلخواهی انجام دهد، وارد یک حلقه بی‌پایان شود یا هر عملیات دیگری را انجام دهد. این موضوع برای هرگونه دسترسی به ویژگی __annotations__ و برای توابع مختلفی در ماژول typing که با حاشیه‌نویسی‌ها کار می‌کنند، مانند typing.get_type_hints() نیز صادق است.

هر مشکل امنیتی ناشی از این موضوع، بلافاصله پس از ایمپورت کدی که ممکن است حاوی حاشیه‌گذاری‌های غیرقابل‌اعتماد باشد نیز صدق می‌کند: ایمپورت کد همیشه می‌تواند منجر به اجرای عملیات دلخواه شود. با این حال، پذیرفتن رشته‌ها یا سایر ورودی‌ها از یک منبع غیرقابل‌اعتماد و ارسال آن‌ها به هر یک از APIهای مربوط به درون‌نگری حاشیه‌گذاری‌ها ناامن است، برای مثال با ویرایش یک دیکشنری __annotations__ یا ایجاد مستقیم یک شیء ForwardRef.