آشنایی با مفهوم Function Annotation در زبان برنامه‌نویسی پایتون


یکی از ویژگی‌های زبان برنامه‌نویسی پایتون که از نسخۀ 3.0 به بعد به این زبان افزوده شده است، مفهومی می‌باشد تحت عنوان Function Annotation که این امکان را برای دولوپرها فراهم می‌کند تا بتوانند توضیحات مورد نظر و همچنین نوع دادۀ مربوط به آرگومان ورودی، دستورات داخلی آن و همچنین مقدار بازگشتی مورد انتظار خود را اضافه نمایند.

به طور کلی، قابلیت Function Annotation در زبان برنامه‌نویسی پایتون به منظور افزودن توضیحاتی در رابطه با لزوم استفاده از دیتا تایپ خاصی برای آرگومان‌های ورودی، نوع مقدار بازگشتی مورد انتظار از فانکشن، نحوۀ عملکرد دستورات داخلی فانکشن مورد نظر و اطلاعاتی از این دست به سورس‌کد مورد استفاده قرار می‌گیرد تا بدین طریق دولوپرها بتوانند بسته به نوع اصطلاحاً Annotation به کار رفته در سورس‌کد، خطاهای موجود در برنامه را پیش از اجرای آن شناسایی کرده و رفع نمایند.

از سوی دیگر، استفاده از مفهوم Function Annotation در توسعه و پیاده‌سازی لایبرری‌های به اصطلاح Third-party منجر بدین می‌گردد تا بتوان به سادگی نحوۀ کار لایبرری مذکور و همچنین نوع آرگومان‌ها، متدها، کلاس‌ها و ماژول‌های تعریف‌شده در آن و کاربرد هر یک را درک کرده و مورد استفاده قرار داد.

نکته
توضیحات اضافه‌شده به سورس‌کد در قالب Annotation در روند اجرای برنامه بی‌تأثیر بوده و استفاده از این قابلیت در زبان برنامه‌نویسی پایتون کاملاً اختیاری می‌باشد.

حال پس از آشنایی با مفهوم Function Annotation، در ادامه به تشریح نحوۀ به‌کارگیری این قابلیت در قالب چند مثال کاربردی می‌پردازیم و توجه داشته باشیم که واژۀ «expression» در تمامی سینتکس‌های به کار رفته در این آموزش بر تعریف توضیحات مورد نظر و همچنین نوع دادۀ مورد انتظار اشاره دارد.

به‌کارگیری مفهوم Annotation جهت تعریف Type پارامترهای ورودی فانکشن‌

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

def myFunction(a: expression, b: expression):
    # Tasks of function

در سینتکس فوق، به منظور استفاده از مفهوم Annotation برای پارامترهای ورودی در تعریف فانکشن مد نظر ابتدا نام پارامتر ورودی را نوشته و به دنبال آن یک علامت : قرار داده سپس توضیحات یا نوع دادۀ مورد نظر را درج می‌نماییم که برای درک بهتر این موضوع داریم:

>>> def multiply(a: int, b: 'annotating b', c: int):
        print(a * b * c)
>>> multiply(1, 2, 3)
6

در کد فوق، فانکشنی تحت عنوان ()multiply با سه پارامتر ورودی تحت عناوین b ،a و c تعریف کرده‌ایم که در آن گفته‌ایم جهت دستیابی به عملکرد مورد انتظار از فانکشن در راستای ضرب اعداد صحیح، آرگومان ورودی منتسب به متغیر a در هنگام فراخوانی فانکشن می‌باید از نوع دادۀ int یا عدد صحیح باشد و در ادامه برای پارامتر دوم استرینگی دلخواه در قالب عبارت «annotating b» درج نموده‌ایم که به عنوان مثال می‌توان در چنین مواردی توضیحی در رابطه با ماهیت و همچنین نحوۀ عملکرد پارامتر ورودی به سورس‌کد اضافه کرد.

