Python · Синтаксис · Просунутий рівень
functools: partial, lru_cache та інше
Опануйте модуль functools: `partial`, `wraps`, `lru_cache`, `cached_property`, `total_ordering` та `singledispatch`.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
partial, reduce, патерн compose, cmp_to_key та @wraps
#total_ordering, singledispatch та повний довідник functools
#Вправи:
functools.partial — попереднє заповнення аргументів
#Використайте `functools.partial` для створення двох спеціалізованих функцій з `power(base, exp)`: `square(n)` — це `power(n, 2)`, `cube(n)` — `power(n, 3)`. Також створіть `add5 = partial(operator.add, 5)`, що додає 5 до будь-якого числа.
import functools
import operator
def power(base: int, exp: int) -> int:
return base ** exp
# Створіть square, cube та add5 через partial
print(square(4)) # 16
print(cube(3)) # 27
print(add5(10)) # 15
Рішення
import functools
import operator
def power(base: int, exp: int) -> int:
return base ** exp
square = functools.partial(power, exp=2)
cube = functools.partial(power, exp=3)
add5 = functools.partial(operator.add, 5)
print(square(4)) # 16
print(cube(3)) # 27
print(add5(10)) # 15
lru_cache для мемоізації
#Використайте `@functools.lru_cache(maxsize=None)` для мемоізації рекурсивної функції `fib(n)`. Виведіть `fib(35)`. Потім використайте `fib.cache_info()`, щоб показати кількість попадань vs промахів. Без кешування `fib(35)` зробив би ~29 мільйонів викликів.
import functools
# Додайте декоратор lru_cache тут
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(35)) # 9227465
print(fib.cache_info()) # CacheInfo(hits=..., misses=36, ...)
Рішення
import functools
@functools.lru_cache(maxsize=None)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(35)) # 9227465
print(fib.cache_info())
cached_property для затратних атрибутів
#Створіть клас `Circle` з `radius: float`. Додайте атрибут `area` через `@functools.cached_property`, що обчислює `math.pi * radius ** 2`. Перевірте, що звернення до `area` двічі повертає те саме значення без повторного обчислення (додайте `print('computing...')` всередині, щоб переконатися, що виводиться лише один раз).
import math
import functools
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
# Додайте cached_property для area тут
c = Circle(5)
print(c.area) # computing... потім 78.539...
print(c.area) # 78.539... (без 'computing...')
Рішення
import math
import functools
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
@functools.cached_property
def area(self) -> float:
print('computing...')
return math.pi * self.radius ** 2
c = Circle(5)
print(c.area)
print(c.area)
total_ordering — методи порівняння
#Використайте `@functools.total_ordering` для створення класу `Version` з полями `major, minor, patch`. Визначте лише `__eq__` та `__lt__`. Декоратор автоматично згенерує `__le__`, `__gt__`, `__ge__`. Перевірте, що всі порівняння працюють.
import functools
@functools.total_ordering
class Version:
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch
def _tuple(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other) -> bool:
pass
def __lt__(self, other) -> bool:
pass
v1 = Version(1, 2, 3)
v2 = Version(1, 3, 0)
print(v1 < v2) # True
print(v1 > v2) # False
print(v1 <= v1) # True
print(v1 == Version(1, 2, 3)) # True
Рішення
import functools
@functools.total_ordering
class Version:
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch
def _tuple(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other) -> bool:
return self._tuple() == other._tuple()
def __lt__(self, other) -> bool:
return self._tuple() < other._tuple()
v1 = Version(1, 2, 3)
v2 = Version(1, 3, 0)
print(v1 < v2) # True
print(v1 > v2) # False
print(v1 <= v1) # True
print(v1 == Version(1, 2, 3)) # True
singledispatch — диспетчеризація за типом
#Використайте `@functools.singledispatch` для написання функції `describe(value)` з різними реалізаціями для `int`, `str`, `list` та типовою для інших типів. int: `f'Integer: {value}'`, str: `f'String of length {len(value)}'`, list: `f'List with {len(value)} items'`, default: `f'Unknown type: {type(value).__name__}'`.
import functools
@functools.singledispatch
def describe(value) -> str:
pass # типова реалізація
# Зареєструйте реалізації для int, str, list тут
print(describe(42)) # Integer: 42
print(describe('hello')) # String of length 5
print(describe([1, 2, 3])) # List with 3 items
print(describe(3.14)) # Unknown type: float
Рішення
import functools
@functools.singledispatch
def describe(value) -> str:
return f'Unknown type: {type(value).__name__}'
@describe.register(int)
def _(value: int) -> str:
return f'Integer: {value}'
@describe.register(str)
def _(value: str) -> str:
return f'String of length {len(value)}'
@describe.register(list)
def _(value: list) -> str:
return f'List with {len(value)} items'
print(describe(42)) # Integer: 42
print(describe('hello')) # String of length 5
print(describe([1, 2, 3])) # List with 3 items
print(describe(3.14)) # Unknown type: float
functools.reduce
#Використайте `functools.reduce` для реалізації трьох операцій без вбудованих функцій: `product(nums)` — добуток всіх чисел, `my_max(nums)` — максимальне значення, `flatten_str(words)` — об'єднання слів через пробіл.
import functools nums = [2, 3, 4, 5] words = ['Привіт', 'світ', 'від', 'reduce'] product = functools.reduce(lambda a, b: a * b, nums) print(product) # 120 my_max = functools.reduce(lambda a, b: a if a > b else b, nums) print(my_max) # 5 flatten_str = functools.reduce(lambda a, b: a + ' ' + b, words) print(flatten_str) # Привіт світ від reduce
Рішення
import functools
nums = [2, 3, 4, 5]
words = ['Привіт', 'світ', 'від', 'reduce']
product = functools.reduce(lambda a, b: a * b, nums)
print(product) # 120
my_max = functools.reduce(lambda a, b: a if a > b else b, nums)
print(my_max) # 5
flatten_str = functools.reduce(lambda a, b: a + ' ' + b, words)
print(flatten_str)
functools.wraps у декораторі
#Напишіть декоратор `log_calls`, що виводить `'Calling <func_name>'` перед викликом обгорнутої функції. Використайте `@functools.wraps(func)` всередині, щоб обгортка зберігала `__name__` та `__doc__` оригінальної функції. Перевірте, що `decorated.__name__` є оригінальною назвою, а не `'wrapper'`.
import functools
def log_calls(func):
# Застосуйте @functools.wraps тут
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
return func(*args, **kwargs)
return wrapper
@log_calls
def greet(name: str) -> str:
'''Привітатися.'''
return f'Привіт, {name}!'
print(greet('Аліса')) # Calling greet / Привіт, Аліса!
print(greet.__name__) # greet (не 'wrapper')
print(greet.__doc__) # Привітатися.
Рішення
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
return func(*args, **kwargs)
return wrapper
@log_calls
def greet(name: str) -> str:
'''Привітатися.'''
return f'Привіт, {name}!'
print(greet('Аліса'))
print(greet.__name__) # greet
print(greet.__doc__) # Привітатися.
lru_cache з обмеженим розміром
#Напишіть функцію `slow_square(n)`, що симулює повільне обчислення (виводить `f'Computing {n}^2'` і повертає `n*n`). Застосуйте `@lru_cache(maxsize=3)`, щоб кешувалися лише 3 останні результати. Виклик: 1, 2, 3, 1 (кеш), 4 (витісняє 2), 2 (обчислюється знову).
import functools
# Додайте @lru_cache(maxsize=3) тут
def slow_square(n: int) -> int:
print(f'Computing {n}^2')
return n * n
print(slow_square(1)) # Computing 1^2 -> 1
print(slow_square(2)) # Computing 2^2 -> 4
print(slow_square(3)) # Computing 3^2 -> 9
print(slow_square(1)) # кеш -> 1
print(slow_square(4)) # Computing 4^2, витісняє 2 -> 16
print(slow_square(2)) # Computing 2^2 знову -> 4
Рішення
import functools
@functools.lru_cache(maxsize=3)
def slow_square(n: int) -> int:
print(f'Computing {n}^2')
return n * n
print(slow_square(1))
print(slow_square(2))
print(slow_square(3))
print(slow_square(1)) # з кешу
print(slow_square(4)) # витісняє 2
print(slow_square(2)) # обчислюється знову
partial як ключ сортування
#Використайте `functools.partial` для створення функції `sort_by_field(items, field)`. Потім створіть `sort_by_age = partial(sort_by_field, field='age')` та `sort_by_name = partial(sort_by_field, field='name')`. Застосуйте обидва до списку словників.
import functools
def sort_by_field(items, field):
return sorted(items, key=lambda x: x[field])
sort_by_age = functools.partial(sort_by_field, field='age')
sort_by_name = functools.partial(sort_by_field, field='name')
people = [
{'name': 'Чарлі', 'age': 30},
{'name': 'Аліса', 'age': 25},
{'name': 'Боб', 'age': 35},
]
for p in sort_by_age(people):
print(p['name'], p['age'])
for p in sort_by_name(people):
print(p['name'])
Рішення
import functools
def sort_by_field(items, field):
return sorted(items, key=lambda x: x[field])
sort_by_age = functools.partial(sort_by_field, field='age')
sort_by_name = functools.partial(sort_by_field, field='name')
people = [
{'name': 'Чарлі', 'age': 30},
{'name': 'Аліса', 'age': 25},
{'name': 'Боб', 'age': 35},
]
for p in sort_by_age(people):
print(p['name'], p['age'])
for p in sort_by_name(people):
print(p['name'])
Комбінування інструментів functools
#Поєднайте `@singledispatch` та `@lru_cache` для написання кешованої функції `process(value)`. Реалізація для `int` повертає `value * 2`, для `str` — `value.upper()`, типова — `str(value)`. Кешуйте реалізації для str та int з `maxsize=32`.
import functools
@functools.singledispatch
def process(value):
return str(value)
@process.register(int)
@functools.lru_cache(maxsize=32)
def _(value: int):
print(f'processing int {value}')
return value * 2
@process.register(str)
@functools.lru_cache(maxsize=32)
def _(value: str):
print(f'processing str {value}')
return value.upper()
print(process(5))
print(process(5)) # з кешу
print(process('hello'))
print(process(3.14))
Рішення
import functools
@functools.singledispatch
def process(value):
return str(value)
@process.register(int)
@functools.lru_cache(maxsize=32)
def _(value: int):
print(f'processing int {value}')
return value * 2
@process.register(str)
@functools.lru_cache(maxsize=32)
def _(value: str):
print(f'processing str {value}')
return value.upper()
print(process(5))
print(process(5)) # з кешу
print(process('hello'))
print(process(3.14))