Как реализовать списковое включение без многократного вызова функции
Коллеги, дисклеймер. Есть вопрос, для которого я придумал абстрактный и совершенно бестолковый, с точки зрения реализации, пример. Пример этот служит исключительно для иллюстрации моего вопроса, а не является примером хорошего решения.
Вот пример:
import random
random.seed(123)
def test_random(len):
return [random.randint(0, 10) ** 2 for _ in range(len)]
print ([test_random(10)[i] for i in range(3)])
Есть функция, которая генерирует и возвращает некую коллекцию, к элементам которой можно обратится по индексу/ключу. Список, словарь, кортеж - не важно, в примере я использовал список, для простоты иллюстрации. При одинаковых входных данных, функция возвращает одинаковый результат. Мы, с помощью спискового/словарного включения, пытаемся сгенерировать какую-то другую коллекцию, на основе возвращенной из этой функции коллекции. Проблема очевидна - при такой реализации мы запрашиваем функцию каждый раз для каждой итерации спискового включения, вместо того, чтобы один раз получить объект и обращаться уже к нему.
Вопрос.
Возможна ли реализации подобной логики без многократных обращений к функции и без предварительного введения временной переменной, в которую я верну результат работы функции?
Ответы (3 шт):
Как вариант можно ещё один цикл for по списку из одного элемента в списковое включение вставить. Хотя кэширование, наверное, лучше. А ещё лучше - предварительно запомнить значение в переменную и потом её использовать. Вообще списковые включения - они не для сложных вещей. Если без включения код не получается написать, то это нормально, не нужно всё к ним сводить.
print([data[i] for data in [test_random(10)] for i in range(3)])
Лучше всё же в явном виде:
data = test_random(10)
print([data[i] for i in range(3)])
Если функция возвращает один и тот же результат при одних и тех же входных данных, можно использовать кэширование, чтобы результат не вычислялся постоянно заново при каждом вызове функции.
Вариант с декоратором @cache
from functools import cache
import random
random.seed(123)
@cache
def test_random(len):
return [random.randint(0, 10) ** 2 for _ in range(len)]
print([test_random(5) for i in range(3)])
Вывод:
[[0, 16, 1, 36, 16], [0, 16, 1, 36, 16], [0, 16, 1, 36, 16]]
Минус тут в том, что если это функция внутри библиотеки, то пользователь может не знать о наличии кэша, и пытаться добавить свои оптимизации. Т.е. есть неочевидность внутренней реализации.
Чтобы было очевидно, можно вручную добавить кэширующий вариант функции тем же декоратором, только используя его как функцию:
def test_random(len):
return [random.randint(0, 10) ** 2 for _ in range(len)]
cached_test_random = cache(test_random)
print ([cached_test_random(5) for i in range(3)])
Если нужна возможность очистки кэша, нужно использовать декоратор @lru_cache, тогда кэш можно будет очищать с помощью метода cache_clear(), который этот декоратор добавляет функции:
import random
from functools import lru_cache
random.seed(123)
@lru_cache
def test_random(len):
return [random.randint(0, 10) ** 2 for _ in range(len)]
print([test_random(5) for i in range(3)])
test_random.cache_clear()
lambda позволяет сохранить результат вычисления в параметре и затем использовать его много раз:
print((lambda lst: [lst[i] for i in range(3)])(test_random(10)))
:= сохраняет значение в начале кортежа, используется в конце кортежа. Технически ничем не отличается от предварительного введения временной переменной:
print((lst := test_random(10), [lst[i] for i in range(3)])[1])