در واقع، توضیحاتی که در قالب Annotation به سورس‌کد افزوده می‌شوند به عنوان جایگزینی برای توضیحات ارائه‌شده در داکیومنت برنامۀ مورد نظر بوده و بدین طریق دولوپرها می‌توانند بدون مراجعه به داکیومنت مربوطه اطلاعاتی در رابطه با نحوۀ کارکرد پارامتر ورودی، متد یا کلاس مد نظر به دست آورند. در نهایت، نوع دادۀ مورد انتظار برای آرگومان ورودی سوم را در مقابل پارامتر ورودی متناظر آن درج کرده‌ایم بدین معنی که در هنگام فراخوانی فانکشن ()multiply مجاز به پاس دادن آرگومانی از نوع دادۀ int به این فانکشن هستیم. در سطر بعد هم دستورات داخلی فانکشن را با رعایت تورفتگی نوشته‌ایم که در آن گفته‌ایم آرگومان‌های ورودی به فانکشن را در هم ضرب کرده و توسط فانکشن ()print در خروجی چاپ کند سپس فانکشن مذکور را با سه آرگومان ورودی فراخوانی کرده‌ایم و می‌بینیم که خروجی حاصل از آن عدد 6 می‌باشد.

برای مثال، فانشکن ()multiply را مجدداً فراخوانی می‌کنیم اما این بار آرگومان‌هایی با دیتا تایپی متفاوت از اطلاعات ارائه‌شده در قالب Function Annotation را به این فانکشن پاس می‌دهیم که برای این منظور کدی مانند زیر خواهیم داشت:

>>> multiply('a', 2, 3)
aaaaaa

همان‌طور که می‌بینیم، فانکشن ()multiply را فراخوانی کرده‌ و آرگومان ورودی اول را مقداری از جنس استرینگ به صورت «a» انتخاب نموده‌ایم که در چنین شرایطی انتظار داریم تا فانکشن مد نظر یک مقدار با دیتا تایپ استرینگ را در دو مقدار از جنس عدد صحیح ضرب کند و می‌بینیم که استرینگ مذکور 6 مرتبه تکرار شده و در قالب دنباله‌ای متشکل از استرینگ مذکور در خروجی ریترن شده است. در چنین شرایطی، بدیهی است که خروجی ریترن‌شده از فانکشن ()multiply یک مقدار عددی نمی‌باشد بدین معنی که فانکشن مذکور عملکرد مورد انتظار از آن را ارائه نمی‌کند. حال اگر در فراخوانی فانکشن ()multiply آرگومان ورودی سوم را مقداری از جنس عدد اعشاری به این فانکشن پاس دهیم، خواهیم داشت:

>>> multiply('a', 2, 3.0)
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    multiply('a', 2, 3.0)
  File "<pyshell#2>", line 2, in multiply
    print(a * b * c)
TypeError: can not multiply sequence by non-int of type 'float'

در کد فوق، آرگومان ورودی سوم را یک عدد اعشاری به صورت 3.0 انتخاب نموده‌ایم و می‌بینیم که فراخوانی فانکشن ()multiply با چنین آرگومان‌هایی منجر به بروز ارور شده است بدین صورت که استرینگ «a» ابتدا در عدد 2 ضرب شده سپس در قالب دنباله‌ای به صورت «aa» در عدد اعشاریِ 3.0 ضرب می‌شود که چنین عملیاتی منجر به بروز اروری مبنی بر عدم امکان ضرب دنباله‌ای در عدد اعشاری می‌گردد.

به‌کارگیری مفهوم Annotation جهت تعریف Type برای پارامترهای ورودی متغیر در فانکشن‌

همان‌طور که در آموزش آشنایی با نحوۀ تعریف و فراخوانی فانکشن‌هایی با تعداد پارامترهای ورودی متغیر در پایتون اشاره کردیم، استفاده از علائم * و ** در کنار شناسۀ مربوط به پارامتر ورودی فانکشن امکان اِعمال تعداد آرگومان‌های ورودی متغیر را به هنگام فراخوانی فانکشن مربوطه برای دولوپرها فراهم می‌کنند که به منظور تعریف نوع داده یا توضیحات مد نظر برای هر یک از آرگومان‌های ورودی می‌توان سینتکس کلی زیر را مد نظر قرار داد:

