Как реализовать списковое включение без многократного вызова функции

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

Вот пример:

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 шт):

Автор решения: CrazyElf

Как вариант можно ещё один цикл 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)])
→ Ссылка
Автор решения: insolor

Если функция возвращает один и тот же результат при одних и тех же входных данных, можно использовать кэширование, чтобы результат не вычислялся постоянно заново при каждом вызове функции.

Вариант с декоратором @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()
→ Ссылка
Автор решения: Stanislav Volodarskiy

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])
→ Ссылка