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 پایتون روی آیتمهای هر دنبالهای (یک فهرست یا یک رشته) به ترتیبی که در دنباله ظاهر شدهاند پیمایش میکند. برای مثال (بدون قصد جناس):
>>> # Measure some strings:
>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words:
... print(w, len(w))
...
cat 3
window 6
defenestrate 12
کدی که هنگام پیمایش روی یک مجموعه، همان مجموعه را تغییر میدهد، ممکن است بهدرستی نوشتن دشوار باشد. در عوض، معمولاً سادهتر است که روی یک کپی از مجموعه پیمایش کنید یا یک مجموعهی جدید ایجاد کنید:
# 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
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() برگردانده میشود طوری رفتار میکند که گویی یک فهرست است، اما در واقع چنین نیست. این یک شیء است که هنگام پیمایش، آیتمهای متوالی دنبالهی موردنظر را برمیگرداند، اما واقعاً فهرست را ایجاد نمیکند و در نتیجه در فضا صرفهجویی میشود.
We say such an object is iterable, that is, suitable as a target for
functions and constructs that expect something from which they can
obtain successive items until the supply is exhausted. We have seen that
the for statement is such a construct, while an example of a function
that takes an iterable is 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:
... # 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 دارد تا دستورهای if: بخش else یک دستور try زمانی اجرا میشود که هیچ استثنایی رخ ندهد، و بخش else یک حلقه زمانی اجرا میشود که هیچ break ای رخ ندهد. برای اطلاعات بیشتر دربارهی دستور try و استثناها، به مدیریت استثناها مراجعه کنید.
4.6. دستورهای 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 از مقدار لفظی سهنقطه ... استفاده میکنند. این استفاده هیچ معنای ویژهای برای پایتون ندارد و بخشی از تعریف زبان نیست (در اینجا میتوانستید از هر عبارت ثابتی استفاده کنید)، اما ... بهصورت قراردادی بهعنوان بدنهی جاینگهدار نیز استفاده میشود. به 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) باشند و برای مقداردهی به متغیرها استفاده شوند:
# 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) دریافت میکند. الگوی چهارم دو مقدار را دریافت میکند، که از نظر مفهومی شبیه به انتساب واگشاییٔ (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): # 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 یک تعریف تابع را معرفی میکند. این کلیدواژه باید پس از خود نام تابع و فهرست پارامترهای رسمی درون پرانتز بیاید. دستورهایی که بدنهی تابع را تشکیل میدهند از خط بعدی شروع میشوند و باید تورفتگی داشته باشند.
اولین دستور بدنهی تابع میتواند بهصورت اختیاری یک رشتهی لفظی باشد؛ این رشتهی لفظی، رشتهی مستندات تابع یا 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): # 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]است، اما کارآمدتر است.
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. آرگومانهای کلیدواژهای¶
توابع همچنین میتوانند با استفاده از آرگومان کلیدواژهای به شکل 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 وجود داشته باشد، یک دیکشنری (به 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))
معمولاً این آرگومانهای 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'
4.9.5. واگشایی آرگومانها¶
حالت معکوس زمانی رخ میدهد که آرگومانها از قبل در یک فهرست یا تاپل قرار دارند، اما برای فراخوانی تابعی که به آرگومانهای جایگاهی جداگانه نیاز دارد، باید واگشایی شوند. برای مثال، تابع توکار 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 !
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 باعث سردرگمی میشود و بهتر است کنار گذاشته شود.
سطرها را طوری شکستهبندی کنید که بیشتر از ۷۹ نویسه نباشند.
این کار به کاربرانی که نمایشگرهای کوچک دارند کمک میکند و امکان نمایش چندین پرونده کد در کنار یکدیگر را در نمایشگرهای بزرگتر فراهم میسازد.
برای جدا کردن توابع و کلاسها، و همچنین بلوکهای بزرگتر کد درون توابع، از سطرهای خالی استفاده کنید.
در صورت امکان، کامنتها را در یک خط جداگانه قرار دهید.
از رشتههای مستندسازی استفاده کنید.
در اطراف عملگرها و پس از ویرگولها فاصله قرار دهید، اما مستقیماً درون سازههای محصورکننده فاصله نگذارید:
a = f(1, 2) + g(3, 4).کلاسها و توابع خود را بهصورت یکدست نامگذاری کنید؛ قرارداد رایج این است که برای کلاسها از
UpperCamelCaseو برای توابع و متدها ازlowercase_with_underscoresاستفاده شود. همیشه ازselfبهعنوان نام اولین آرگومان متد استفاده کنید (برای اطلاعات بیشتر دربارهی کلاسها و متدها به در نگاه اول به کلاسها مراجعه کنید).اگر قرار است کد شما در محیطهای بینالمللی استفاده شود، از کدگذاریهای پیچیده و خاص استفاده نکنید. کدگذاری پیشفرض پایتون، یعنی UTF-8، یا حتی ASCII ساده، در هر صورت بهترین انتخابها هستند.
به همین ترتیب، اگر حتی کوچکترین احتمالی وجود دارد که افرادی با زبانهای دیگر این کد را بخوانند یا نگهداری کنند، از نویسههای غیر ASCII در شناسهها استفاده نکنید.
پانویسها