>>> def myFunction(*arguments: expression, **keywordedargs: expression):
        # Tasks of function

در سینتکس فوق، مشابه مثال پیشین عمل کرده و بدین طریق دیتای مورد نظر خود را می‌توانیم به سورس‌کد مربوط به فانکشن مورد نظر اضافه نماییم. در همین راستا، برای ارائۀ مثالی ساده از به‌کارگیری مفهوم Annotation در فانکشن‌هایی با پارامترهای ورودی متغیر، کدی مانند زیر خواهیم داشت:

>>> def myFunction(*a: 'list of arguments', **b: 'dict of named arguments'):
        print(a, b)
>>> myFunction('c', 1, varA = 3 )
('c', 1) {'varA': 3}

در کد فوق، فانکشنی تحت عنوان ()myFunction تعریف کرده و پارامترهای ورودی آن را به ترتیب a و b در نظر گرفته‌ایم که با استفاده از علائم * و ** در کنار نام هر یک از پارامترهای ورودی مذکور گفته‌ایم در صورت فراخوانی فانکشن ()myFunction می‌توانیم به تعداد دلخواه آرگومان ورودی به فانکشن مذکور پاس داده و همچنین برخی از آرگومان‌های ورودی را به عنوان مقداری به پارامتر ورودی متناظرشان منتسب نماییم.

همان‌طور که مشاهده می‌کنید، به منظور ارائۀ توضیحات در رابطه با ماهیت هر یک از پارامترهای a و b یک علامت : پس از شناسۀ پارامتر مد نظر قرار داده و استرینگی در قالب توضیحات مرتبط با ماهیت هر یک از آرگومان‌های ورودی به فانکشن در استفاده از علامت * و همچنین ** را در مقابل آن‌ها درج کرده‌ایم و در ادامه دستورات داخلی فانکشن را با رعایت تورفتگی نوشته‌ایم که در آن گفته‌ایم آرگومان ورودی به فانکشن را با استفاده از فانکشن ()print در خروجی چاپ کند.

در نهایت، فانکشن مذکور را فراخوانی نموده‌ایم که سه آرگومان ورودی به آن پاس داده‌ایم که عبارتند از دو آرگومان ورودی از جنس استرینگ و عدد و همچنین یک مقدار عددی که آن را به شناسۀ پارامتر ورودی مد نظر اختصاص داده‌ایم. بنابراین با فراخوانی فانکشن مذکور هر یک از آرگومان‌ها در خروجی چاپ می‌شوند و می‌بینیم که خروجی حاصل از این فراخوانی متشکل از لیستی از دو آرگومان ورودی اول و همچنین آبجکتی از جنس دیکشنری می‌باشد که در آن شناسۀ متغیر به عنوان Key و مقدار منتسب به آن به عنوان Value در نظر گرفته شده است.

به‌کارگیری مفهوم Annotation جهت تعریف Type برای مقادیر بازگشتی فانکشن‌

برای تعریف نوع دادۀ بازگشتیِ مورد انتظار از فانکشن‌ها از علامت <- در مقابل نام فانکشن و پیش از علامت : مربوط به تعریف فانکشن مد نظر استفاده کرده و نوع دادۀ مقدار بازگشتی در ادامه نوشته می‌شود که سینتکس کلی آن به صورت زیر می‌باشد:

def myFunction(a: expression, b: expression) -> expression:
    # Tasks of function

همان‌طور که مشاهده می‌کنیم، مشابه روش قبل می‌توانیم توضیحات مرتبط با پارامترهای ورودی فانکشن را درج کرده و در ادامه با قرار دادن علامت <- توضیحات مربوط به مقدار بازگشتی فانکشن یا نوع دادۀ مورد نظر را بنویسیم. به عنوان مثال، اگر بخواهیم نوع مقدار بازگشتی مورد انتظار در فانکشن ()multiply مربوط به مثال قبل را مشخص کنیم، کد مربوطه را به صورت زیر تغییر می‌دهیم:

