4. ابزارهای بیشتر برای کنترل جریان¶
علاوه بر دستور while که بهتازگی معرفی شد، پایتون چند مورد دیگر نیز دارد که در این فصل با آنها آشنا خواهیم شد.
4.1. دستورهای 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 دستورات مراجعه کنید.
4.2. دستورهای for¶
دستور for در پایتون کمی با چیزی که ممکن است در C یا Pascal به آن عادت کرده باشید تفاوت دارد. بهجای اینکه همیشه روی یک تصاعد حسابی از اعداد پیمایش کند (مانند Pascal)، یا به کاربر امکان تعریف هم گام پیمایش و هم شرط توقف را بدهد (مانند C)، دستور for پایتون روی آیتمهای هر دنبالهای (یک فهرست یا یک رشته) به ترتیبی که در دنباله ظاهر شدهاند پیمایش میکند. برای مثال (بدون قصد استعاره):
>>> # اندازهگیری طول چند رشته:
>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words:
... print(w, len(w))
...
cat 3
window 6
defenestrate 12
کدی که هنگام پیمایش روی یک مجموعه، همان مجموعه را تغییر میدهد، ممکن است بهدرستی نوشتن دشوار باشد. در عوض، معمولاً سادهتر است که روی یک کپی از مجموعه پیمایش کنید یا یک مجموعهٔ جدید ایجاد کنید:
# ایجاد یک مجموعهی نمونه
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}
# راهبرد: پیمایش روی یک کپی
for user, status in users.copy().items():
if status == 'inactive':
del users[user]
# راهبرد: ایجاد یک مجموعهی جدید
active_users = {}
for user, status in users.items():
if status == 'active':
active_users[user] = status
4.3. تابع range()¶
اگر واقعاً نیاز دارید روی یک دنباله از اعداد پیمایش کنید، تابع داخلی range() بسیار کاربردی است. این تابع تصاعدهای حسابی تولید میکند:
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
نقطهٔ پایانی دادهشده هرگز بخشی از دنبالهٔ تولیدشده نیست؛ range(10) مقدار تولید میکند که همان اندیسهای مجاز برای آیتمهای یک دنباله با طول ۱۰ هستند. امکان این وجود دارد که شروع بازه را از عدد دیگری تعیین کنید، یا افزایشی متفاوت (حتی منفی؛ که گاهی «گام» نامیده میشود) مشخص کنید:
>>> 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 است؛ یعنی برای استفاده بهعنوان هدف تابعها و ساختارهایی مناسب است که انتظار دارند چیزی وجود داشته باشد که بتوانند از آن آیتمهای متوالی را تا زمانی که چیزی باقی نمانده است دریافت کنند. دیدیم که دستور for چنین ساختاری است، در حالی که نمونهای از تابعی که یک شیء تکرارپذیر دریافت میکند sum() است:
>>> sum(range(4)) # 0 + 1 + 2 + 3
6
بعداً تابعهای بیشتری را خواهیم دید که اشیای تکرارپذیر برمیگردانند و اشیای تکرارپذیر را بهعنوان آرگومان دریافت میکنند. در فصل ساختمان دادهها، دربارهٔ list() با جزئیات بیشتری صحبت خواهیم کرد.
4.4. دستورهای 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
4.5. بخشهای else در حلقهها¶
در یک حلقهٔ for یا while، دستور break میتواند با یک بخش else همراه شود. اگر حلقه بدون اجرای break به پایان برسد، بخش else اجرا میشود.
در یک حلقهٔ for، بخش else پس از پایان آخرین تکرار حلقه اجرا میشود؛ یعنی زمانی که هیچ breakای رخ نداده باشد.
در یک حلقهٔ while، این بخش پس از نادرست شدن شرط حلقه اجرا میشود.
در هر دو نوع حلقه، بخش else در صورتی اجرا نمیشود که حلقه توسط یک break خاتمه یافته باشد. البته روشهای دیگر پایان زودهنگام حلقه، مانند یک 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:
... # حلقه بدون پیدا کردن هیچ مقسومعلیهی به پایان رسید
... 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 دارد تا دستورهای if: بخش else یک دستور try زمانی اجرا میشود که هیچ استثنایی رخ ندهد، و بخش else یک حلقه زمانی اجرا میشود که هیچ break ای رخ ندهد. برای اطلاعات بیشتر دربارهٔ دستور try و استثناها، به مدیریت استثناها مراجعه کنید.
4.6. دستورهای pass¶
دستور pass هیچ کاری انجام نمیدهد. زمانی میتوان از آن استفاده کرد که از نظر نحوی وجود یک دستور لازم باشد، اما برنامه به هیچ عملی نیاز نداشته باشد. برای مثال:
>>> while True:
... pass # انتظار فعال برای وقفهی صفحهکلید (Ctrl+C)
...
این دستور معمولاً برای ایجاد کلاسهای حداقلی استفاده میشود:
>>> class MyEmptyClass:
... pass
...
جای دیگری که میتوان از pass استفاده کرد، بهعنوان جاینگهدار برای بدنهٔ یک تابع یا شرط در زمانی است که روی کد جدید کار میکنید؛ این کار به شما اجازه میدهد در سطح انتزاعیتری به فکر کردن ادامه دهید. pass بهصورت بیصدا نادیده گرفته میشود:
>>> def initlog(*args):
... pass # فراموش نکنید این تابع را پیادهسازی کنید!
...
برای حالت آخر، بسیاری از افراد بهجای pass از مقدار لفظی بیضی ... استفاده میکنند. این استفاده هیچ معنای ویژهای برای پایتون ندارد و بخشی از تعریف زبان نیست (در اینجا میتوانستید از هر عبارت ثابتی استفاده کنید)، اما ... بهصورت قراردادی بهعنوان بدنهٔ جاینگهدار نیز استفاده میشود. به The Ellipsis Object مراجعه کنید.
4.7. match دستورات¶
یک دستور match یک عبارت را دریافت میکند و مقدار آن را با الگوهای متوالی که در یک یا چند بلوک case ارائه شدهاند مقایسه میکند. این دستور در ظاهر شبیه دستور switch در C، Java یا JavaScript (و بسیاری از زبانهای دیگر) است، اما شباهت بیشتری به تطبیق الگو (pattern matching) در زبانهایی مانند Rust یا Haskell دارد. تنها اولین الگویی که تطبیق پیدا کند اجرا میشود و این دستور همچنین میتواند اجزا (عناصر دنباله یا ویژگیهای شیء) را از مقدار استخراج کرده و در متغیرها قرار دهد. اگر هیچ caseای تطبیق پیدا نکند، هیچیک از شاخهها اجرا نمیشوند.
سادهترین شکل، یک مقدار موضوع را با یک یا چند مقدار لفظی مقایسه میکند:
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 assignments) باشند و برای مقداردهی به متغیرها استفاده شوند:
# نقطه یک تاپل (x, y) است
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) دریافت میکند. الگوی چهارم دو مقدار را دریافت میکند، که از نظر مفهومی شبیه به انتساب واگشاییٔ (x, y) = point است.
اگر برای ساختاربندی دادههای خود از کلاسها استفاده میکنید، میتوانید نام کلاس را بههمراه فهرستی از آرگومانها که شبیه سازنده است استفاده کنید، اما با قابلیت دریافت ویژگیها در متغیرها:
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")
میتوانید از پارامترهای موقعیتی با برخی کلاسهای داخلی که برای ویژگیهای خود ترتیب مشخصی ارائه میکنند (برای مثال dataclassها) استفاده کنید. همچنین میتوانید با تنظیم ویژگی ویژهٔ __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")
چند ویژگی کلیدی دیگر این دستور:
مانند انتسابهای واگشایی، الگوهای تاپل و فهرست دقیقاً معنای یکسانی دارند و در واقع با دنبالههای دلخواه تطبیق پیدا میکنند. یک استثنای مهم این است که آنها با تکرارگرها یا رشتهها تطبیق پیدا نمیکنند.
الگوهای دنبالهای از واگشاییٔ گسترده پشتیبانی میکنند:
[x, y, *rest]و(x, y, *rest)مشابه انتسابهای واگشایی عمل میکنند. نام پس از*نیز میتواند_باشد، بنابراین(x, y, *_)با دنبالهای دارای حداقل دو آیتم تطبیق پیدا میکند، بدون اینکه آیتمهای باقیمانده را در متغیری قرار دهد.الگوهای نگاشتی:
{"bandwidth": b, "latency": l}مقادیر"bandwidth"و"latency"را از یک دیکشنری دریافت میکند. برخلاف الگوهای دنبالهای، کلیدهای اضافی نادیده گرفته میشوند. واگشاییای مانند**restنیز پشتیبانی میشود. (اما**_اضافی است، بنابراین مجاز نیست.)زیرالگوها را میتوان با استفاده از کلیدواژهٔ
asدریافت کرد:case (Point(x1, y1), Point(x2, y2) as p2): ...
عنصر دوم ورودی را بهعنوان
p2دریافت میکند (تا زمانی که ورودی یک دنبالهٔ دو عضوی از نقاط باشد)بیشتر مقادیر لفظی با استفاده از برابری مقایسه میشوند، اما تکنمونههای
True،FalseوNoneبا استفاده از هویت مقایسه میشوند.الگوها میتوانند از ثابتهای نامگذاریشده استفاده کنند. اینها باید نامهای نقطهای باشند تا از تفسیر شدن آنها بهعنوان متغیرهای دریافتکننده جلوگیری شود:
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 مراجعه کنید که با قالب یک آموزش نوشته شده است.
4.8. تعریف توابع¶
میتوانیم تابعی ایجاد کنیم که سری فیبوناچی را تا یک کران دلخواه بنویسد:
>>> def fib(n): # نوشتن دنبالهی فیبوناچی کوچکتر از n
... """Print a Fibonacci series less than n."""
... a, b = 0, 1
... while a < n:
... print(a, end=' ')
... a, b = b, a+b
... print()
...
>>> # اکنون تابعی را که تعریف کردیم فراخوانی میکنیم:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
کلیدواژهٔ def یک تعریف تابع را معرفی میکند. این کلیدواژه باید پس از خود نام تابع و فهرست پارامترهای رسمی درون پرانتز بیاید. دستورهایی که بدنهٔ تابع را تشکیل میدهند از خط بعدی شروع میشوند و باید تورفتگی داشته باشند.
اولین دستور بدنهٔ تابع میتواند بهصورت اختیاری یک رشتهٔ لفظی باشد؛ این رشتهٔ لفظی، رشتهٔ مستندات تابع یا docstring است. (اطلاعات بیشتر دربارهٔ رشتههای مستندات را میتوانید در بخش رشتههای مستندسازی پیدا کنید.) ابزارهایی وجود دارند که از docstringها برای تولید خودکار مستندات آنلاین یا چاپی، یا برای اجازه دادن به کاربر جهت مرور تعاملی کد استفاده میکنند؛ بهتر است در کدی که مینویسید docstringها را قرار دهید، بنابراین آن را به یک عادت تبدیل کنید.
اجرای یک تابع، یک جدول نماد جدید ایجاد میکند که برای متغیرهای محلی تابع استفاده میشود. بهطور دقیقتر، تمام انتسابهای متغیر در یک تابع، مقدار را در جدول نماد محلی ذخیره میکنند؛ در حالی که ارجاعهای متغیر ابتدا در جدول نماد محلی، سپس در جدولهای نماد محلی تابعهای دربرگیرنده، سپس در جدول نماد سراسری، و در نهایت در جدول نامهای داخلی جستوجو میشوند. بنابراین، متغیرهای سراسری و متغیرهای تابعهای دربرگیرنده نمیتوانند مستقیماً درون یک تابع مقداردهی شوند (مگر اینکه برای متغیرهای سراسری با یک دستور global، یا برای متغیرهای تابعهای دربرگیرنده با یک دستور nonlocal نامگذاری شده باشند)، اگرچه میتوان به آنها ارجاع داد.
پارامترهای واقعی (آرگومانها) در یک فراخوانی تابع، هنگام فراخوانی تابع در جدول نماد محلی تابع فراخوانیشده قرار میگیرند؛ بنابراین، آرگومانها با استفاده از انتقال با مقدار (call by value) ارسال میشوند (که در آن مقدار همیشه یک ارجاع به شیء است، نه مقدار خود شیء). [1] هنگامی که یک تابع، تابع دیگری را فراخوانی میکند یا بهصورت بازگشتی خودش را فراخوانی میکند، یک جدول نماد محلی جدید برای آن فراخوانی ایجاد میشود.
یک تعریف تابع، نام تابع را با شیء تابع در جدول نماد فعلی مرتبط میکند. مفسر شیئی را که آن نام به آن اشاره میکند بهعنوان یک تابع تعریفشده توسط کاربر تشخیص میدهد. نامهای دیگر نیز میتوانند به همان شیء تابع اشاره کنند و برای دسترسی به تابع استفاده شوند:
>>> 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): # بازگرداندن دنبالهی فیبوناچی تا n
... """Return a list containing the Fibonacci series up to n."""
... result = []
... a, b = 0, 1
... while a < n:
... result.append(a) # توضیح زیر را ببینید
... a, b = b, a+b
... return result
...
>>> f100 = fib2(100) # فراخوانی تابع
>>> f100 # نمایش نتیجه
[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]است، اما کارآمدتر است.
4.9. اطلاعات بیشتر دربارهٔ تعریف تابعها¶
همچنین امکان تعریف تابعهایی با تعداد متغیری از آرگومانها وجود دارد. سه شکل وجود دارد که میتوان آنها را با یکدیگر ترکیب کرد.
4.9.1. مقدارهای پیشفرض آرگومانها¶
کاربردیترین شکل، مشخص کردن یک مقدار پیشفرض برای یک یا چند آرگومان است. این کار تابعی ایجاد میکند که میتوان آن را با آرگومانهای کمتری نسبت به تعداد تعریفشده فراخوانی کرد. برای مثال:
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 را معرفی میکند. این کلیدواژه بررسی میکند که آیا یک دنباله مقدار مشخصی را در خود دارد یا خیر.
مقدارهای پیشفرض در نقطهٔ تعریف تابع و در محدودهٔ تعریفکننده (defining scope) ارزیابی میشوند، بنابراین
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
4.9.2. آرگومانهای کلیدواژهای¶
تابعها همچنین میتوانند با استفاده از keyword arguments به شکل 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) # یک آرگومان موقعیتی
parrot(voltage=1000) # یک آرگومان کلیدواژهای
parrot(voltage=1000000, action='VOOOOOM') # دو آرگومان کلیدواژهای
parrot(action='VOOOOOM', voltage=1000000) # دو آرگومان کلیدواژهای
parrot('a million', 'bereft of life', 'jump') # سه آرگومان موقعیتی
parrot('a thousand', state='pushing up the daisies') # یک آرگومان موقعیتی، یک آرگومان کلیدواژهای
اما تمام فراخوانیهای زیر نامعتبر خواهند بود:
parrot() # آرگومان الزامی وارد نشده است
parrot(voltage=5.0, 'dead') # آرگومان غیرکلیدواژهای پس از آرگومان کلیدواژهای آمده است
parrot(110, voltage=220) # مقدار تکراری برای یک آرگومان یکسان
parrot(actor='John Cleese') # آرگومان کلیدواژهای ناشناخته
در یک فراخوانی تابع، آرگومانهای کلیدواژهای باید پس از آرگومانهای موقعیتی بیایند. تمام آرگومانهای کلیدواژهای ارسالشده باید با یکی از آرگومانهای پذیرفتهشده توسط تابع مطابقت داشته باشند (برای مثال، 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 وجود داشته باشد، یک فرهنگ لغت (به Mapping Types --- dict مراجعه کنید) دریافت میکند که شامل تمام آرگومانهای کلیدواژهای است، بهجز آنهایی که متناظر با یک پارامتر رسمی هستند. این مورد میتواند با یک پارامتر رسمی به شکل *name ترکیب شود (که در زیربخش بعدی توضیح داده شده است)؛ این پارامتر یک tuple دریافت میکند که شامل آرگومانهای موقعیتی فراتر از فهرست پارامترهای رسمی است. (*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
توجه داشته باشید که ترتیب چاپ آرگومانهای کلیدواژهای تضمین شده است که با ترتیبی که در فراخوانی تابع ارائه شدهاند مطابقت داشته باشد.
4.9.3. پارامترهای ویژه¶
به طور پیشفرض، آرگومانها را میتوان در یک تابع پایتون یا با استفاده از موقعیت یا بهصورت صریح با کلیدواژه ارسال کرد. برای خوانایی و کارایی، منطقی است که نحوه ارسال آرگومانها را محدود کنیم تا یک توسعهدهنده تنها با نگاه کردن به تعریف تابع بتواند تعیین کند که موارد با موقعیت، با موقعیت یا کلیدواژه، یا فقط با کلیدواژه ارسال میشوند.
تعریف یک تابع ممکن است به شکل زیر باشد:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
که در آن / و * اختیاری هستند. اگر استفاده شوند، این نمادها نوع پارامتر را بر اساس نحوه ارسال آرگومانها به تابع مشخص میکنند: فقط موقعیتی، موقعیتی یا کلیدواژهای، و فقط کلیدواژهای. پارامترهای کلیدواژهای همچنین به عنوان پارامترهای نامدار شناخته میشوند.
4.9.3.1. آرگومانهای موقعیتی یا کلیدواژهای¶
اگر / و * در تعریف تابع وجود نداشته باشند، آرگومانها را میتوان با موقعیت یا با کلیدواژه به یک تابع ارسال کرد.
4.9.3.2. پارامترهای فقط موقعیتی¶
با بررسی دقیقتر، میتوان برخی پارامترها را به صورت فقط موقعیتی مشخص کرد. اگر فقط موقعیتی باشند، ترتیب پارامترها اهمیت دارد و پارامترها نمیتوانند با کلیدواژه ارسال شوند. پارامترهای فقط موقعیتی قبل از یک / (خط مورب) قرار میگیرند. / برای جدا کردن منطقی پارامترهای فقط موقعیتی از سایر پارامترها استفاده میشود. اگر / در تعریف تابع وجود نداشته باشد، هیچ پارامتر فقط موقعیتی وجود ندارد.
پارامترهای پس از / میتوانند موقعیتی یا کلیدواژهای یا فقط کلیدواژهای باشند.
4.9.3.3. آرگومانهای فقط کلیدواژهای¶
برای مشخص کردن پارامترها بهعنوان فقط کلیدواژهای، که نشان میدهد پارامترها باید با آرگومان کلیدواژهای ارسال شوند، یک * را در فهرست آرگومانها درست قبل از اولین پارامتر فقط کلیدواژهای قرار دهید.
4.9.3.4. مثالهای توابع¶
تعریفهای تابع زیر را با دقت بررسی کنید و به نشانههای / و * توجه کنید:
>>> 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 استفاده شود.
4.9.3.5. خلاصه¶
مورد استفاده تعیین میکند که کدام پارامترها را در تعریف تابع استفاده کنید:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
برای راهنمایی:
اگر میخواهید نام پارامترها برای کاربر قابل دسترس نباشد، از فقط-موقعیتی استفاده کنید. این زمانی مفید است که نام پارامترها معنای واقعی ندارند، زمانی که میخواهید ترتیب آرگومانها هنگام فراخوانی تابع اجباری باشد، یا زمانی که به تعدادی پارامتر موقعیتی و کلیدواژههای دلخواه نیاز دارید.
زمانی که نامها دارای معنا هستند و تعریف تابع با مشخص کردن صریح نامها قابل فهمتر میشود، یا زمانی که میخواهید کاربران به موقعیت آرگومانهای ارسالشده وابسته نشوند، از فقط کلیدواژهای استفاده کنید.
برای یک API، از پارامترهای فقط موقعیتی استفاده کنید تا از شکستن تغییرات API در صورت تغییر نام پارامتر در آینده جلوگیری شود.
4.9.4. فهرستهای دلخواه آرگومانها¶
در نهایت، کمکاربردترین گزینه این است که مشخص کنید یک تابع میتواند با تعداد دلخواهی از آرگومانها فراخوانی شود. این آرگومانها در یک تاپل بستهبندی میشوند (به تاپلها و دنبالهها مراجعه کنید). پیش از آرگومانهای با تعداد متغیر، میتوان صفر یا چند آرگومان معمولی داشت.
def write_multiple_items(file, separator, *args):
file.write(separator.join(args))
معمولاً این آرگومانهای متغیرتعداد در انتهای فهرست پارامترهای رسمی قرار میگیرند، زیرا تمام آرگومانهای ورودی باقیماندهای که به تابع ارسال میشوند را جمعآوری میکنند. هر پارامتر رسمی که پس از پارامتر *args قرار گیرد، یک آرگومان «فقط کلیدواژهای» است؛ یعنی فقط میتواند بهعنوان کلیدواژه استفاده شود و نه به صورت آرگومان موقعیتی.
>>> def concat(*args, sep="/"):
... return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'
4.9.5. باز کردن بسته آرگومانها¶
حالت معکوس زمانی رخ میدهد که آرگومانها از قبل در یک فهرست یا تاپل قرار دارند، اما برای فراخوانی تابعی که به آرگومانهای موقعیتی جداگانه نیاز دارد، باید باز شوند. برای مثال، تابع داخلی range() آرگومانهای جداگانه start و stop را انتظار دارد. اگر این آرگومانها به صورت جداگانه در دسترس نیستند، فراخوانی تابع را با عملگر * برای باز کردن آرگومانها از یک فهرست یا تاپل بنویسید:
>>> list(range(3, 6)) # فراخوانی معمولی با آرگومانهای جداگانه
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args)) # فراخوانی با آرگومانهای بازشده از یک فهرست
[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 !
4.9.6. عبارتهای لامبدا¶
توابع کوچک بینام را میتوان با کلیدواژه lambda ایجاد کرد. این تابع مجموع دو آرگومان خود را برمیگرداند: lambda a, b: a+b. توابع لامبدا را میتوان هر جا که به اشیای تابع نیاز باشد استفاده کرد. آنها از نظر نحوی به یک عبارت منفرد محدود هستند. از نظر معنایی، آنها تنها شکری نحوی برای یک تعریف معمولی تابع هستند. مانند تعریفهای توابع تودرتو، توابع لامبدا میتوانند به متغیرهای محدودهی دربرگیرنده دسترسی داشته باشند:
>>> 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')]
4.9.7. رشتههای مستندسازی¶
در ادامه، برخی قراردادها دربارهٔ محتوا و قالببندی رشتههای مستندسازی آمده است.
خط اول باید همیشه یک خلاصهٔ کوتاه و مختصر از هدف شیء باشد. برای کوتاهتر بودن، نباید بهطور صریح نام یا نوع شیء را بیان کند، زیرا این موارد از راههای دیگری در دسترس هستند (مگر اینکه نام، خود یک فعل باشد که عملکرد یک تابع را توصیف میکند). این خط باید با یک حرف بزرگ شروع شود و با نقطه پایان یابد.
اگر خطوط بیشتری در رشتهٔ مستندسازی وجود دارد، خط دوم باید خالی باشد تا بهصورت بصری خلاصه را از ادامهٔ توضیحات جدا کند. خطوط بعدی باید شامل یک یا چند پاراگراف باشند که قراردادهای فراخوانی شیء، عوارض جانبی آن و موارد مشابه را توضیح میدهند.
تجزیهگر پایتون هنگام استفاده از رشتههای چندخطی بهعنوان رشتههای مستندسازی ماژول، کلاس یا تابع، تورفتگی را از آنها حذف میکند.
در اینجا یک نمونه از یک رشتهٔ مستندسازی چندخطی آمده است:
>>> 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()
>>>
4.9.8. حاشیهنویسیهای تابع¶
حاشیهنویسیهای تابع فرادادههای کاملاً اختیاری دربارهٔ نوعهای استفادهشده در توابع تعریفشده توسط کاربر هستند (برای اطلاعات بیشتر به 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'
4.10. میانپرده: سبک کدنویسی¶
اکنون که قصد دارید بخشهای طولانیتر و پیچیدهتری از کد پایتون بنویسید، زمان مناسبی است تا دربارهٔ سبک کدنویسی صحبت کنیم. بیشتر زبانها را میتوان به سبکهای مختلفی نوشت (یا دقیقتر، قالببندی کرد)؛ برخی از این سبکها خواناتر از دیگران هستند. آسانتر کردن خواندن کد برای دیگران همیشه ایدهٔ خوبی است و پیروی از یک سبک کدنویسی مناسب کمک بسیار زیادی به این موضوع میکند.
برای پایتون، PEP 8 بهعنوان راهنمای سبکی مطرح شده است که بیشتر پروژهها از آن پیروی میکنند؛ این راهنما سبکی بسیار خوانا و چشمنواز را ترویج میکند. هر توسعهدهندهٔ پایتون باید در مقطعی آن را مطالعه کند؛ در ادامه مهمترین نکات آن را برای شما استخراج کردهایم:
از تورفتگی ۴ فاصلهای استفاده کنید و از Tab استفاده نکنید.
۴ فاصله سازشی مناسب بین تورفتگی کم (که امکان تودرتو شدن بیشتر را فراهم میکند) و تورفتگی زیاد (که خواندن را آسانتر میکند) است. استفاده از Tab باعث سردرگمی میشود و بهتر است کنار گذاشته شود.
خطوط را طوری شکستهبندی کنید که بیشتر از ۷۹ نویسه نباشند.
این کار به کاربرانی که نمایشگرهای کوچک دارند کمک میکند و امکان نمایش چندین فایل کد در کنار یکدیگر را در نمایشگرهای بزرگتر فراهم میسازد.
برای جدا کردن توابع و کلاسها، و همچنین بلوکهای بزرگتر کد درون توابع، از خطوط خالی استفاده کنید.
در صورت امکان، توضیحات را در یک خط جداگانه قرار دهید.
از رشتههای مستندسازی (docstring) استفاده کنید.
در اطراف عملگرها و پس از ویرگولها فاصله قرار دهید، اما مستقیماً درون سازههای محصورکننده فاصله نگذارید:
a = f(1, 2) + g(3, 4).کلاسها و توابع خود را بهصورت یکدست نامگذاری کنید؛ قرارداد رایج این است که برای کلاسها از
UpperCamelCaseو برای توابع و متدها ازlowercase_with_underscoresاستفاده شود. همیشه ازselfبهعنوان نام اولین آرگومان متد استفاده کنید (برای اطلاعات بیشتر دربارهٔ کلاسها و متدها به در نگاه اول به کلاسها مراجعه کنید).اگر قرار است کد شما در محیطهای بینالمللی استفاده شود، از کدگذاریهای پیچیده و خاص استفاده نکنید. کدگذاری پیشفرض پایتون، یعنی UTF-8، یا حتی ASCII ساده، در هر صورت بهترین انتخابها هستند.
به همین ترتیب، اگر حتی کوچکترین احتمالی وجود دارد که افرادی با زبانهای دیگر این کد را بخوانند یا نگهداری کنند، از نویسههای غیر ASCII در شناسهها استفاده نکنید.
پانویسها