Python پایتون ۳.۱۴
فصل ۴

ابزارهای بیشتر برای کنترل جریان

📖 ۲۰ دقیقه 🎯 متوسط 🔄 ۲۰۲۶

علاوه بر دستور 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 یا پاسکال به آن عادت داشته باشید متفاوت است. به جای اینکه همیشه روی یک تصاعد عددی تکرار کند (مانند پاسکال)، یا به کاربر امکان تعریف گام تکرار و شرط توقف را بدهد (مانند 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
...

۴.۳. تابع range()

اگر واقعاً نیاز به تکرار روی دنباله‌ای از اعداد دارید، تابع درون‌ساخت range() به کار می‌آید. این تابع تصاعدهای عددی تولید می‌کند:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

نقطهٔ پایانی داده‌شده هرگز بخشی از دنبالهٔ تولیدشده نیست؛ range(10) ده مقدار تولید می‌کند، اندیس‌های مجاز برای آیتم‌های یک دنباله به طول ۱۰. می‌توان اجازه داد که range از عدد دیگری شروع شود، یا گام متفاوتی تعیین کرد (حتی منفی؛ گاهی اوقات به این 'گام' گفته می‌شود):

>>> 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() طوری رفتار می‌کند که انگار یک لیست است، اما در واقع این‌طور نیست. این یک شیء است که وقتی روی آن تکرار می‌کنید، آیتم‌های متوالی دنبالهٔ مورد نظر را برمی‌گرداند، اما واقعاً لیست را نمی‌سازد، بنابراین در فضا صرفه‌جویی می‌کند.

می‌گوییم چنین شیئی تکرارپذیر است، یعنی به‌عنوان هدفی برای توابع و ساختارهایی که انتظار چیزی را دارند که بتوانند آیتم‌های متوالی را از آن دریافت کنند تا وقتی که تأمین تمام شود، مناسب است. دیده‌ایم که دستور 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» مراجعه کنید.

۴.۷. دستور match

دستور match یک عبارت را می‌گیرد و مقدار آن را با الگوهای متوالی که به‌صورت یک یا چند بلوک case داده شده‌اند مقایسه می‌کند. این به‌طور سطحی شبیه به دستور switch در C، جاوا یا جاوااسکریپت (و بسیاری زبان‌های دیگر) است، اما بیشتر شبیه به تطبیق الگو در زبان‌هایی مانند 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"
...

به آخرین بلوک توجه کنید: «نام متغیر» _ به‌عنوان یک کاراکتر عمومی عمل می‌کند و هرگز در مطابقت شکست نمی‌خورد.

می‌توانید چندین مقدار تحت‌اللفظی را با استفاده از | ("یا") در یک الگو ترکیب کنید:

        case 401 | 403 | 404:
            return "Not allowed"

الگوها می‌توانند شبیه تخصیص‌های بازکردن بسته به نظر برسند و می‌توانند برای مقید کردن متغیرها استفاده شوند:

>>> # 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 در بالا) هرگز به آن‌ها اختصاص داده نمی‌شود.

الگوها می‌توانند به‌طور دلخواه تودرتو شوند. برای مثال، اگر یک لیست کوتاه از نقاط داشته باشیم، با __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 به یک الگو اضافه کنیم که به‌عنوان «نگهبان» شناخته می‌شود. اگر نگهبان نادرست باشد، 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")
...

چندین ویژگی کلیدی دیگر این دستور:

  • مانند تخصیص‌های بازکردن بسته، الگوهای tuple و list دقیقاً معنای یکسانی دارند و در واقع دنباله‌های دلخواه را مطابقت می‌دهند. یک استثنای مهم این است که آن‌ها تکرارگرها یا رشته‌ها را مطابقت نمی‌دهند.
  • الگوهای دنباله از بازکردن بستهٔ توسعه‌یافته پشتیبانی می‌کنند: [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 جذب خواهد کرد (تا زمانی که ورودی یک دنباله از دو نقطه باشد).
  • بیشتر مقادیر تحت‌اللفظی با تساوی مقایسه می‌شوند، با این حال Singletonهای 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 که به‌صورت یک آموزش نوشته شده است مراجعه کنید.

۴.۸. تعریف توابع

می‌توانیم تابعی ایجاد کنیم که سری فیبوناچی را تا یک مرز دلخواه بنویسد:

>>> 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ها برای تولید خودکار مستندات آنلاین یا چاپی استفاده می‌کنند، یا به کاربر امکان می‌دهند به‌طور تعاملی کد را مرور کند؛ گنجاندن docstring در کدی که می‌نویسید تمرین خوبی است، بنابراین آن را به عادت تبدیل کنید.

اجرای یک تابع یک جدول نماد جدید را که برای متغیرهای محلی تابع استفاده می‌شود، معرفی می‌کند. به‌طور دقیق‌تر، همهٔ تخصیص‌های متغیر در یک تابع، مقدار را در جدول نماد محلی ذخیره می‌کنند؛ در حالی که ارجاعات متغیر ابتدا در جدول نماد محلی جست‌وجو می‌شوند، سپس در جداول نماد محلی توابع محصورکننده، سپس در جدول نماد سراسری، و در نهایت در جدول نام‌های درون‌ساخت. بنابراین، متغیرهای سراسری و متغیرهای توابع محصورکننده نمی‌توانند مستقیماً درون یک تابع مقداردهی شوند (مگر اینکه برای متغیرهای سراسری، در یک دستور global نام برده شوند، یا برای متغیرهای توابع محصورکننده، در یک دستور nonlocal نام برده شوند)، اگرچه می‌توان به آن‌ها ارجاع داد.

پارامترهای واقعی (آرگومان‌ها) برای فراخوانی تابع، هنگام فراخوانی در جدول نماد محلی تابع فراخوانی‌شده معرفی می‌شوند؛ بنابراین، آرگومان‌ها با استفاده از فراخوانی با مقدار ارسال می‌شوند (جایی که مقدار همیشه یک ارجاع به شیء است، نه مقدار شیء). [1] وقتی یک تابع، تابع دیگری را فراخوانی می‌کند، یا خودش را به‌صورت بازگشتی فراخوانی می‌کند، یک جدول نماد محلی جدید برای آن فراخوانی ایجاد می‌شود.

یک تعریف تابع، نام تابع را با شیء تابع در جدول نماد جاری مرتبط می‌کند. مفسر شیء اشاره‌شده توسط آن نام را به‌عنوان یک تابع تعریف‌شده توسط کاربر می‌شناسد. نام‌های دیگر نیز می‌توانند به همان شیء تابع اشاره کنند و همچنین می‌توانند برای دسترسی به تابع استفاده شوند:

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

اگر از زبان‌های دیگر می‌آیید، ممکن است اعتراض کنید که fib یک تابع نیست بلکه یک رویه است زیرا مقداری برنمی‌گرداند. در واقع، حتی توابع بدون دستور 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

عدد 5 را چاپ خواهد کرد.

هشدار مهم: مقدار پیش‌فرض فقط یک بار ارزیابی می‌شود. این وقتی تفاوت ایجاد می‌کند که پیش‌فرض یک شیء تغییرپذیر مانند لیست، دیکشنری یا نمونه‌هایی از بیشتر کلاس‌ها باشد. برای مثال، تابع زیر آرگومان‌های ارسال‌شده به خود را در فراخوانی‌های بعدی انباشته می‌کند:

>>> def f(a, L=[]):
...     L.append(a)
...     return L
...
>>> print(f(1))
[1]
>>> print(f(2))
[1, 2]
>>> print(f(3))
[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 حضور داشته باشد، یک دیکشنری دریافت می‌کند (به «انواع نگاشت — 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

توجه کنید که ترتیب چاپ آرگومان‌های کلیدواژه‌ای تضمین می‌شود که با ترتیبی که در فراخوانی تابع ارائه شده‌اند مطابقت داشته باشد.

۴.۹.۳. پارامترهای ویژه

به‌طور پیش‌فرض، آرگومان‌ها می‌توانند به یک تابع پایتون یا بر اساس موقعیت یا به‌طور صریح با کلیدواژه ارسال شوند. برای خوانایی و کارایی، منطقی است که نحوهٔ ارسال آرگومان‌ها را محدود کنیم تا یک توسعه‌دهنده فقط با نگاه کردن به تعریف تابع تشخیص دهد که آیا آیتم‌ها بر اساس موقعیت، موقعیت یا کلیدواژه، یا کلیدواژه ارسال می‌شوند.

یک تعریف تابع ممکن است شبیه این باشد:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional 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 اگر نام پارامتر در آینده تغییر کند، از فقط-موقعیتی استفاده کنید.

۴.۹.۴. لیست‌های آرگومان دلخواه

در نهایت، کم‌ترین گزینهٔ استفاده‌شده این است که تعیین کنیم یک تابع می‌تواند با تعداد دلخواهی از آرگومان‌ها فراخوانی شود. این آرگومان‌ها در یک tuple بسته‌بندی خواهند شد (به «تاپل‌ها و دنباله‌ها» مراجعه کنید). قبل از تعداد متغیر آرگومان‌ها، صفر یا چند آرگومان عادی می‌توانند وجود داشته باشند.

>>> 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'

۴.۹.۵. بازکردن بستهٔ لیست‌های آرگومان

وضعیت معکوس زمانی رخ می‌دهد که آرگومان‌ها از قبل در یک لیست یا tuple هستند اما باید برای فراخوانی تابعی که به آرگومان‌های موقعیتی جداگانه نیاز دارد، باز شوند. برای نمونه، تابع درون‌ساخت range() انتظار آرگومان‌های start و stop جداگانه دارد. اگر به‌طور جداگانه در دسترس نیستند، فراخوانی تابع را با عملگر * بنویسید تا آرگومان‌ها از یک لیست یا tuple باز شوند:

>>> 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 ایجاد کرد. این تابع مجموع دو آرگومانش را برمی‌گرداند: lambda a, b: a+b. توابع Lambda می‌توانند هر جایی که اشیاء تابع لازم هستند استفاده شوند. آن‌ها از نظر نحوی به یک عبارت منفرد محدود هستند. از نظر معنایی، آن‌ها فقط شکر نحوی برای یک تعریف تابع عادی هستند. مانند تعاریف تابع تو در تو، توابع lambda می‌توانند به متغیرهای حوزهٔ محصورکننده ارجاع دهند:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

مثال بالا از یک عبارت lambda برای برگرداندن یک تابع استفاده می‌کند. کاربرد دیگر ارسال یک تابع کوچک به‌عنوان آرگومان است. برای نمونه، list.sort() یک تابع کلید مرتب‌سازی key می‌گیرد که می‌تواند یک تابع lambda باشد:

>>> 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')]

۴.۹.۷. رشته‌های مستندات

در اینجا برخی قراردادها دربارهٔ محتوا و قالب‌بندی رشته‌های مستندات آمده است.

خط اول همیشه باید یک خلاصهٔ کوتاه و مختصر از هدف شیء باشد. برای ایجاز، نباید به‌طور صریح نام یا نوع شیء را بیان کند، زیرا این‌ها از راه‌های دیگر در دسترس هستند (مگر اینکه نام تصادفاً یک فعل باشد که عملکرد یک تابع را توصیف می‌کند). این خط باید با یک حرف بزرگ شروع شود و با نقطه پایان یابد.

اگر خطوط بیشتری در رشتهٔ مستندات وجود دارد، خط دوم باید خالی باشد تا خلاصه را به‌صورت بصری از بقیهٔ توضیحات جدا کند. خطوط بعدی باید یک یا چند پاراگراف باشند که قراردادهای فراخوانی شیء، اثرات جانبی آن و غیره را توصیف می‌کنند.

تجزیه‌گر پایتون تورفتگی را از رشته‌های تحت‌اللفظی چندخطی وقتی به‌عنوان 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()
    >>>

۴.۹.۸. حاشیه‌نویسی توابع

حاشیه‌نویسی توابع اطلاعات فراداده‌ای کاملاً اختیاری دربارهٔ انواع استفاده‌شده توسط توابع تعریف‌شده توسط کاربر هستند (برای اطلاعات بیشتر به 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ها سردرگمی ایجاد می‌کنند و بهتر است کنار گذاشته شوند.
  • خطوط را طوری بشکنید که از ۷۹ نویسه تجاوز نکنند. این به کاربران با نمایشگرهای کوچک کمک می‌کند و امکان داشتن چندین فایل کد کنار هم روی نمایشگرهای بزرگ‌تر را فراهم می‌کند.
  • از خطوط خالی برای جدا کردن توابع و کلاس‌ها، و بلوک‌های بزرگ‌تر کد درون توابع استفاده کنید.
  • در صورت امکان، توضیحات را در یک خط جداگانه قرار دهید.
  • از docstring استفاده کنید.
  • دور عملگرها و بعد از کاماها فاصله بگذارید، اما نه مستقیماً درون ساختارهای کروشه‌ای: a = f(1, 2) + g(3, 4).
  • کلاس‌ها و توابع خود را به‌طور یکنواخت نام‌گذاری کنید؛ قرارداد این است که از UpperCamelCase برای کلاس‌ها و lowercase_with_underscores برای توابع و متدها استفاده کنید. همیشه از self به‌عنوان نام برای اولین آرگومان متد استفاده کنید (برای اطلاعات بیشتر در مورد کلاس‌ها و متدها به «نگاه اول به کلاس‌ها» مراجعه کنید).
  • اگر قرار است کدتان در محیط‌های بین‌المللی استفاده شود، از کدگذاری‌های فانتزی استفاده نکنید. پیش‌فرض پایتون، UTF-8، یا حتی ASCII ساده در هر صورت بهترین کارایی را دارد.
  • به همین ترتیب، از نویسه‌های غیر ASCII در شناسه‌ها استفاده نکنید اگر حتی کمترین شانسی وجود دارد که افرادی که به زبان دیگری صحبت می‌کنند کد را بخوانند یا نگهداری کنند.

پانویس‌ها

[1] در واقع، «فراخوانی با ارجاع به شیء» توصیف بهتری خواهد بود، زیرا اگر یک شیء تغییرپذیر ارسال شود، فراخوان هر تغییری که فراخوانی‌شده در آن ایجاد کند (مانند آیتم‌های درج‌شده در یک لیست) را خواهد دید.