برنامه‌نویسی پایتون فهرست مطالب
فصل ۱۳

توابع پیشرفته

فصل پایانی توابع. توابعی که هر تعداد آرگومان می‌پذیرند، توابعی که نوعشان مستند است، و توابعی که خودشان را صدا می‌زنند.

در این فصل می‌خوانید
  1. آرگومان‌های ستاره‌دار
  2. آرگومان‌های دوستاره
  3. همه با هم و ترتیبشان
  4. تحویل دادن آرگومان‌ها به تابع دیگر
  5. راهنمای نوع روی متغیر
  6. راهنمای نوع روی تابع
  7. راهنمای نوع مجموعه‌ها
  8. راهنمای نوع، بررسی نیست
  9. دام پیش‌فرض تغییرپذیر
  10. مستندنویسی
  11. بازگشت

۱. آرگومان‌های ستاره‌دار

گاهی نمی‌دانید چند آرگومان به تابعتان داده می‌شود. تابع print خودش همین‌طور است: می‌شود یک مقدار یا ده مقدار به آن داد. یک ستاره پیش از نام پارامتر، این توانایی را به تابع شما هم می‌دهد.

# A '*' in front of a parameter collects ALL the extra positional arguments
# into a tuple. The name 'args' is only a convention.
def sum_numbers(*args):
    print('args is:', args, type(args))
    total = 0
    for number in args:
        total += number
    return total

print(sum_numbers(1, 2, 3))
print(sum_numbers(10, 20, 30, 40))
print(sum_numbers())              # an empty tuple -> 0

# A '*' at the CALL site does the opposite: it spreads a list into arguments.
numbers = [5, 10, 15]
print(sum_numbers(*numbers))

آرگومان‌های ستاره‌دار: نمونه 01_args.py

args is: (1, 2, 3) <class 'tuple'>
6
args is: (10, 20, 30, 40) <class 'tuple'>
100
args is: () <class 'tuple'>
0
args is: (5, 10, 15) <class 'tuple'>
30

شرح مثال

۲. آرگومان‌های دوستاره

# '**' collects all the extra KEYWORD arguments into a dictionary.
def show_info(**kwargs):
    print('kwargs is:', kwargs, type(kwargs))
    for key, value in kwargs.items():
        print(key, ':', value)

show_info(name='Ali', age=22, city='Rasht')
print()
show_info(Riyazi=20, Farsi=19, Dini=12, Varzesh=18)

# '**' at the call site spreads a dictionary into keyword arguments.
data = {'name': 'Sara', 'age': 19}
show_info(**data)

آرگومان‌های دوستاره: نمونه 02_kwargs.py

دو ستاره، آرگومان‌های نام‌دار را در یک دیکشنری جمع می‌کند. دیکشنری موضوع فصل بعد است؛ فعلاً همین‌قدر بدانید که هر مقدار در آن یک کلید دارد. متد items() کلید و مقدار را با هم می‌دهد.

۳. همه با هم و ترتیبشان

# The complete order of the parameters:
#   normal, with a default, *args, **kwargs
def demo(a, b=10, *args, **kwargs):
    print('a:', a)
    print('b:', b)
    print('args:', args)
    print('kwargs:', kwargs)

demo(1, 20, 30, 40, name='Ali', city='Rasht')
print()
demo(1)

ترتیب کامل: نمونه 03_args_and_kwargs.py

a: 1
b: 20
args: (30, 40)
kwargs: {'name': 'Ali', 'city': 'Rasht'}

a: 1
b: 10
args: ()
kwargs: {}
ترتیبنوع پارامترمثال
۱معمولیa
۲دارای پیش‌فرضb=10
۳بقیه بی‌نام‌ها*args
۴بقیه نام‌دارها**kwargs

۴. تحویل دادن آرگومان‌ها به تابع دیگر

رایج‌ترین کاربرد واقعی این دو ابزار این است: تابعی بسازید که هر چه گرفت، دست‌نخورده به تابع دیگری بدهد.

# A very common use of *args / **kwargs: accept anything and hand it over
# unchanged to another function. Here we build our own print().
def show(*args, **kwargs):
    print(*args, **kwargs)

def main():
    show('Hello World!', end=' *** ')
    show('Hello Kadoos!', end=' *** ')
    show()
    show('a', 'b', 'c', sep='-')

if __name__ == '__main__':
    main()

تحویل دادن: نمونه 04_forwarding.py

تابع show دقیقاً مثل print رفتار می‌کند، چون هر چه بگیرد پاس می‌دهد. اگر روزی بخواهید همه پیام‌های برنامه را در فایل هم بنویسید، فقط همین یک تابع را عوض می‌کنید. در فصل بیست و ششم می‌بینید که دکوراتورها دقیقاً روی همین تکنیک ساخته شده‌اند.

۵. راهنمای نوع روی متغیر

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

