یکی از امکانات جالب پایتون دکوریتورها (Decorators) هست؛ که به طور ساده و خلاصه یک تابع هست که در ورودی یک تابع دیگر را گرفته، روی آن اعمالی انجام میدهد و مقداری که باز میگرداند با نام همان تابع ذخیره میشود. اگر با FastAPI کار کرده باشید حتما با این کد آشنایید:
from fastapi import FastAPI,
app = FastAPI()
@app.get("/")
def hello_world():
return {"Hello": "World"}
یا برای فلسک (Flask) داریم:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
تو این مطلب میخوام بگم جریان این علامت @
چی هست و چه کارایی میتونید باهاش بکنید.
یک دکوریتور چطور کار میکنه
همونطور که قبلا گفتم دکوریتور یک تابع هست که یک پارامتر میگرد و یک مقدار بر میگرداند. خب، با این تعریف که هرچی تابع تک پارامتری داریم دکوریتور میشه! درسته، درواقع چیزی که تفاوت ایجاد میکنه نوع عملکرد علامت @ هست.
نمونه برنامه اول: ایجاد رابط کاربری متنی
فرض کنید برای یک رابط کاربری متنی (CLI) در برنامهتان میخواهید یک دیکشنری (Dictionary) با کلید نام تابع و مقدار تابع کنترل کننده (Handler) داشته باشید. تا بتوانید از آن به این شکل استفاده کنید:
all_commands = {}
...
while True:
line = input("> ")
cmd, *args = line.split()
if cmd not in all_commands:
print("Invalid Command")
continue
all_commands[cmd](*args) # Call the function and send input words (except first) as argument
حالا کد بالا یک خط از کاربر میگیره و با توجه به اولین کلمه در اون خط اگر در کلیدهای دیکشنری بود تابعی که بهش داده بودیم رو روی مابقی کلمات اون خط صدا میزنه مثلا میتونیم توی دیکشنری این رو داشته باشیم:
all_commands["echo"] = print
اینطوری اگه یک نفر بنویسه echo hello world
برنامه بهش hello world
رو نمایش میده. حالا برای توابع بزرگتر باید چیکار کنیم؟ فرض کنید میخواهیم یک دستور (تابع) به نام calc اضافه کنیم تا بتونه محاسبههای ساده رو انجام بده. این تابع به شکل زیر پیادهسازی میشه:
all_commands = {}
def calc(l=None, op=None, r=None):
try:
l, r = int(l), int(r)
except:
print("SyntaxError\n\n usage: calc 2 + 5\nspaces are important")
return
operators = {
"+": lambda: l + r,
"-": lambda: l - r,
"*": lambda: l * r,
"/": lambda: l / r,
"//": lambda: l // r,
}
if op not in operators:
print("operator '%s' is not supported" % op)
return
print(operators[op]())
while True:
line = input("> ")
cmd, *args = line.split()
اگه یک دکوریتور مناسب داشتیم میتونستیم مثل کد پایین خیلی خوشگل و شیک این تابع رو هم به دستورات برنامهمون اضافه کنیم:
@command
def calc(l=None, op=None, r=None):
...
این همه مقدمه چیدم که اینجا بگم چطور بیاییم و برای این نمونه برنامه دکوریتوری به اسم command
بسازیم که هر تابعی که خواستیم به دستورات اضافه کنیم قبلش اون رو بنویسیم و کار رو انجام بده:
all_commands = {}
def command(func: callable) -> callable:
all_commands[func.__name__] = func
return func
@command
def calc(l=None, op=None, r=None):
...
به همین سادگی! گفتم که دکوریتور یه تابعه! تابع command
که به همراه راهنمای نوع ورودی و خروجیهاش (type hint) نوشتمش، میاد و یک شی صدازدنی (یطورایی همون تابع خودمون!) رو میگیره و همون رو بر میگردونه. تو پایتون مثلا زمانی که این دکوریتور رو واسه calc
استفاده میکنیم تابع رو در پارامتر به command
میده و چیزی که command
بر میگردونه رو به اسم calc
ذخیره میکنه. (یعنی اگه command چیزی بر نمیگردوند calc
مقدار None
میگرفت) توی تابع command
اومدیم تابعای رو که توی پارامتر میاد رو توی دیکشنری با اسمش ذخیره کردیم.
کد ما تا اینجای کار این شکلیه:
all_commands = {"echo": print}
def command(func: callable) -> callable:
all_commands[func.__name__] = func
return func
@command
def calc(l=None, op=None, r=None):
try:
l, r = int(l), int(r)
except:
print("SyntaxError\n\n usage: calc 2 + 5\nspaces are important")
return
operators = {
"+": lambda: l + r,
"-": lambda: l - r,
"*": lambda: l * r,
"/": lambda: l / r,
"//": lambda: l // r,
}
if op not in operators:
print("operator '%s' is not supported" % op)
return
print(operators[op]())
while True:
line = input("> ")
cmd, *args = line.split()
if cmd not in all_commands:
print("Invalid Command")
continue
all_commands[cmd](
*args
) # Call the function and send input words (except first) as argument
دکوریتور های پیشرفتهتر!
نمیدونم متوجه فرق دکوریتور ما با اون مثال های اولی که آوردم شدید یا نه، برای دکوریتور هایی که تو مثال های flask و fastapi داشتیم مثلا مینوشتیم @app.get("/")
اما تو دکوریتور خودمون فقط داریم مینویسیم @command
جریان اینه که بعضی مواقع تو دکوریتور نیاز داریم که پارامتر های دیگهای هم بگیریم، مثلا فرض کنید تو برنامه بالایی میخوایی بگیم برای اضافه کردن یه دستور به دیکشنریمون میخواییم یه اسم دلخواه و شاید متفاوت با اسم خود تابع اضافه کنیم. (اول این رو میگم و بعد میرسیم به این که چطور تابعی بسازیم که در جاهای مختلف هم اسم خود تابع رو بذاره و هم اسم دلخواه)
مثلا چنین چیزی میخواییم:
@command("for")
def for_function(start=None, end=None):
try:
start, end = int(start), int(end)
except:
print("SyntaxError\n\n usage: for 2 5\nspaces are important")
for i in range(start, end):
print(i)
مثلا خواستیم دستور for
رو اضافه کنیم ولی چون از کلمه کلیدی های پایتونه نمیتونیم.
برای چنین مواقعی تابعی که برای command
تعریف میکنیم وقتی صدا زده میشه یه تابع دیگه بر میگردونه و پایتون تابع هدف رو به تابع برگردونده شده میده. برای همین تابع command اینطوری میشه:
def command(name: str) -> callable:
def decorator(func:callable):
all_commands[name] = func
return func
return decorator
جریان اینه که خود تابع command
یه رشته (string) در پارامتر به عنوان اسم دستور میگیره و از اونجا که تو پایتون میشه همینطور تابع تو دل تابع نوشت! یه تابع تعریف میکنه که تابعی که میگیره رو به کلید name
تو دیکشنری ذخیره کنه و خود تابع رو مثل قبل برگردونه. تابع command
هم وقتی صدا زده میشه تابعی که تو دل خودشه رو بر میگردونه.
دکوریتور ترکیبی
همونطور که قول دادم! اینجا دکوریتوری مینویسم که هر دو حالت بشه ازش استفاده کرد. البته روشهای مختلفی هست ولی این یک مدلشه:
def command(f: str) -> callable:
def decorator(func: callable):
all_commands[name] = func
return func
if isinstance(f, str):
name = f # `f` is a string so it is the name
return decorator
else:
# `f` is not a string so may be it's a callable
name = f.__name__
# this code may raise AttributeError,
# but this is a simple example, isn't?!
return decorator(f)
تو این کد چک میکنیم ببینیم پارامتری که به command
اومده از چه جنسیه، اگه رشته بود پس اسم دستور هست و اگر نه خود تابع دستور هست. در هر دو صورت اسم دستور با متغیر name
تعیین میشه، وقتی رشته بود name
برابر میشه با رشته داده شده و خود تابع داخلیه فرستاده میشه؛ چون توی کد این تابع به شکل @command("example")
صدا زده شده، پس باید یه تابع تحویل پایتون بده که پایتون تابع هدف رو به اون بفرسته. در غیر این صورت احتمالا پارامتره یه تابعه (چون این دکوریتور رو برای خودمون مینویسیم زیاد پیچیدهش نکردم که همه چیز رو چک کنه!) پس به صورت @command
صدا زده شده و یه تابع بهش داده شده، مثل قبل اسم تابع رو تو name
ذخیره کنه و خودش تابع توی دلش رو روی این تابعی که گرفته صدا بزنه
نمونه برنامه دوم: استفاده از @property
در پایتون
یادش بخیر، اون زمان ها که من C#
کد میزدم یه چیز باحالی داشت به نام property بعدا که اومدم سراغ پایتون دنبالش گشتم و دیدم پایتون هم یه چیزی مشابهاش داره ولی از دکوریتور براش استفاده میکنه. جریان چیه حالا!
این property تو کلاسها هست و یکجور فیلده که موقع گرفتن مقدار یا ذخیره مقدار توش در واقع یه تابع صدا زده میشه. قبلا در مورد dataclass گفتم، اینجا برای این مثال ازش استفاده میکنم تا مجبور نباشم تابع __init__
رو بنویسم، دوست داشتید اون مطلبم رو هم بخونید.
from dataclasses import dataclass
from datetime import date, datetime
@dataclass
class Person:
name: str
birth_date: date
@property
def age(self):
return (date.today() - self.birth_date).days // 365
حالا اگه یه نمونه از این کلاس بسازیم میتونیم فیلد area رو مثل بقیه فیلد ها بگیریم ولی هر بار این فیلد با توجه به دو مقدار ارتفاع و عرض محاسبه میشه:
me = Person("Behnam", date(2000, 9, 29))
print(me.age)
حالا اگه بخواییم این property(خصوصیت) مقدار هم بگیره میتونیم براش یه setter
مشخص کنیم:
from dataclasses import dataclass
from datetime import date, timedelta
@dataclass
class Person:
name: str
birth_date: date
@property
def age(self):
return (date.today() - self.birth_date).days // 365
@age.setter
def age(self):
self.birth_date = date.today() + timedelta(value*365)