>>> def multiply(a: int, b: 'annotating b', c: int) -> int:
        print(a * b * c)
>>> multiply(1, 2, 3)
6

در کد فوق، پس از تعریف فانکشن یک علامت <- قرار داده‌ایم و در ادامه نوع دادۀ مقدار بازگشتی مورد انتظار از آن را درج نموده سپس یک : قرار داده و در ادامه با رعایت تورفتگی دستورات مربوط به بدنۀ داخلی فانکشن مد نظر را نوشته‌ایم. بنابراین می‌توان گفت که استفاده از قابلیت Annotation جهت تعریف نوع دادۀ مربوط به مقدار بازگشتی فانکشن از این جهت انجام می‌شود که فراخوانی فانکشن مد نظر می‌باید منجر به ریترن شدن مقداری از نوع دادۀ عدد صحیح گردد چرا که در غیر این صورت ممکن است که فانکشن مد نظر در سایر قسمت‌های برنامه عملکرد مورد انتظار را نداشته باشد.

به علاوه، با استفاده از اتریبیوتی تحت عنوان __annotations__ می‌توان به تعریف توضیحات، تایپ‌ها و دیتای درج‌شده در سورس‌کد در قالب مفاهیم Annotation که در یک کلاس، متد و یا فانکشن به کار رفته‌اند دست پیدا کرد که برای این منظور کافی است تا اتریبیوت مذکور را روی آبجکت فانکشن مد نظر فراخوانی کرد به طوری که برای مثال داریم:

>>> multiply.__annotations__
{'a': int, 'b': 'annotating b', 'c': int, 'return': int}

در دستور فوق، اتریبیوت __annotations__ را روی فانکشن مورد نظر فراخوانی کرده و می‌بینیم که تمامی اَنوتیشن‌های به کار رفته در آن در قالب آبجکتی از جنس دیکشنری در خروجی ریترن می‌شود.

به طور کلی، از جمله مزایای Function Annotation نسبت به استفاده از روش درج توضیحات مربوط در داکیومنت‌های کلاس یا لایبرری مد نظر می‌توان به سادگی در اِعمال تغییرات مرتبط با داکیومنت‌های درج‌شده در سورس‌کد اشاره کرد به طوری که مثلاً در صورت نیاز به تغییر نام آرگومانی در سورس‌کد برنامه به راحتی می‌توانیم این کار را انجام دهیم اما این در حالی است که در روش درج توضیحات در داکیومنت برنامه نیاز داریم تا نام آرگومان مد نظر را در مستندات کلاس یا لایبرری مربوطه یافته و آن را تغییر دهیم.

همچنین به راحتی می‌توانیم تمامی آرگومان‌های مورد نظر خود را شناسایی کرده و توضیحات مد نظر را برای آن‌ها در داخل سورس‌کد درج کنیم در حالی که ممکن است ذکر توضیحات مربوط به برخی آرگومان‌ها در پروسۀ داکیومنتیشن فراموش شود مضاف بر اینکه به منظور دسترسی به تمامی توضیحات مندرج در داکیومنت‌ها نیاز به رعایت برخی استانداردهای مستندسازی داریم تا بتوانیم با فراخوانی برخی فانکشن‌های مربوطه داکیومنت مورد نظر خود را به اصطلاح Parse کنیم اما این در حالی است که با به‌کارگیری قابلیت Function Annotation به سادگی از طریق اتریبیوت __annotations__ به تمامی توضیحات مندرج در سورس‌کد دسترسی داریم.

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


لیست نظرات
کاربر میهمان
دیدگاه شما چیست؟
کاربر میهمان
erfan_goodarzi
erfan_goodarzi
۱۳۹۸/۰۴/۲۰
سلام.
با تشکر از آموزش خوبتون یه سؤال درمورد این مبحث داشتم.
فرق Annotation با کامنت گذاری با استفاده از # چیست؟!؟
نمیشه اینو به جای اون استفاده کرد؟!؟