# A type hint says WHAT KIND of value a name is supposed to hold.
# It changes nothing at run time, but the editor can then warn us.
number: int = 23
PI: float = 3.14
name: str = 'Kadoos'
is_active: bool = True

user_input: str = input('Enter a number: ')
my_number: float = float(user_input)

print(number, PI, name, is_active, my_number)

راهنمای نوع متغیر: نمونه 05_type_hints_variables.py

۶. راهنمای نوع روی تابع

# Parameters are annotated with ': type', the result with '-> type'.
def add(number1: float, number2: float) -> float:
    return number1 + number2

def greet(name: str) -> str:
    return f'Hello {name}'

# A function that returns nothing is annotated with '-> None'.
def main() -> None:
    print(add(3.2, 2.0))
    print(greet('Kadoos'))

if __name__ == '__main__':
    main()

راهنمای نوع تابع: نمونه 06_type_hints_functions.py

نوع پارامتر با دونقطه و نوع خروجی با فلش نوشته می‌شود. تابعی که چیزی برنمی‌گرداند، -> None می‌گیرد. از این فصل به بعد، همه کدهای این کتاب راهنمای نوع دارند.

۷. راهنمای نوع مجموعه‌ها

# Hints for collections say what is INSIDE them.
def get_total(numbers: list[int]) -> int:
    return sum(numbers)

def show_student(data: dict[str, int]) -> None:
    print(data)

# '|' means "one type OR the other".
def add(number1: int | float = 1, number2: int | float = 1) -> int | float:
    return number1 + number2

# 'X | None' is used for a value that may be missing.
def find_name(names: list[str], target: str) -> str | None:
    for name in names:
        if name == target:
            return name
    return None

# A long signature is easier to read spread over several lines.
def register(
    number1: float = 1.0,
    number2: float = 1.0,
    name: str = 'Kadoos',
    active: bool = True,
) -> float:
    return number1 + number2

راهنمای نوع مجموعه‌ها: نمونه 07_type_hints_collections.py

نوشتاریعنی
list[int]لیستی از عددهای صحیح
dict[str, int]دیکشنری با کلید رشته و مقدار عدد
int | floatیا صحیح یا اعشاری
str | Noneیا رشته یا هیچ. برای وقتی ممکن است چیزی پیدا نشود
-> Noneتابع چیزی برنمی‌گرداند

نوشتار str | None بسیار مهم است و به خواننده هشدار می‌دهد که خروجی ممکن است None باشد، پس باید بررسی‌اش کند. بسیاری از خطاهای واقعی برنامه‌ها از همین جا می‌آیند که کسی فراموش کرده None را بررسی کند.

۸. راهنمای نوع، بررسی نیست

# Python does NOT check the type hints while the program runs.
def add(a: int, b: int) -> int:
    return a + b

print(add(2, 3))          # 5, as expected

# The hints say "int", but this call still works, because '+' also works
# on two strings. The editor will warn us; Python will not.
print(add('Hi', 'Hello'))  # 'HiHello'

# If we really want to refuse wrong types, we must check by hand.
def strict_add(a: int, b: int) -> int:
    if not isinstance(a, int) or not isinstance(b, int):
        raise TypeError('strict_add() only accepts integers')
    return a + b

print(strict_add(2, 3))
# strict_add('Hi', 'Hello')  ->  TypeError

راهنما اجباری نیست: نمونه 08_hints_are_not_checks.py

سوءتفاهم رایج

راهنمای نوع هیچ چیزی را اجبار نمی‌کند. پایتون هنگام اجرا کاملاً نادیده‌شان می‌گیرد. آنها برای انسان و برای ویرایشگر نوشته می‌شوند، نه برای مفسر. اگر واقعاً می‌خواهید نوع را کنترل کنید، باید با isinstance دستی بررسی کنید و خطا بیندازید. دستور raise موضوع فصل نوزدهم است.

۹. دام پیش‌فرض تغییرپذیر

این یکی از معروف‌ترین دام‌های پایتون است و حتی برنامه‌نویس‌های باتجربه هم گاهی گرفتارش می‌شوند.

# A default value is created ONCE, when the function is defined.
# With a list as a default, all the calls end up sharing the SAME list.
def add_student_wrong(name, students=[]):
    students.append(name)
    return students

print(add_student_wrong('Ali'))     # ['Ali']
print(add_student_wrong('Sara'))    # ['Ali', 'Sara']  <- surprise!

# The correct pattern uses None as the default.
def add_student(name: str, students: list[str] | None = None) -> list[str]:
    if students is None:
        students = []
    students.append(name)
    return students

print(add_student('Ali'))           # ['Ali']
print(add_student('Sara'))          # ['Sara']

existing = ['Taghi', 'Naghi']
print(add_student('Rahman', existing))

دام پیش‌فرض: نمونه 09_mutable_default_trap.py

شرح مثال

۱۰. مستندنویسی

'''Module docstring: a short description of what this file does.'''


