🗳 LRU-кэш

Кэш нужен, чтобы запоминать результаты каких-то тяжелых операций: вычислений, доступа к диску или запросов в сеть. В Python есть отличный декоратор, чтобы элегантно снабдить вашу функцию кэшированием: @functools.lru_cache(maxsize=128, typed=False)

LRU значит least recently used. Кэшу задают максимальный размер, и при его достижении элементы начинают вытесняться. Первыми вытесняются элементы, неиспользованные дольше всех, а свободные места занимают свежие элементы.

📎 Пример. Если аргумент декорируемой функции не встречался в кэше, то выполнится тело функции за полсекунды, результат будет сохранен в кэше. И в следующий раз тело функции не будет вызвано, а вернется результат из кэша:

from functools import lru_cache
import time

@lru_cache(maxsize=4)
def slow_sqr(i):
    print(f'Calculating sqr for {i}...')
    time.sleep(0.5)  # задумаемся...
    return i ** 2

for i in [1, 2, 3, 1, 3, 4, 4, 1]:
    print(f'i = {i}  => i ** 2 = {slow_sqr(i)}')

print(slow_sqr.cache_info())
# CacheInfo(hits=4, misses=4, maxsize=4, currsize=4)

📎 Пример. Можно не задавать ограничение размеру кэша. Вычислим числа Фибоначчи рекурсивным методом. Без @lru_cache этот метод экстремально неэффективный с ростом n. Однако, с кэшированием – это уже динамическое программирование и работает сравнительно быстро:

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print(fib(500))

print(fib.cache_info())

• В основе кэша – словарь, поэтому все аргументы функции должны быть хэшируемы (hash). Надеюсь, никто не путает хэш (hash) и кэш (cache).

• Вызовы f(a=1, b=2) и f(b=2, a=1) – разные и создадут две записи в кэше.

• Рекомендуется ставить maxsize как степень двойки.

f.cache_info() – информация о размере кэша и статистике его работы

f.cache_clear() – очистка кэша

• Если аргумент декоратора typed=True, то аргументы разных типов будут кэшироваться отдельно. Пример: f(3) и f(3.0) будут отвечать разным записям в кэше.

🤩 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway! 👈

Тонкости try

Что вернет функция foo()?
def foo():
    try:
        return 'try'
    finally:
        return 'finally'

foo()

Правильный ответ будет ‘finally’:

Дело в том, что функция возвращает результат последнего выполненного return. А, учитывая, что блок finally всегда выполняется, то будет выполнено два return, последний из них будет return ‘finally’.

Что будет при вложенных блоках finally?
# вспомогательная функция, чтобы считать return-ы
def returner(s):
    print(f'  return {s}')
    return s

def foo():
    try:
        return returner('try')
    finally:
        return returner('finally')

print('Result: ', foo())

print('-' * 50)

def baz():
    try:
        try:
            return returner('try')
        finally:
            return returner('finally inner')
    finally:
        return returner('finally outer')

print('Result: ', baz())

Вывод:

  return try
  return finally
Result:  finally
--------------------------------------------------
  return try
  return finally inner
  return finally outer
Result:  finally outer

Как видим срабатывают все return (срабатывают, значит вычисляются аргументы выражения return), но будет возвращен из функции результат только последнего return.

Еще один коварный вопрос про try и finally.

Что будет при выполнении кода?


for i in range(10):
    try:
        print(1 / i)
    finally:
        print('finally')
        break

На первой итерации цикла произойдет исключение из-за деления на 0. Блока except нет. Но тем не менее исключение все равно будет подавлено, потому что в блоке finally есть break. Вот такая особенность языка. В будущих версиях (3.8+) тоже самое должно работать и с конструкцией continue.

🧙 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

🤭 В Python 3.8 будет оператор «морж»

Python 3.8 все еще в разработке, но уже можно полистать список грядущих изменений, и, пожалуй, самое значимое из них (и возможно единственное заметное изменение) – ввод нового оператора присваивания := (морж). Старички вспомнили Паскаль. Смысл этого оператора – дать имя результату выражения. Т.е. вычисляем правую часть моржа, связываем с именем переменной слева и возвращаем результат моржа наружу.

Раньше делали так:

x = len(s)
if x:
    print(x)

Будем делать так:

if x := len(s):  # можно в 3.8
    print(x)

Мотивация введения оператора := состоит в том, что уже наработано много примеров кода, когда он делает запись более лаконичной, не вызывая при этом повторного вычисления выражений.

📎 Пример. Используем вычисленное однажды значение f(x) под именем y:

[y := f(x), y**2, y**3]

📎 Пример. Читаем, сохраняем в chunk и сразу проверяем условие цикла:

while chunk := file.read(8192):
   process(chunk)

📎 Пример. Можно применить в проходах по спискам, чтобы дважды не вычислять f(x):

filtered_data = [y for x in data if (y := f(x)) is not None]

📎 Примеры можно/нельзя:

x := 5    # нельзя
(x := 5)  # можно
x = y := 0  # нельзя
x = (y := 0)  # можно

Приоритет запятой возле нового оператора. Сравните:

x = 1, 2     # x -> (1, 2)
(x := 1, 2)  # x -> 1

P.S.: Казалось бы, почему не сделать так: if x = len(s)? Ответ: чтобы не путать с if x == len(s). В C-подобных языках это частая проблема.

Специально для канала @pyway.