منابع ترجمه · The Python Tutorial · فصل ۴
ابزارهای بیشتر کنترل جریان (آموزش رسمی پایتون، فصل ۴)
منبع اصلی: The Python Tutorial, Chapter 4: More Control Flow Tools — Python Software Foundation
💡 این صفحه مثل یک نوتبوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلولهای بعدی همین صفحه در دسترس است. سلولها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلولهای قبلیِ اجرانشده را یکجا اجرا کنید.
علاوه بر دستور while که تازه معرفی شد، پایتون چند دستور دیگر هم دارد که در این فصل با آنها روبهرو خواهیم شد.
۴٫۱. دستورهای if
شاید شناختهشدهترین نوع دستور، دستور if باشد. برای مثال:
>>> x = int(input("Please enter an integer: "))
Please enter an integer: 42
>>> if x < 0:
... x = 0
... print('Negative changed to zero')
... elif x == 0:
... print('Zero')
... elif x == 1:
... print('Single')
... else:
... print('More')
...
More
میتواند صفر یا چند بخش elif وجود داشته باشد و بخش else اختیاری است. کلیدواژهٔ «elif» کوتاهشدهٔ «else if» است و برای جلوگیری از تورفتگی بیش از حد مفید است. یک دنبالهٔ if … elif … elif … جایگزینی برای دستورهای switch یا case است که در زبانهای دیگر یافت میشوند.
اگر یک مقدار را با چند ثابت مقایسه میکنید، یا نوعها یا ویژگیهای خاصی را بررسی میکنید، ممکن است دستور match را هم مفید بیابید. برای جزئیات بیشتر به دستورهای match نگاه کنید.
۴٫۲. دستورهای for
دستور for در پایتون کمی با آنچه ممکن است در C یا Pascal به آن عادت داشته باشید فرق دارد. بهجای اینکه همیشه روی یک دنبالهٔ حسابی از اعداد تکرار کند (مانند Pascal)، یا به کاربر امکان دهد هم گام تکرار و هم شرط توقف را تعریف کند (مانند C)، دستور for در پایتون روی آیتمهای هر دنبالهای (یک فهرست یا یک رشته)، به همان ترتیبی که در دنباله ظاهر میشوند، تکرار میکند. برای مثال (بدون قصد جناس):
>>> # Measure some strings:
>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words:
... print(w, len(w))
...
cat 3
window 6
defenestrate 12
کدی که یک مجموعه (collection) را در حین تکرار روی همان مجموعه تغییر میدهد، ممکن است درستنوشتنش دشوار باشد. در عوض، معمولاً سرراستتر است که روی یک کپی از مجموعه حلقه بزنید یا یک مجموعهٔ جدید بسازید:
# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}
# Strategy: Iterate over a copy
for user, status in users.copy().items():
if status == 'inactive':
del users[user]
# Strategy: Create a new collection
active_users = {}
for user, status in users.items():
if status == 'active':
active_users[user] = status
۴٫۳. تابع range()
اگر واقعاً نیاز دارید روی یک دنبالهٔ اعداد تکرار کنید، تابع درونساخت range() به کار میآید. این تابع دنبالههای حسابی تولید میکند:
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
نقطهٔ پایانی دادهشده هرگز بخشی از دنبالهٔ تولیدشده نیست؛ range(10) ده مقدار تولید میکند، یعنی اندیسهای مجاز برای آیتمهای یک دنباله به طول ۱۰. میتوان کاری کرد که بازه از عدد دیگری شروع شود، یا افزایش (increment) متفاوتی مشخص کرد (حتی منفی؛ گاهی به آن «گام» (step) میگویند):
>>> list(range(5, 10))
[5, 6, 7, 8, 9]
>>> list(range(0, 10, 3))
[0, 3, 6, 9]
>>> list(range(-10, -100, -30))
[-10, -40, -70]
برای تکرار روی اندیسهای یک دنباله، میتوانید range() و len() را به این شکل ترکیب کنید:
>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
... print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb
با این حال، در بیشتر چنین مواردی استفاده از تابع enumerate() راحتتر است؛ به تکنیکهای حلقهزدن نگاه کنید.
اگر فقط یک range را چاپ کنید، چیز عجیبی رخ میدهد:
>>> range(10)
range(0, 10)
از جهات زیادی شیئی که range() برمیگرداند طوری رفتار میکند که انگار یک فهرست است، اما در واقع چنین نیست. این شیء، هنگام تکرار روی آن، آیتمهای متوالی دنبالهٔ مورد نظر را برمیگرداند، اما فهرست را واقعاً نمیسازد و به این ترتیب در فضا صرفهجویی میشود.
به چنین شیئی میگوییم تکرارپذیر (iterable)، یعنی برای هدف قرار گرفتن توسط توابع و ساختارهایی مناسب است که انتظار دارند چیزی را دریافت کنند که بتوانند از آن آیتمهای متوالی بگیرند تا زمانی که منبع تمام (exhausted) شود. دیدیم که دستور for چنین ساختاری است، در حالی که نمونهای از تابعی که یک تکرارپذیر میگیرد sum() است:
>>> sum(range(4)) # 0 + 1 + 2 + 3
6
بعداً توابع بیشتری خواهیم دید که تکرارپذیرها را برمیگردانند و تکرارپذیرها را بهعنوان آرگومان میگیرند. در فصل ساختارهای داده، دربارهٔ list() با جزئیات بیشتری بحث خواهیم کرد.
۴٫۴. دستورهای break و continue
دستور break از درونیترین حلقهٔ for یا while که آن را در بر گرفته خارج میشود:
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(f"{n} equals {x} * {n//x}")
... break
...
4 equals 2 * 2
6 equals 2 * 3
8 equals 2 * 4
9 equals 3 * 3
دستور continue با تکرار بعدی حلقه ادامه میدهد:
>>> for num in range(2, 10):
... if num % 2 == 0:
... print(f"Found an even number {num}")
... continue
... print(f"Found an odd number {num}")
...
Found an even number 2
Found an odd number 3
Found an even number 4
Found an odd number 5
Found an even number 6
Found an odd number 7
Found an even number 8
Found an odd number 9
۴٫۵. بندهای else روی حلقهها
در یک حلقهٔ for یا while، دستور break ممکن است با یک بند else جفت شود. اگر حلقه بدون اجرای break پایان یابد، بند else اجرا میشود.
در یک حلقهٔ for، بند else پس از پایان آخرین تکرار حلقه اجرا میشود، یعنی اگر هیچ break رخ نداده باشد.
در یک حلقهٔ while، پس از نادرست شدن شرط حلقه اجرا میشود.
در هر دو نوع حلقه، اگر حلقه با break پایان یافته باشد بند else اجرا نمیشود. البته راههای دیگر زودتر پایان دادن به حلقه، مانند return یا یک استثنای پرتابشده، نیز از اجرای بند else صرفنظر میکنند.
این موضوع در حلقهٔ for زیر که اعداد اول را جستوجو میکند نشان داده شده است:
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, 'equals', x, '*', n//x)
... break
... else:
... # loop fell through without finding a factor
... print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
(بله، این کد درست است. با دقت نگاه کنید: بند else به حلقهٔ for تعلق دارد، نه به دستور if.)
یک راه برای فکر کردن به بند else این است که تصور کنید با if درون حلقه جفت شده است. با اجرای حلقه، دنبالهای مانند if/if/if/else اجرا میشود. if درون حلقه است و چند بار با آن روبهرو میشویم. اگر شرط هرگز درست شود، یک break رخ میدهد. اگر شرط هیچگاه درست نشود، بند else بیرون از حلقه اجرا خواهد شد.
وقتی با حلقه به کار میرود، بند else بیشتر با بند else دستور try وجه اشتراک دارد تا با بند else دستورهای if: بند else در دستور try وقتی اجرا میشود که هیچ استثنایی رخ ندهد، و بند else در حلقه وقتی اجرا میشود که هیچ break رخ ندهد. برای اطلاعات بیشتر دربارهٔ دستور try و استثناها، به مدیریت استثناها نگاه کنید.
۴٫۶. دستورهای pass
دستور pass هیچ کاری نمیکند. وقتی از نظر نحوی یک دستور لازم است اما برنامه نیازی به هیچ عملی ندارد، میتوان از آن استفاده کرد. برای مثال:
>>> while True:
... pass # Busy-wait for keyboard interrupt (Ctrl+C)
...
این معمولاً برای ساختن کلاسهای حداقلی به کار میرود:
>>> class MyEmptyClass:
... pass
...
جای دیگری که pass را میتوان به کار برد، بهعنوان جانگهدار برای بدنهٔ یک تابع یا شرط وقتی روی کد جدید کار میکنید است، که به شما اجازه میدهد در سطح انتزاعیتری به فکر کردن ادامه دهید. دستور pass بیصدا نادیده گرفته میشود:
>>> def initlog(*args):
... pass # Remember to implement this!
...
برای این مورد آخر، بسیاری از افراد بهجای pass از لیترال سهنقطه (ellipsis) یعنی ... استفاده میکنند. این استفاده برای پایتون معنای ویژهای ندارد و بخشی از تعریف زبان نیست (میتوانستید هر عبارت ثابتی را اینجا به کار ببرید)، اما ... هم بهطور قراردادی بهعنوان بدنهٔ جانگهدار استفاده میشود. به شیء Ellipsis نگاه کنید.
۴٫۷. دستورهای match
یک دستور match یک عبارت میگیرد و مقدار آن را با الگوهای متوالی که بهصورت یک یا چند بلوک case داده شدهاند مقایسه میکند. این از ظاهر شبیه دستور switch در C، Java یا JavaScript (و بسیاری زبانهای دیگر) است، اما بیشتر شبیه تطبیق الگو در زبانهایی مانند Rust یا Haskell است. فقط اولین الگویی که تطبیق پیدا کند اجرا میشود و همچنین میتواند اجزا (عناصر دنباله یا ویژگیهای شیء) را از مقدار استخراج کرده و در متغیرها قرار دهد. اگر هیچ caseای تطبیق نکند، هیچیک از شاخهها اجرا نمیشود.
سادهترین شکل، مقایسهٔ یک مقدار موضوع (subject) با یک یا چند لیترال است:
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"
به بلوک آخر توجه کنید: «نام متغیر» _ بهعنوان یک عنصر عمومی (wildcard) عمل میکند و هیچگاه تطبیق را شکست نمیدهد.
میتوانید چند لیترال را با | («یا») در یک الگو ترکیب کنید:
case 401 | 403 | 404:
return "Not allowed"
الگوها میتوانند شبیه انتسابهای باز کردن بستهبندی (unpacking) به نظر برسند و میتوان از آنها برای مقداردهی به متغیرها استفاده کرد:
# point is an (x, y) tuple
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
این را با دقت مطالعه کنید! الگوی اول دو لیترال دارد و میتوان آن را تعمیمی از الگوی لیترال نشاندادهشده در بالا دانست. اما دو الگوی بعدی یک لیترال و یک متغیر را ترکیب میکنند، و متغیر مقداری را از موضوع (point) میگیرد (bind میکند). الگوی چهارم دو مقدار را میگیرد، که آن را از نظر مفهومی شبیه انتساب باز کردن بستهبندی (x, y) = point میکند.
اگر از کلاسها برای ساختار دادن به دادههایتان استفاده میکنید، میتوانید نام کلاس را با فهرستی از آرگومانها شبیه سازنده (constructor) به کار ببرید، ولی با قابلیت گرفتن ویژگیها در متغیرها:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def where_is(point):
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y):
print(f"Y={y}")
case Point(x=x, y=0):
print(f"X={x}")
case Point():
print("Somewhere else")
case _:
print("Not a point")
میتوانید با برخی کلاسهای درونساخت که برای ویژگیهایشان ترتیبی فراهم میکنند (مثلاً dataclasses) از پارامترهای موقعیتی استفاده کنید. همچنین میتوانید با تعیین ویژگی ویژهٔ __match_args__ در کلاسهای خود، موقعیت مشخصی را برای ویژگیها در الگوها تعریف کنید. اگر مقدار آن ("x", "y") باشد، الگوهای زیر همگی همارزند (و همگی ویژگی y را به متغیر var میگیرند):
Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)
یک روش پیشنهادی برای خواندن الگوها این است که به آنها بهعنوان شکل تعمیمیافتهٔ چیزی نگاه کنید که در سمت چپ یک انتساب میگذارید، تا بفهمید کدام متغیرها با چه مقادیری تنظیم میشوند. فقط نامهای مستقل (مانند var در بالا) توسط دستور match مقداردهی میشوند. نامهای نقطهدار (مانند foo.bar)، نامهای ویژگی (مثل x= و y= در بالا) یا نام کلاسها (که با «(…)» کنارشان شناخته میشوند، مانند Point در بالا) هرگز مقداردهی نمیشوند.
الگوها میتوانند بهطور دلخواه تودرتو باشند. برای مثال، اگر فهرست کوتاهی از Pointها داشته باشیم و __match_args__ را اضافه کرده باشیم، میتوانیم آن را اینگونه تطبیق دهیم:
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
match points:
case []:
print("No points")
case [Point(0, 0)]:
print("The origin")
case [Point(x, y)]:
print(f"Single point {x}, {y}")
case [Point(0, y1), Point(0, y2)]:
print(f"Two on the Y axis at {y1}, {y2}")
case _:
print("Something else")
میتوانیم یک بند if به یک الگو اضافه کنیم که به آن «نگهبان» (guard) میگویند. اگر نگهبان نادرست باشد، match به امتحان کردن بلوک case بعدی ادامه میدهد. توجه کنید که گرفتن مقدار پیش از ارزیابی نگهبان انجام میشود:
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")
چند ویژگی کلیدی دیگر این دستور:
-
مانند انتسابهای باز کردن بستهبندی، الگوهای تاپل و فهرست دقیقاً یک معنا دارند و در واقع با دنبالههای دلخواه تطبیق میکنند. یک استثنای مهم این است که با تکرارکنندهها (iterators) یا رشتهها تطبیق نمیکنند.
-
الگوهای دنباله از باز کردن بستهبندی گسترشیافته پشتیبانی میکنند:
[x, y, *rest]و(x, y, *rest)مشابه انتسابهای باز کردن بستهبندی کار میکنند. نام بعد از*میتواند_هم باشد، بنابراین(x, y, *_)با دنبالهای با دستکم دو آیتم تطبیق میکند بدون آنکه آیتمهای باقیمانده را بگیرد. -
الگوهای نگاشت (mapping):
{"bandwidth": b, "latency": l}مقادیر"bandwidth"و"latency"را از یک دیکشنری میگیرد. بر خلاف الگوهای دنباله، کلیدهای اضافی نادیده گرفته میشوند. باز کردن بستهبندیای مانند**restنیز پشتیبانی میشود. (اما**_زائد خواهد بود، پس مجاز نیست.) -
زیرالگوها را میتوان با کلیدواژهٔ
asگرفت:case (Point(x1, y1), Point(x2, y2) as p2): ...عنصر دوم ورودی را بهعنوان
p2میگیرد (به شرط آنکه ورودی دنبالهای از دو Point باشد) -
بیشتر لیترالها با برابری مقایسه میشوند، اما تکنمونههای (singleton)
True،FalseوNoneبا هویت (identity) مقایسه میشوند. -
الگوها میتوانند از ثابتهای نامدار استفاده کنند. اینها باید نام نقطهدار باشند تا بهعنوان متغیرهای گیرنده تفسیر نشوند:
from enum import Enum class Color(Enum): RED = 'red' GREEN = 'green' BLUE = 'blue' color = Color(input("Enter your choice of 'red', 'blue' or 'green': ")) match color: case Color.RED: print("I see red!") case Color.GREEN: print("Grass is green") case Color.BLUE: print("I'm feeling the blues :(")
برای توضیح دقیقتر و مثالهای بیشتر میتوانید به PEP 636 نگاه کنید که با قالب آموزشی نوشته شده است.
۴٫۸. تعریف توابع
میتوانیم تابعی بسازیم که دنبالهٔ فیبوناچی را تا یک حد دلخواه مینویسد:
>>> def fib(n): # write Fibonacci series less than n
... """Print a Fibonacci series less than n."""
... a, b = 0, 1
... while a < n:
... print(a, end=' ')
... a, b = b, a+b
... print()
...
>>> # Now call the function we just defined:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
کلیدواژهٔ def یک تعریف تابع را معرفی میکند. باید بعد از آن نام تابع و فهرست پرانتزدار پارامترهای صوری (formal parameters) بیاید. دستورهایی که بدنهٔ تابع را تشکیل میدهند از خط بعد شروع میشوند و باید تورفتگی داشته باشند.
اولین دستور بدنهٔ تابع میتواند بهصورت اختیاری یک لیترال رشتهای باشد؛ این لیترال رشتهای، رشتهٔ مستندسازی تابع یا docstring است. (دربارهٔ docstringها بیشتر در بخش رشتههای مستندسازی آمده است.) ابزارهایی وجود دارند که از docstringها برای تولید خودکار مستندات آنلاین یا چاپی، یا برای اجازه دادن به کاربر برای گشتوگذار تعاملی در کد استفاده میکنند؛ گنجاندن docstring در کدی که مینویسید عادت خوبی است، پس آن را به عادت خود تبدیل کنید.
اجرای یک تابع، یک جدول نماد (symbol table) جدید معرفی میکند که برای متغیرهای محلی تابع به کار میرود. دقیقتر بگوییم، همهٔ انتسابهای متغیر در یک تابع مقدار را در جدول نماد محلی ذخیره میکنند؛ در حالی که ارجاعهای متغیر ابتدا در جدول نماد محلی جستوجو میکنند، سپس در جدولهای نماد محلی توابع دربرگیرنده، سپس در جدول نماد سراسری و در نهایت در جدول نامهای درونساخت. بنابراین، متغیرهای سراسری و متغیرهای توابع دربرگیرنده را نمیتوان مستقیماً درون یک تابع مقداردهی کرد (مگر آنکه برای متغیرهای سراسری، در یک دستور global نام برده شوند، یا برای متغیرهای توابع دربرگیرنده، در یک دستور nonlocal)، هرچند میتوان به آنها ارجاع داد.
پارامترهای واقعی (آرگومانها) در فراخوانی تابع، هنگام فراخوانی در جدول نماد محلی تابع فراخوانیشده معرفی میشوند؛ بنابراین آرگومانها با فراخوانی با مقدار (call by value) ارسال میشوند (که در آن مقدار همیشه یک ارجاع به شیء است، نه مقدار خود شیء). [۱] وقتی یک تابع تابع دیگری را فراخوانی میکند، یا خودش را بهصورت بازگشتی فراخوانی میکند، یک جدول نماد محلی جدید برای آن فراخوانی ساخته میشود.
یک تعریف تابع، نام تابع را با شیء تابع در جدول نماد جاری مرتبط میکند. مفسر شیئی را که آن نام به آن اشاره میکند بهعنوان تابع تعریفشده توسط کاربر میشناسد. نامهای دیگر هم میتوانند به همان شیء تابع اشاره کنند و میتوان از آنها نیز برای دسترسی به تابع استفاده کرد:
>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89
اگر از زبانهای دیگر آمده باشید، ممکن است اعتراض کنید که fib یک تابع نیست بلکه یک رویه (procedure) است چون مقداری برنمیگرداند. در واقع حتی توابع بدون دستور return هم مقداری برمیگردانند، هرچند مقداری نسبتاً کسلکننده. این مقدار None نام دارد (نامی درونساخت است). نوشتن مقدار None معمولاً توسط مفسر سرکوب میشود اگر تنها مقداری باشد که قرار است نوشته شود. اگر واقعاً بخواهید میتوانید با print() آن را ببینید:
>>> fib(0)
>>> print(fib(0))
None
نوشتن تابعی که بهجای چاپ کردن، فهرستی از اعداد دنبالهٔ فیبوناچی را برمیگرداند ساده است:
>>> def fib2(n): # return Fibonacci series up to n
... """Return a list containing the Fibonacci series up to n."""
... result = []
... a, b = 0, 1
... while a < n:
... result.append(a) # see below
... a, b = b, a+b
... return result
...
>>> f100 = fib2(100) # call it
>>> f100 # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
این مثال، طبق معمول، چند ویژگی جدید پایتون را نشان میدهد:
-
دستور
returnبا یک مقدار از تابع بازمیگردد.returnبدون آرگومان عبارت،Noneبرمیگرداند. رسیدن به انتهای تابع همNoneبرمیگرداند. -
دستور
result.append(a)یک متد از شیء فهرستresultرا فراخوانی میکند. متد تابعی است که به یک شیء «تعلق دارد» وobj.methodnameنامیده میشود، که در آنobjشیئی است (ممکن است یک عبارت باشد) وmethodnameنام متدی است که توسط نوع شیء تعریف شده است. انواع مختلف متدهای مختلفی تعریف میکنند. متدهای انواع مختلف میتوانند نام یکسان داشته باشند بدون آنکه ابهامی ایجاد شود. (میتوان با استفاده از کلاسها انواع و متدهای شیء خودتان را تعریف کنید، به کلاسها نگاه کنید.) متدappend()که در مثال نشان داده شده برای اشیای فهرست تعریف شده است؛ یک عنصر جدید در انتهای فهرست اضافه میکند. در این مثال همارزresult = result + [a]است، اما کارآمدتر.
۴٫۹. بیشتر دربارهٔ تعریف توابع
همچنین میتوان توابعی با تعداد متغیری آرگومان تعریف کرد. سه شکل وجود دارد که میتوان آنها را ترکیب کرد.
۴٫۹٫۱. مقادیر پیشفرض آرگومان
مفیدترین شکل، تعیین یک مقدار پیشفرض برای یک یا چند آرگومان است. این کار تابعی میسازد که میتوان آن را با آرگومانهای کمتری از آنچه تعریف شده فراخوانی کرد. برای مثال:
def ask_ok(prompt, retries=4, reminder='Please try again!'):
while True:
reply = input(prompt)
if reply in {'y', 'ye', 'yes'}:
return True
if reply in {'n', 'no', 'nop', 'nope'}:
return False
retries = retries - 1
if retries < 0:
raise ValueError('invalid user response')
print(reminder)
این تابع را میتوان به چند روش فراخوانی کرد:
- با دادن فقط آرگومان اجباری:
ask_ok('Do you really want to quit?') - با دادن یکی از آرگومانهای اختیاری:
ask_ok('OK to overwrite the file?', 2) - یا حتی با دادن همهٔ آرگومانها:
ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
این مثال همچنین کلیدواژهٔ in را معرفی میکند. این کلیدواژه آزمایش میکند که آیا یک دنباله شامل مقدار معینی هست یا نه.
مقادیر پیشفرض در نقطهٔ تعریف تابع و در حوزهٔ تعریفکننده ارزیابی میشوند، بنابراین
i = 5
def f(arg=i):
print(arg)
i = 6
f()
مقدار 5 را چاپ میکند.
هشدار مهم: مقدار پیشفرض فقط یک بار ارزیابی میشود. این موضوع وقتی مقدار پیشفرض یک شیء تغییرپذیر مانند فهرست، دیکشنری یا نمونههای اکثر کلاسها باشد تفاوت ایجاد میکند. برای مثال، تابع زیر آرگومانهایی را که در فراخوانیهای بعدی به آن داده میشود جمع میکند:
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
این کد چاپ میکند:
[1]
[1, 2]
[1, 2, 3]
اگر نمیخواهید مقدار پیشفرض بین فراخوانیهای بعدی مشترک باشد، میتوانید تابع را بهجای آن اینگونه بنویسید:
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
۴٫۹٫۲. آرگومانهای کلیدواژهای
توابع را همچنین میتوان با آرگومانهای کلیدواژهای به شکل kwarg=value فراخوانی کرد. برای مثال، تابع زیر:
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
print("-- This parrot wouldn't", action, end=' ')
print("if you put", voltage, "volts through it.")
print("-- Lovely plumage, the", type)
print("-- It's", state, "!")
یک آرگومان اجباری (voltage) و سه آرگومان اختیاری (state، action و type) میپذیرد. این تابع را میتوان به هر یک از روشهای زیر فراخوانی کرد:
parrot(1000) # 1 positional argument
parrot(voltage=1000) # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM') # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000) # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump') # 3 positional arguments
parrot('a thousand', state='pushing up the daisies') # 1 positional, 1 keyword
اما همهٔ فراخوانیهای زیر نامعتبر هستند:
parrot() # required argument missing
parrot(voltage=5.0, 'dead') # non-keyword argument after a keyword argument
parrot(110, voltage=220) # duplicate value for the same argument
parrot(actor='John Cleese') # unknown keyword argument
در یک فراخوانی تابع، آرگومانهای کلیدواژهای باید بعد از آرگومانهای موقعیتی بیایند. همهٔ آرگومانهای کلیدواژهای ارسالشده باید با یکی از آرگومانهایی که تابع میپذیرد مطابقت داشته باشند (مثلاً actor یک آرگومان معتبر برای تابع parrot نیست) و ترتیب آنها مهم نیست. این شامل آرگومانهای غیراختیاری هم میشود (مثلاً parrot(voltage=1000) هم معتبر است). هیچ آرگومانی نمیتواند بیش از یک بار مقدار بگیرد. در اینجا مثالی هست که بهدلیل همین محدودیت شکست میخورد:
>>> def function(a):
... pass
...
>>> function(0, a=0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for argument 'a'
وقتی آخرین پارامتر صوری به شکل **name وجود داشته باشد، یک دیکشنری (نگاشتها را ببینید) دریافت میکند که شامل همهٔ آرگومانهای کلیدواژهای است، بهجز آنهایی که با یک پارامتر صوری مطابقت دارند. این را میتوان با پارامتر صوری به شکل *name (که در زیربخش بعدی توضیح داده میشود) ترکیب کرد که یک تاپل شامل آرگومانهای موقعیتی فراتر از فهرست پارامترهای صوری دریافت میکند. (*name باید پیش از **name بیاید.) برای مثال، اگر تابعی اینگونه تعریف کنیم:
def cheeseshop(kind, *arguments, **keywords):
print("-- Do you have any", kind, "?")
print("-- I'm sorry, we're all out of", kind)
for arg in arguments:
print(arg)
print("-" * 40)
for kw in keywords:
print(kw, ":", keywords[kw])
میتوان آن را اینگونه فراخوانی کرد:
cheeseshop("Limburger", "It's very runny, sir.",
"It's really very, VERY runny, sir.",
shopkeeper="Michael Palin",
client="John Cleese",
sketch="Cheese Shop Sketch")
و البته چاپ میکند:
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch
توجه کنید که ترتیب چاپشدن آرگومانهای کلیدواژهای تضمین میشود با ترتیبی که در فراخوانی تابع داده شدهاند مطابقت داشته باشد.
۴٫۹٫۳. پارامترهای ویژه
بهطور پیشفرض، آرگومانها را میتوان به یک تابع پایتون هم با موقعیت و هم بهصراحت با کلیدواژه ارسال کرد. برای خوانایی و کارایی، منطقی است که نحوهٔ ارسال آرگومانها محدود شود تا توسعهدهنده فقط با نگاه کردن به تعریف تابع بتواند تشخیص دهد که آیتمها با موقعیت، با موقعیت یا کلیدواژه، یا با کلیدواژه ارسال میشوند.
یک تعریف تابع ممکن است اینگونه به نظر برسد:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
(در نمودار: pos1, pos2 فقط موقعیتی، pos_or_kwd موقعیتی یا کلیدواژهای، و kwd1, kwd2 فقط کلیدواژهای هستند.)
که در آن / و * اختیاری هستند. اگر استفاده شوند، این نمادها نوع پارامتر را بر اساس نحوهٔ ارسال آرگومانها به تابع نشان میدهند: فقط موقعیتی (positional-only)، موقعیتی-یا-کلیدواژهای (positional-or-keyword) و فقط کلیدواژهای (keyword-only). پارامترهای کلیدواژهای را پارامترهای نامدار هم مینامند.
۴٫۹٫۳٫۱. آرگومانهای موقعیتی-یا-کلیدواژهای
اگر / و * در تعریف تابع وجود نداشته باشند، آرگومانها را میتوان با موقعیت یا با کلیدواژه به تابع ارسال کرد.
۴٫۹٫۳٫۲. پارامترهای فقط موقعیتی
با نگاهی کمی دقیقتر، میتوان برخی پارامترها را بهصورت فقط موقعیتی علامتگذاری کرد. اگر پارامترها فقط موقعیتی باشند، ترتیب پارامترها مهم است و پارامترها را نمیتوان با کلیدواژه ارسال کرد. پارامترهای فقط موقعیتی پیش از یک / (اسلش) قرار میگیرند. / برای جدا کردن منطقی پارامترهای فقط موقعیتی از بقیهٔ پارامترها به کار میرود. اگر در تعریف تابع / وجود نداشته باشد، هیچ پارامتر فقط موقعیتیای وجود ندارد.
پارامترهایی که بعد از / میآیند میتوانند موقعیتی-یا-کلیدواژهای یا فقط کلیدواژهای باشند.
۴٫۹٫۳٫۳. آرگومانهای فقط کلیدواژهای
برای علامتگذاری پارامترها بهعنوان فقط کلیدواژهای، که نشان میدهد پارامترها باید با آرگومان کلیدواژهای ارسال شوند، یک * را در فهرست آرگومانها درست پیش از اولین پارامتر فقط کلیدواژهای قرار دهید.
۴٫۹٫۳٫۴. مثالهای تابع
تعریفهای تابع نمونهٔ زیر را در نظر بگیرید و به نشانگرهای / و * دقت کنید:
>>> def standard_arg(arg):
... print(arg)
...
>>> def pos_only_arg(arg, /):
... print(arg)
...
>>> def kwd_only_arg(*, arg):
... print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
... print(pos_only, standard, kwd_only)
اولین تعریف تابع، standard_arg، که آشناترین شکل است، هیچ محدودیتی برای قرارداد فراخوانی ایجاد نمیکند و آرگومانها را میتوان با موقعیت یا کلیدواژه ارسال کرد:
>>> standard_arg(2)
2
>>> standard_arg(arg=2)
2
تابع دوم pos_only_arg بهدلیل وجود / در تعریف تابع، محدود به استفاده از پارامترهای موقعیتی است:
>>> pos_only_arg(1)
1
>>> pos_only_arg(arg=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'
تابع سوم kwd_only_arg فقط آرگومانهای کلیدواژهای را مجاز میکند، همانطور که با * در تعریف تابع نشان داده شده است:
>>> kwd_only_arg(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given
>>> kwd_only_arg(arg=3)
3
و آخرین تابع هر سه قرارداد فراخوانی را در یک تعریف تابع به کار میبرد:
>>> combined_example(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given
>>> combined_example(1, 2, kwd_only=3)
1 2 3
>>> combined_example(1, standard=2, kwd_only=3)
1 2 3
>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'
در پایان، این تعریف تابع را در نظر بگیرید که یک تصادم بالقوه بین آرگومان موقعیتی name و **kwds دارد که name را بهعنوان یک کلید دارد:
def foo(name, **kwds):
return 'name' in kwds
هیچ فراخوانی ممکنی وجود ندارد که باعث شود این تابع True برگرداند، چون کلیدواژهٔ 'name' همیشه به پارامتر اول متصل میشود. برای مثال:
>>> foo(1, **{'name': 2})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>
اما با استفاده از / (آرگومانهای فقط موقعیتی) این امکانپذیر میشود، چون name را بهعنوان آرگومان موقعیتی و 'name' را بهعنوان کلید در آرگومانهای کلیدواژهای مجاز میکند:
>>> def foo(name, /, **kwds):
... return 'name' in kwds
...
>>> foo(1, **{'name': 2})
True
به عبارت دیگر، نام پارامترهای فقط موقعیتی را میتوان بدون ابهام در **kwds به کار برد.
۴٫۹٫۳٫۵. جمعبندی
مورد استفاده تعیین میکند که در تعریف تابع از کدام پارامترها استفاده شود:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
بهعنوان راهنما:
- اگر میخواهید نام پارامترها در دسترس کاربر نباشد، از فقط موقعیتی استفاده کنید. این وقتی مفید است که نام پارامترها معنای واقعی ندارند، یا میخواهید ترتیب آرگومانها را هنگام فراخوانی تابع اجباری کنید، یا لازم دارید برخی پارامترهای موقعیتی و کلیدواژههای دلخواه را بگیرید.
- وقتی نامها معنا دارند و با صریح بودن نامها تعریف تابع قابلفهمتر میشود، یا میخواهید کاربران را از تکیه بر موقعیت آرگومان ارسالشده بازدارید، از فقط کلیدواژهای استفاده کنید.
- برای یک API، از فقط موقعیتی استفاده کنید تا اگر نام پارامتر در آینده تغییر کرد، تغییرهای شکننده در API رخ ندهد.
۴٫۹٫۴. فهرستهای آرگومان دلخواه
در پایان، کماستفادهترین گزینه این است که مشخص کنیم یک تابع را میتوان با تعداد دلخواهی آرگومان فراخوانی کرد. این آرگومانها در یک تاپل بستهبندی میشوند (تاپلها و دنبالهها را ببینید). پیش از تعداد متغیر آرگومانها، صفر یا چند آرگومان معمولی میتواند بیاید:
def write_multiple_items(file, separator, *args):
file.write(separator.join(args))
معمولاً، این آرگومانهای متغیر (variadic) در انتهای فهرست پارامترهای صوری خواهند بود، چون همهٔ آرگومانهای ورودی باقیمانده را که به تابع ارسال میشوند جمع میکنند. هر پارامتر صوری که بعد از پارامتر *args بیاید آرگومان «فقط کلیدواژهای» است، یعنی فقط میتواند بهجای آرگومان موقعیتی بهصورت کلیدواژه به کار رود:
>>> def concat(*args, sep="/"):
... return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'
۴٫۹٫۵. باز کردن بستهٔ فهرستهای آرگومان
وضعیت معکوس وقتی رخ میدهد که آرگومانها از قبل در یک فهرست یا تاپل باشند اما برای فراخوانی تابعی که آرگومانهای موقعیتی جداگانه میخواهد باید باز شوند. برای مثال، تابع درونساخت range() آرگومانهای جداگانهٔ start و stop را انتظار دارد. اگر بهطور جداگانه در دسترس نباشند، فراخوانی تابع را با عملگر * بنویسید تا آرگومانها را از یک فهرست یا تاپل باز کند:
>>> list(range(3, 6)) # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args)) # call with arguments unpacked from a list
[3, 4, 5]
به همین ترتیب، دیکشنریها میتوانند آرگومانهای کلیدواژهای را با عملگر ** تحویل دهند:
>>> def parrot(voltage, state='a stiff', action='voom'):
... print("-- This parrot wouldn't", action, end=' ')
... print("if you put", voltage, "volts through it.", end=' ')
... print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !
۴٫۹٫۶. عبارتهای لامبدا
توابع کوچک بینام را میتوان با کلیدواژهٔ lambda ساخت. این تابع مجموع دو آرگومانش را برمیگرداند: lambda a, b: a+b. توابع لامبدا را میتوان در هر جایی که شیء تابع لازم است به کار برد. از نظر نحوی محدود به یک عبارت واحد هستند. از نظر معنایی، فقط شکر نحوی (syntactic sugar) برای یک تعریف تابع معمولیاند. مانند تعریفهای تابع تودرتو، توابع لامبدا میتوانند به متغیرهای حوزهٔ دربرگیرنده ارجاع دهند:
>>> def make_incrementor(n):
... return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43
مثال بالا از یک عبارت لامبدا برای برگرداندن یک تابع استفاده میکند. کاربرد دیگر، ارسال یک تابع کوچک بهعنوان آرگومان است. برای مثال، list.sort() یک تابع کلید مرتبسازی key میگیرد که میتواند یک تابع لامبدا باشد:
>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
۴٫۹٫۷. رشتههای مستندسازی
در اینجا چند قرارداد دربارهٔ محتوا و قالببندی رشتههای مستندسازی (docstrings) آمده است.
خط اول همیشه باید خلاصهای کوتاه و موجز از هدف شیء باشد. برای اختصار، نباید صریحاً نام یا نوع شیء را بیان کند، چون اینها از راههای دیگر در دسترس هستند (مگر اینکه نام اتفاقاً فعلی باشد که عملکرد تابع را توصیف میکند). این خط باید با حرف بزرگ شروع شود و با نقطه پایان یابد.
اگر خطوط بیشتری در رشتهٔ مستندسازی باشد، خط دوم باید خالی باشد تا خلاصه بهطور بصری از بقیهٔ توضیحات جدا شود. خطوط بعدی باید یک یا چند پاراگراف باشند که قراردادهای فراخوانی شیء، عوارض جانبی آن و غیره را توصیف میکنند.
تجزیهگر پایتون تورفتگی را از لیترالهای رشتهای چندخطی، وقتی بهعنوان docstring ماژول، کلاس یا تابع به کار میروند، حذف میکند.
در اینجا نمونهای از یک docstring چندخطی آمده است:
>>> def my_function():
... """Do nothing, but document it.
...
... No, really, it doesn't do anything:
...
... >>> my_function()
... >>>
... """
... pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.
No, really, it doesn't do anything:
>>> my_function()
>>>
۴٫۹٫۸. حاشیهنویسیهای تابع
حاشیهنویسیهای تابع (function annotations) فرادادههایی کاملاً اختیاری دربارهٔ انواع استفادهشده توسط توابع تعریفشده توسط کاربر هستند (برای اطلاعات بیشتر PEP 3107 و PEP 484 را ببینید).
حاشیهنویسیها بهصورت یک دیکشنری در ویژگی __annotations__ تابع ذخیره میشوند و بر هیچ بخش دیگری از تابع تأثیری ندارند. حاشیهنویسی پارامترها با یک دونقطه بعد از نام پارامتر و به دنبال آن عبارتی که به مقدار حاشیهنویسی ارزیابی میشود تعریف میشود. حاشیهنویسی مقدار بازگشتی با نویسهٔ -> و به دنبال آن یک عبارت، بین فهرست پارامترها و دونقطهای که پایان دستور def را نشان میدهد تعریف میشود. مثال زیر یک آرگومان اجباری، یک آرگومان اختیاری و مقدار بازگشتی حاشیهنویسیشده دارد:
>>> def f(ham: str, eggs: str = 'eggs') -> str:
... print("Annotations:", f.__annotations__)
... print("Arguments:", ham, eggs)
... return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'
۴٫۱۰. میانپرده: سبک کدنویسی
حالا که میخواهید قطعههای بلندتر و پیچیدهتری از پایتون بنویسید، زمان خوبی است که دربارهٔ سبک کدنویسی صحبت کنیم. بیشتر زبانها را میتوان (یا بهطور مختصرتر، قالببندی کرد) به سبکهای مختلف نوشت؛ بعضی خواناترند. آسان کردن خواندن کد شما برای دیگران همیشه ایدهٔ خوبی است و اتخاذ یک سبک کدنویسی خوب کمک بسیار زیادی به این کار میکند.
برای پایتون، PEP 8 بهعنوان راهنمای سبکی که بیشتر پروژهها از آن پیروی میکنند پدید آمده است؛ سبک کدنویسی بسیار خوانا و دلنشینی را ترویج میکند. هر توسعهدهندهٔ پایتون باید در مقطعی آن را بخواند؛ در اینجا مهمترین نکات برایتان استخراج شده است:
-
از تورفتگی ۴ فاصلهای استفاده کنید و از tab استفاده نکنید.
۴ فاصله سازشی خوب بین تورفتگی کوچک (که عمق تودرتویی بیشتر را مجاز میکند) و تورفتگی بزرگ (که خواندن را آسانتر میکند) است. tabها سردرگمی ایجاد میکنند و بهتر است کنار گذاشته شوند.
-
خطوط را طوری بشکنید که از ۷۹ نویسه بیشتر نشوند.
این به کاربران با نمایشگرهای کوچک کمک میکند و امکان داشتن چند فایل کد در کنار هم روی نمایشگرهای بزرگتر را فراهم میکند.
-
از خطوط خالی برای جدا کردن توابع و کلاسها، و بلوکهای بزرگتر کد درون توابع استفاده کنید.
-
در صورت امکان، توضیحات (comments) را در خط جداگانهٔ خودشان بگذارید.
-
از docstring استفاده کنید.
-
دور عملگرها و بعد از ویرگولها فاصله بگذارید، اما درست داخل ساختارهای پرانتزی نه:
a = f(1, 2) + g(3, 4). -
نام کلاسها و توابع خود را یکدست انتخاب کنید؛ قرارداد این است که از
UpperCamelCaseبرای کلاسها وlowercase_with_underscoresبرای توابع و متدها استفاده شود. همیشه ازselfبهعنوان نام اولین آرگومان متد استفاده کنید (برای اطلاعات بیشتر دربارهٔ کلاسها و متدها به نگاهی اول به کلاسها مراجعه کنید). -
اگر قرار است کد شما در محیطهای بینالمللی استفاده شود، از کدگذاریهای عجیب استفاده نکنید. UTF-8 پیشفرض پایتون، یا حتی ASCII ساده، در هر حال بهترین عملکرد را دارند.
-
همینطور، اگر کمترین احتمالی هست که افرادی که به زبان دیگری صحبت میکنند کد را بخوانند یا نگهداری کنند، از نویسههای غیر ASCII در شناسهها استفاده نکنید.
پاورقیها
[۱] در واقع، فراخوانی با ارجاع شیء (call by object reference) توصیف بهتری خواهد بود، زیرا اگر یک شیء تغییرپذیر ارسال شود، فراخواننده هر تغییری را که فراخوانیشونده روی آن ایجاد میکند (آیتمهایی که در یک فهرست درج میشوند) خواهد دید.