توابع پیشرفته
فصل پایانی توابع. توابعی که هر تعداد آرگومان میپذیرند، توابعی که نوعشان مستند است، و توابعی که خودشان را صدا میزنند.
۱. آرگومانهای ستارهدار
گاهی نمیدانید چند آرگومان به تابعتان داده میشود. تابع
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
شرح مثال
- ستاره همه آرگومانهای اضافی را در یک تاپل جمع میکند. تاپل شبیه لیست است ولی تغییرناپذیر؛ در فصل بعد کاملاً میبینیدش.
- نام
argsفقط یک قرارداد است.*numbersهم کاملاً درست است، ولی همه دنیاargsمینویسند. - اگر هیچ آرگومانی داده نشود، تاپل خالی میشود و حلقه اصلاً اجرا نمیشود.
- ستاره در محل صدا زدن کار برعکس میکند: یک لیست را باز میکند و اعضایش را جدا جدا به تابع میدهد.
۲. آرگومانهای دوستاره
# '**' 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
شرح مثال
- مقدار پیشفرض یک بار ساخته میشود، آن هم موقع تعریف تابع، نه در هر صدا زدن.
- پس آن لیست خالی، بین همه فراخوانیها مشترک است و هر بار چیزی به آن اضافه میشود.
- راه حل استاندارد و جهانی: پیشفرض را
Noneبگذارید و داخل تابع، اگرNoneبود لیست تازه بسازید. - این قاعده برای هر نوع تغییرپذیری صادق است: لیست، دیکشنری و مجموعه. برای عدد و رشته مشکلی نیست چون تغییرناپذیرند.
- دقت کنید که برای مقایسه با
Noneازisاستفاده میکنیم نه==. این قرارداد پایتون است.
۱۰. مستندنویسی
'''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
شرح مثال
- مستند یا docstring، یک رشته سهگیومهای است که بلافاصله بعد از خط
defمیآید. - فرقش با توضیح معمولی این است که در خود برنامه ذخیره میشود و با
help()قابل دیدن است. توضیح با#فقط در فایل میماند. - ساختار استاندارد: یک خط خلاصه، خط خالی، بعد شرح پارامترها و خروجی.
- فایل هم میتواند مستند داشته باشد؛ کافی است اولین چیز فایل یک رشته سهگیومهای باشد.
- در ویرایشگر، وقتی موس را روی نام تابع نگه دارید همین متن را میبینید. برای تابعی که خودتان شش ماه بعد استفاده میکنید، نعمت بزرگی است.
۱۱. بازگشت
بازگشت یعنی تابعی که خودش را صدا میزند. هر تابع بازگشتی حتماً باید یک حالت پایه داشته باشد، یعنی شرطی که در آن دیگر خودش را صدا نمیزند. بدون آن، برنامه تا بینهایت پیش میرود.
# 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
شرح مثال
- هر تابع بازگشتی دو بخش دارد: حالت پایه که مستقیم جواب میدهد، و حالت بازگشتی که مسئله را کوچکتر میکند و دوباره خودش را صدا میزند.
- مسئله باید در هر مرحله واقعاً کوچکتر شود، وگرنه به حالت پایه نمیرسیم.
- اگر حالت پایه را فراموش کنید، پایتون بعد از حدود هزار فراخوانی تودرتو خطای
RecursionErrorمیدهد. - بازگشت زیباست ولی رایگان نیست: هر فراخوانی حافظه میگیرد. نسخه حلقهای فاکتوریل هم سریعتر است و هم کممصرفتر.
- فیبوناچی بازگشتی مثال بدی از کارایی است:
fibonacci(35)چند ثانیه طول میکشد چون بارها مقدارهای تکراری را دوباره حساب میکند. در فصل بیست و ششم باlru_cacheهمین را در یک چشم به هم زدن حل میکنیم.
جایی که خود مسئله ساختار بازگشتی دارد: پیمایش پوشهها و زیرپوشهها، پردازش یک درخت، تحلیل عبارتهای ریاضی تودرتو. برای شمردن ساده و جمع زدن، حلقه بهتر است.
و در پایان، یک برنامه کامل با همه چیزهای این فصل:
'''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() |
| بازگشت | تابعی که خودش را صدا میزند. حتماً حالت پایه لازم دارد |
تمرینهای فصل سیزده
- تابع
maximum(*args)را بنویسید که بزرگترین مقدار از هر تعداد ورودی را برگرداند. - تابع
make_profile(**kwargs)را بنویسید که هر کلید و مقداری که گرفت را چاپ کند. - به همه توابعی که در فصل یازدهم نوشتید، راهنمای نوع و مستند اضافه کنید.
- تابع
join_texts(*texts, separator=' ')را بنویسید که متنها را به هم بچسباند. - با مثال خودتان توضیح دهید چرا
def f(items=[])خطرناک است. - تابع
sum_to(n)را به شکل بازگشتی بنویسید. - تابع
reverse_text(text)را به شکل بازگشتی بنویسید. - تابع
power(base, exponent)را به شکل بازگشتی بنویسید. - زمان اجرای نسخه بازگشتی و نسخه حلقهای
factorial(20)را با هم مقایسه کنید.