کد پایتونی، بخش دوم
در پایتون، تابع هم یک مقدار است: میشود در متغیر ریختش، به تابع دیگری داد و از تابع برگرداندش. این فصل درباره همین ایده و پیامدهای قدرتمندش است.
۱. تابع lambda
# 'lambda arguments: expression' creates a function without a name.
# It can only contain ONE expression, and it returns it automatically.
double = lambda x: x * 2
print(double(5))
# Exactly the same thing, written normally - and this version is better,
# because the function then has a real name in the tracebacks.
def double_v2(x):
return x * 2
print(double_v2(5))
# A lambda earns its place when a function is needed for one single moment,
# usually as an argument of another function.
students = [('Ali', 18), ('Sara', 20), ('Reza', 12)]
print(sorted(students, key=lambda student: student[1]))
# Several parameters are allowed.
add = lambda a, b: a + b
print(add(2, 3))
تابع بینام: نمونه 01_lambda.py
شرح مثال
lambdaیک تابع بینام میسازد که فقط یک عبارت دارد و همان را برمیگرداند.returnندارد و نمیتواند داشته باشد.- جایش آنجاست که یک تابع کوچک برای یک لحظه لازم است، معمولاً به عنوان آرگومان تابعی دیگر.
- اگر میخواهید به lambda نام بدهید و در متغیر بریزیدش، همان
defرا بنویسید. کوتاهتر نمیشود و در پیامهای خطا نام واقعی نشان میدهد.
۲. مرتبسازی با کلید
students = [
{'name': 'Ali', 'score': 18, 'age': 20},
{'name': 'Sara', 'score': 20, 'age': 19},
{'name': 'Reza', 'score': 12, 'age': 22},
]
# 'key' is a function that says WHAT to compare for each item.
print(sorted(students, key=lambda s: s['score']))
print(sorted(students, key=lambda s: s['score'], reverse=True))
# Sorting by two criteria: return a tuple.
print([s['name'] for s in sorted(students, key=lambda s: (-s['score'], s['name']))])
# operator.itemgetter / attrgetter are the faster, ready-made version.
from operator import itemgetter
print([s['name'] for s in sorted(students, key=itemgetter('age'))])
# min / max accept the same key.
print(max(students, key=lambda s: s['score'])['name'])
# Sorting words by length, then alphabetically.
words = ['banana', 'kiwi', 'apple', 'fig']
print(sorted(words, key=lambda w: (len(w), w)))
مرتبسازی با کلید: نمونه 02_sort_with_key.py
یک: برای مرتبسازی چندمعیاره، تاپل برگردانید.
پایتون اول عضو اول را مقایسه میکند و در صورت تساوی، عضو دوم را.
دو: برای اینکه یک معیار نزولی و بقیه صعودی باشند،
علامت منفی جلوی معیار عددی بگذارید:
(-s['score'], s['name']) یعنی نمره از زیاد به کم و در
نمره برابر، نام الفبایی.
۳. توابع map و filter
numbers = [1, 2, 3, 4, 5, 6]
# map() applies a function to every item.
print(list(map(lambda x: x ** 2, numbers)))
# The comprehension says the same thing more clearly:
print([x ** 2 for x in numbers])
# filter() keeps the items for which the function returns True.
print(list(filter(lambda x: x % 2 == 0, numbers)))
print([x for x in numbers if x % 2 == 0])
# map() shines when the function already exists and has a name.
words = ['10', '20', '30']
print(list(map(int, words)))
print(list(map(str.upper, ['ali', 'sara'])))
# Both return an ITERATOR, not a list: they compute nothing until you loop.
result = map(lambda x: x * 2, numbers)
print(result) # <map object ...>
print(list(result)) # now it is computed
print(list(result)) # [] - an iterator can only be read once!
map و filter: نمونه 03_map_filter.py
شرح مثال
mapیک تابع را روی همه اعضا اعمال میکند وfilterاعضایی را نگه میدارد که تابع برایشانTrueبرگرداند.- در بیشتر موارد، درک لیستی خواناتر است. جامعه پایتون هم همان را ترجیح میدهد.
- استثنا: وقتی تابع از قبل نام دارد، مثل
map(int, words)، شکلmapتمیزتر است. - هر دو یک تکرارگر برمیگردانند، نه لیست. تا وقتی رویش حلقه نزنید یا
list()نکنید، هیچ محاسبهای انجام نمیشود. و مهمتر: فقط یک بار قابل خواندن است. خط آخر مثال، دام رایجی را نشان میدهد.
۴. ماژول functools
from functools import reduce, partial, lru_cache
import time
# reduce() folds a sequence into a single value.
numbers = [1, 2, 3, 4, 5]
print(reduce(lambda a, b: a + b, numbers)) # 15 - but sum() is better
print(reduce(lambda a, b: a * b, numbers)) # 120 - here it is useful
# partial() freezes some arguments of a function.
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5), cube(5))
# lru_cache remembers the results: a huge win for recursive functions.
def slow_fibonacci(n):
if n < 2:
return n
return slow_fibonacci(n - 1) + slow_fibonacci(n - 2)
@lru_cache(maxsize=None)
def fast_fibonacci(n):
if n < 2:
return n
return fast_fibonacci(n - 1) + fast_fibonacci(n - 2)
start = time.perf_counter()
slow_fibonacci(30)
print(f'without cache: {time.perf_counter() - start:.3f} s')
start = time.perf_counter()
fast_fibonacci(30)
print(f'with cache: {time.perf_counter() - start:.6f} s')
ماژول functools: نمونه 04_functools.py
15 120 25 125 without cache: 0.253 s with cache: 0.000027 s
| ابزار | وظیفه آن |
|---|---|
reduce | تا کردن یک دنباله به یک مقدار. برای جمع، sum بهتر است |
partial | ثابت کردن بعضی آرگومانهای یک تابع |
lru_cache | حفظ نتیجهها. برای توابع بازگشتی معجزه میکند |
آن اختلاف زمانی را ببینید: فیبوناچی بازگشتی فصل سیزدهم که چند ثانیه طول
میکشید، با یک خط @lru_cache عملاً آنی میشود. دلیلش این
است که هر مقدار فقط یک بار محاسبه و بعد ذخیره میشود.
۵. اولین دکوراتور
دکوراتور تابعی است که یک تابع میگیرد و تابع تازهای برمیگرداند. با آن میشود رفتاری به تابع اضافه کرد بدون دست زدن به کد خودش.
# A decorator is a function that takes a function and returns a new one.
def logger(func):
def wrapper(*args, **kwargs):
print(f'Call {func.__name__} with {args} {kwargs}')
result = func(*args, **kwargs)
print(f'{func.__name__} returned {result}')
return result # never forget to return the result!
return wrapper
@logger # exactly the same as: add = logger(add)
def add(num1: float, num2: float) -> float:
return num1 + num2
def main() -> None:
total = add(3, 4)
# Without the '@' syntax it would be written like this:
def sub(a, b):
return a - b
sub = logger(sub)
sub(10, 3)
اولین دکوراتور: نمونه 05_decorator_basics.py
Call add with (3, 4) {}
add returned 7
چطور کار میکند
@loggerبالایdef addدقیقاً یعنیadd = logger(add). همین و بس.loggerتابع اصلی را میگیرد و تابعwrapperرا برمیگرداند.- از این به بعد، هر بار که
addصدا زده شود، در عملwrapperاجرا میشود. wrapperکارهای اضافه را انجام میدهد، تابع اصلی را صدا میزند و نتیجهاش را برمیگرداند.
اگر در wrapper بنویسید func(*args) و
نتیجهاش را برنگردانید، تابع تزیینشده همیشه None
میدهد. این رایجترین باگ دکوراتورهاست و پیدا کردنش سخت است، چون
هیچ خطایی نمیدهد.
استفاده از *args, **kwargs باعث میشود دکوراتور با
هر تابعی کار کند، مهم نیست چند پارامتر داشته باشد.
این همان تکنیک فصل سیزدهم است.
۶. دکوراتور wraps
from functools import wraps
# Problem: a naive decorator replaces the function, so its name and its
# docstring disappear.
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def good_decorator(func):
@wraps(func) # copies __name__, __doc__, ... onto the wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def hello_bad():
'Say hello.'
@good_decorator
def hello_good():
'Say hello.'
print(hello_bad.__name__, '|', hello_bad.__doc__) # wrapper | None
print(hello_good.__name__, '|', hello_good.__doc__) # hello_good | Say hello.
دکوراتور wraps: نمونه 06_decorator_wraps.py
بدون @wraps، نام و مستند تابع اصلی گم میشوند و
help() چیز بیفایدهای نشان میدهد. یک خط است و همیشه
بنویسیدش.
۷. دکوراتور با پارامتر
from functools import wraps
# A decorator that takes parameters needs THREE levels:
# the factory -> the decorator -> the wrapper
def repeat(times: int):
'Run the decorated function "times" times.'
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name: str) -> None:
print(f'Hello {name}!')
def require_positive(func):
'A decorator that validates the arguments.'
@wraps(func)
def wrapper(*args, **kwargs):
for value in args:
if isinstance(value, (int, float)) and value < 0:
raise ValueError(f'{func.__name__} refuses negative values')
return func(*args, **kwargs)
return wrapper
@require_positive
def area(width: float, height: float) -> float:
return width * height
دکوراتور با پارامتر: نمونه 07_decorator_with_arguments.py
وقتی دکوراتور پارامتر بگیرد، سه سطح تابع لازم است.
دلیلش ساده است: @repeat(3) یعنی اول repeat(3)
صدا زده میشود و نتیجهاش دکوراتور واقعی است. پس
بیرونیترین تابع باید یک دکوراتور برگرداند.
۸. دکوراتور زمانسنجی
'''The most useful decorator of everyday life: measuring a function.'''
import time
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f'{func.__name__} took {elapsed:.4f} seconds')
return result
return wrapper
@timing
def sum_with_loop(n: int) -> int:
total = 0
for i in range(n):
total += i
return total
@timing
def sum_with_builtin(n: int) -> int:
return sum(range(n))
if __name__ == '__main__':
sum_with_loop(5_000_000)
sum_with_builtin(5_000_000)
زمانسنجی: نمونه 08_timing_decorator.py
sum_with_loop took 0.3120 seconds sum_with_builtin took 0.0680 seconds
این دکوراتور را در هر پروژهای کپی کنید. برای پیدا کردن اینکه کدام بخش برنامه کند است، فقط کافی است یک خط بالای تابع بگذارید و بعد برش دارید.
۹. مولدها
# A function that contains 'yield' is a GENERATOR: it does not run when it is
# called; it runs a little more every time you ask for the next value.
def simple_gen():
print('Start')
yield 1
print('Yield after 1')
yield 2
print('Yield after 2')
yield 3
print('Finish!')
def main() -> None:
g = simple_gen()
print(g) # <generator object ...> - nothing has run yet
print(next(g)) # runs until the first yield
print(next(g))
print(next(g))
# A fourth next() would raise StopIteration - that is exactly what a
# 'for' loop catches for us.
try:
next(g)
except StopIteration:
print('the generator is exhausted')
print('--- with a for loop ---')
for value in simple_gen():
print('got', value)
# A generator can only be read once.
g2 = simple_gen()
print(list(g2))
print(list(g2)) # [] - already exhausted
مولد: نمونه 09_generators.py
شرح مثال
- هر تابعی که
yieldداشته باشد، مولد است. صدا زدنش هیچ کدی را اجرا نمیکند و فقط یک شیء مولد میسازد. - هر
next()تابع را تاyieldبعدی اجرا میکند و همان جا متوقفش میکند. حالت متغیرها حفظ میشود. - وقتی تابع تمام شود،
StopIterationمیاندازد. حلقهforخودش این را میگیرد، پس شما هیچوقت ضرورتی ندارد به آن فکر کنید. - مولد یک بار مصرف است. اگر دو بار لازمش دارید، دو بار بسازیدش.
۱۰. خط لوله مولدها
'''Why generators matter: laziness and memory.'''
def primes():
'An INFINITE generator of prime numbers.'
number = 2
while True:
for divisor in range(2, int(number ** 0.5) + 1):
if number % divisor == 0:
break
else:
yield number
number += 1
def read_large_file(path: str):
'Yield the lines of a file one by one, whatever its size.'
with open(path, 'r', encoding='utf-8') as f:
for line in f:
yield line.rstrip('\n')
def main() -> None:
# An infinite generator is fine as long as we stop asking.
generator = primes()
for _ in range(20):
print(next(generator), end=' ')
print()
# A list comprehension builds everything in memory,
# a generator expression computes on demand.
import sys
as_list = [x ** 2 for x in range(100_000)]
as_generator = (x ** 2 for x in range(100_000))
print('list :', sys.getsizeof(as_list), 'bytes')
print('generator :', sys.getsizeof(as_generator), 'bytes')
# A pipeline: each step is lazy, nothing is stored.
lines = read_large_file('demo.txt')
non_empty = (line for line in lines if line.strip())
numbers = (int(line) for line in non_empty)
positives = (n for n in numbers if n > 0)
print('sum of the positive numbers:', sum(positives))
خط لوله: نمونه 10_generator_pipeline.py
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 list : 800984 bytes generator : 200 bytes
سه درس این مثال
- دنباله بیپایان. تابع
primesحلقهای بیپایان دارد ولی برنامه هنگ نمیکند، چون فقط هر وقت مقدار بخواهید یک قدم جلو میرود. - حافظه. لیست صدهزارتایی نزدیک هشتصد کیلوبایت میگیرد و مولد فقط دویست بایت. برای فایل چندگیگابایتی، این تفاوت بین کار کردن و از کار افتادن است.
- خط لوله. چهار مرحله پشت سر هم چیده شدهاند و هیچکدام تا لحظه
sumاجرا نمیشوند. آن وقت دادهها یکییکی از کل لوله عبور میکنند. این الگو در پردازش دادههای بزرگ حیاتی است.
۱۱. ماژول itertools
import itertools
# count() - an infinite counter
for i in itertools.count(10, 5):
if i > 30:
break
print(i, end=' ')
print()
# cycle() - repeats a sequence for ever
colors = itertools.cycle(['red', 'green', 'blue'])
print([next(colors) for _ in range(7)])
# chain() - glues several sequences into one
print(list(itertools.chain([1, 2], [3, 4], 'ab')))
# islice() - a slice of any iterator (even an infinite one)
print(list(itertools.islice(itertools.count(), 5)))
# combinations() / permutations() / product()
print(list(itertools.combinations('ABC', 2)))
print(list(itertools.permutations('ABC', 2)))
print(list(itertools.product([1, 2], 'ab')))
# groupby() - groups CONSECUTIVE items, so sort first!
students = [
{'name': 'Ali', 'city': 'Rasht'},
{'name': 'Sara', 'city': 'Rasht'},
{'name': 'Reza', 'city': 'Lahijan'},
]
students.sort(key=lambda s: s['city'])
for city, group in itertools.groupby(students, key=lambda s: s['city']):
print(city, [s['name'] for s in group])
# accumulate() - the running total
print(list(itertools.accumulate([1, 2, 3, 4])))
ماژول itertools: نمونه 11_itertools.py
| ابزار | وظیفه آن |
|---|---|
count(a, b) | شمارنده بیپایان |
cycle(seq) | تکرار بیپایان یک دنباله |
chain(a, b) | چسباندن چند دنباله |
islice(it, n) | برش از هر تکرارگر، حتی بیپایان |
combinations | همه ترکیبها، بدون اهمیت ترتیب |
permutations | همه جایگشتها، با اهمیت ترتیب |
product | ضرب دکارتی، مثل حلقه تودرتو |
groupby | گروهبندی اعضای پشت سر هم |
accumulate | مجموع در حال اجرا |
groupby فقط اعضای مجاور را گروه
میکند. اگر داده مرتب نباشد، یک شهر ممکن است چند بار در نتیجه
ظاهر شود. همیشه اول با همان کلید sort کنید. اگر
گروهبندی ساده میخواهید، defaultdict(list) فصل
پانزدهم راحتتر است.
پوشه S26-pythonic-code-2 شامل نمونههای
01_lambda.py تا 11_itertools.py.
خلاصه فصل
| ابزار | وظیفه آن |
|---|---|
lambda x: ... | تابع بینام یکخطی، برای استفاده یکباره |
sorted(key=...) | مرتبسازی بر اساس هر معیاری. تاپل برای چند معیار |
map و filter | معمولاً درک لیستی خواناتر است |
partial | ثابت کردن بعضی آرگومانها |
@lru_cache | حفظ نتیجههای تکراری |
| دکوراتور | تابعی که تابع میگیرد و تابع برمیگرداند |
@wraps(func) | حفظ نام و مستند تابع اصلی. همیشه بنویسیدش |
| دکوراتور با پارامتر | سه سطح تابع لازم دارد |
yield | ساختن مولد. تنبل، کمحافظه، یکبار مصرف |
(x for x in ...) | عبارت مولد |
itertools | جعبهابزار آماده کار با دنبالهها |
تمرینهای فصل بیست و شش
- لیستی از دانشجویان را با
sorted(key=...)اول بر اساس نمره و بعد بر اساس نام مرتب کنید. - سه فراخوانی
mapوfilterرا به درک لیستی تبدیل کنید و خواناییشان را مقایسه کنید. - دکوراتور
@count_callsبنویسید که بگوید تابع چند بار صدا زده شده است. - دکوراتور
@retry(times)بنویسید که اگر تابع خطا داد، دوباره صدایش بزند. - دکوراتور
@cache_resultبنویسید که نتیجهها را در دیکشنری نگه دارد و آن را باlru_cacheمقایسه کنید. - مولد
even_numbers()بنویسید که تا بینهایت اعداد زوج تولید کند. - مولدی بنویسید که فایلی را بخواند و فقط خطهای حاوی یک کلمه خاص را بدهد.
- خط لولهای از سه مولد روی فایل اعداد بسازید: خواندن، فیلتر مثبتها، و جمع زدن.
- با
itertools.combinationsهمه جفتهای یک لیست از نامها را چاپ کنید. - با
itertools.groupbyلیستی از دانشجویان را بر اساس شهر گروه کنید. به خاطر داشته باشید اول مرتب کنید.