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نامی است که تعریفنشده است، ارزیابی آن با قالبFORWARDREFlist[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
STRINGformat fortyping.TypedDictclasses 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ها نگاشت میکند، در حالی که این تابع یک مقدار واحد را برمیگرداند.این برای استفاده با توابع ارزیابی تولیدشده برای عناصری که با تأخیر ارزیابی میشوند و به نامهای مستعار نوع و پارامترهای نوع مربوط هستند، در نظر گرفته شده است:
typing.TypeAliasType.evaluate_value()، مقدار نامهای مستعار نوعtyping.TypeVar.evaluate_bound()، کران متغیرهای نوعtyping.TypeVar.evaluate_constraints()، محدودیتهای متغیرهای نوعtyping.TypeVar.evaluate_default()، مقدار پیشفرض متغیرهای نوعtyping.ParamSpec.evaluate_default()، مقدار پیشفرض مشخصات پارامترtyping.TypeVarTuple.evaluate_default()، مقدار پیشفرض تاپلهای متغیر نوع
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 عموماً میتواند کد منبع اصلی را بازیابی کند؛ برخی دیگر پشتیبانی نمیشوند، به این معنا که ممکن است به خروجی نادرست یا خطا منجر شوند.
موارد زیر پشتیبانی میشوند (گاهی با ملاحظات):
-
از
ast.Invert(~)،ast.UAdd(+) وast.USub(-) پشتیبانی میشوداز
ast.Not(not) پشتیبانی نمیشود
ast.Dict(بهجز هنگام استفاده از واگشایی**)ast.Call(مگر هنگام استفاده از واگشایی**)ast.Constant(البته نه نمایش دقیق ثابت؛ برای مثال، دنبالههای خنثیسازی در رشتهها از بین میروند؛ اعداد مبنای شانزده به مبنای ده تبدیل میشوند)ast.Attribute(با فرض اینکه مقدار ثابت نباشد)ast.Subscript(با فرض اینکه مقدار یک ثابت نباشد)ast.Starred(واگشایی*)
موارد زیر پشتیبانی نمیشوند، اما هنگامی که رشتهساز (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.