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