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

کد پایتونی، بخش دوم

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

در این فصل می‌خوانید
  1. تابع lambda
  2. مرتب‌سازی با کلید
  3. توابع map و filter
  4. ماژول functools
  5. اولین دکوراتور
  6. دکوراتور wraps
  7. دکوراتور با پارامتر
  8. دکوراتور زمان‌سنجی
  9. مولدها
  10. خط لوله مولدها
  11. ماژول itertools

۱. تابع 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

شرح مثال

۲. مرتب‌سازی با کلید

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

شرح مثال

۴. ماژول 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

چطور کار می‌کند

  1. @logger بالای def add دقیقاً یعنی add = logger(add). همین و بس.
  2. logger تابع اصلی را می‌گیرد و تابع wrapper را برمی‌گرداند.
  3. از این به بعد، هر بار که add صدا زده شود، در عمل wrapper اجرا می‌شود.
  4. wrapper کارهای اضافه را انجام می‌دهد، تابع اصلی را صدا می‌زند و نتیجه‌اش را برمی‌گرداند.
فراموش نکردن return

اگر در 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

شرح مثال

۱۰. خط لوله مولدها

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

سه درس این مثال

۱۱. ماژول 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

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جعبه‌ابزار آماده کار با دنباله‌ها

تمرین‌های فصل بیست و شش

  1. لیستی از دانشجویان را با sorted(key=...) اول بر اساس نمره و بعد بر اساس نام مرتب کنید.
  2. سه فراخوانی map و filter را به درک لیستی تبدیل کنید و خوانایی‌شان را مقایسه کنید.
  3. دکوراتور @count_calls بنویسید که بگوید تابع چند بار صدا زده شده است.
  4. دکوراتور @retry(times) بنویسید که اگر تابع خطا داد، دوباره صدایش بزند.
  5. دکوراتور @cache_result بنویسید که نتیجه‌ها را در دیکشنری نگه دارد و آن را با lru_cache مقایسه کنید.
  6. مولد even_numbers() بنویسید که تا بی‌نهایت اعداد زوج تولید کند.
  7. مولدی بنویسید که فایلی را بخواند و فقط خط‌های حاوی یک کلمه خاص را بدهد.
  8. خط لوله‌ای از سه مولد روی فایل اعداد بسازید: خواندن، فیلتر مثبت‌ها، و جمع زدن.
  9. با itertools.combinations همه جفت‌های یک لیست از نام‌ها را چاپ کنید.
  10. با itertools.groupby لیستی از دانشجویان را بر اساس شهر گروه کنید. به خاطر داشته باشید اول مرتب کنید.
فصل قبل: کد پایتونی، بخش اول فصل بعد: CSV و JSON