def area_of_rectangle(length: float, width: float) -> float:
    '''Return the area of a rectangle.

    Args:
        length: the length of the rectangle.
        width: the width of the rectangle.

    Returns:
        The area, i.e. length * width.
    '''
    return length * width


def main() -> None:
    print(area_of_rectangle(3, 4))

    # A docstring is stored in the function and can be read at run time.
    print(area_of_rectangle.__doc__)

    # help() displays it nicely.
    help(area_of_rectangle)


if __name__ == '__main__':
    main()

مستندنویسی: نمونه 10_docstrings.py

شرح مثال

۱۱. بازگشت

بازگشت یعنی تابعی که خودش را صدا می‌زند. هر تابع بازگشتی حتماً باید یک حالت پایه داشته باشد، یعنی شرطی که در آن دیگر خودش را صدا نمی‌زند. بدون آن، برنامه تا بی‌نهایت پیش می‌رود.

# A recursive function is a function that calls itself.
# It always needs a BASE CASE, otherwise it never stops.

def factorial(n: int) -> int:
    'n! computed recursively.'
    if n <= 1:            # base case
        return 1
    return n * factorial(n - 1)


def fibonacci(n: int) -> int:
    'The n-th Fibonacci number (0, 1, 1, 2, 3, 5, ...).'
    if n < 2:             # two base cases: fib(0) = 0 and fib(1) = 1
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


def countdown(n: int) -> None:
    'Print n, n-1, ... 1 and then "Go!".'
    if n == 0:
        print('Go!')
        return
    print(n)
    countdown(n - 1)

بازگشت: نمونه 11_recursion.py

اجرای factorial(4) را دنبال کنیم:

factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1                 <- base case
                                 -> 2 * 1  = 2
                                 -> 3 * 2  = 6
                                 -> 4 * 6  = 24

شرح مثال

پس بازگشت کجا واقعاً به درد می‌خورد

جایی که خود مسئله ساختار بازگشتی دارد: پیمایش پوشه‌ها و زیرپوشه‌ها، پردازش یک درخت، تحلیل عبارت‌های ریاضی تودرتو. برای شمردن ساده و جمع زدن، حلقه بهتر است.

و در پایان، یک برنامه کامل با همه چیزهای این فصل:

'''A fully annotated and documented version of the prime test.'''


def is_prime(number: int) -> bool:
    '''Return True when 'number' is a prime number.'''
    if number < 2:
        return False

    # Testing the divisors up to the square root is enough.
    divisor: int = 2
    while divisor * divisor <= number:
        if number % divisor == 0:
            return False
        divisor += 1

    return True


def main() -> None:
    user_input: str = input('Enter a number: ')
    number: int = int(user_input)

    if is_prime(number):
        print(number, 'is prime.')
    else:
        print(number, 'is NOT prime.')


if __name__ == '__main__':
    main()

نمونه کامل: نمونه 12_prime_with_hints.py

فایل‌های تمرینی این فصل

پوشه S13-functions-advanced شامل نمونه‌های 01_args.py تا 12_prime_with_hints.py.

خلاصه فصل

ابزاروظیفه آن
*argsجمع کردن آرگومان‌های بی‌نام اضافی در یک تاپل
**kwargsجمع کردن آرگومان‌های نام‌دار اضافی در یک دیکشنری
f(*lst)باز کردن یک لیست به آرگومان‌های جدا
ترتیب پارامترهامعمولی، پیش‌فرض‌دار، *args، **kwargs
x: int و -> strراهنمای نوع. فقط مستند است، اجبار نیست
list[int] str | Noneراهنمای نوع مجموعه‌ها و نوع اختیاری
def f(x=[])خطرناک. پیش‌فرض را None بگذارید
مستندرشته سه‌گیومه‌ای زیر def، قابل دیدن با help()
بازگشتتابعی که خودش را صدا می‌زند. حتماً حالت پایه لازم دارد

تمرین‌های فصل سیزده

  1. تابع maximum(*args) را بنویسید که بزرگ‌ترین مقدار از هر تعداد ورودی را برگرداند.
  2. تابع make_profile(**kwargs) را بنویسید که هر کلید و مقداری که گرفت را چاپ کند.
  3. به همه توابعی که در فصل یازدهم نوشتید، راهنمای نوع و مستند اضافه کنید.
  4. تابع join_texts(*texts, separator=' ') را بنویسید که متن‌ها را به هم بچسباند.
  5. با مثال خودتان توضیح دهید چرا def f(items=[]) خطرناک است.
  6. تابع sum_to(n) را به شکل بازگشتی بنویسید.
  7. تابع reverse_text(text) را به شکل بازگشتی بنویسید.
  8. تابع power(base, exponent) را به شکل بازگشتی بنویسید.
  9. زمان اجرای نسخه بازگشتی و نسخه حلقه‌ای factorial(20) را با هم مقایسه کنید.
فصل قبل: آرگومان‌ها و حوزه متغیر فصل بعد: دیکشنری، تاپل و مجموعه