Избегайте повторяющихся и дорогостоящих операций с использованием условного предложения

Рассмотрим следующее понимание списка:

 >>> def f(x):
...     import time
...     time.sleep(.1)       # Simulate expensive function
...     return x**2

>>> [f(x) for x in range(1000) if f(x) > 10]
[16, 25, 36, ...]


 

Это приводит к двум вызовами f(x) для 1000 значений x : один вызов для генерации значения , а другой для проверки , if это условие. Если f(x) является особенно дорогостоящей операцией, это может существенно влиять на производительность. Хуже того , если вы звоните f() имеет побочные эффекты, это может иметь неожиданные результаты.

Вместо этого, вы должны оценить дорогостоящую операцию только один раз для каждого значения x путем генерации промежуточного Iterable ( выражение генератора ) следующим образом :

 >>> [v for v in (f(x) for x in range(1000)) if v > 10]
[16, 25, 36, ...]

 

Или, используя встроенную карту эквивалент:

 >>> [v for v in map(f, range(1000)) if v > 10]
[16, 25, 36, ...]


 

Другой способ , который может привести к более читаемый код, чтобы поместить частичный результат ( v в предыдущем примере) в качестве итератора (например, список или кортеж) , а затем итерации над ним. Поскольку v будет единственным элементом в Iterable, результат является то , что мы теперь имеют ссылку на выход нашей медленной функции , вычисленной только один раз:

 >>> [v for x in range(1000) for v in [f(x)] if v > 10]
[16, 25, 36, ...]

 

Однако на практике логика кода может быть более сложной, и важно, чтобы она была читабельной. В общем, отдельные генератор функции рекомендуются более сложным однострочник:

>>> def process_prime_numbers(iterable):
...     for x in iterable:
...         if is_prime(x):
...             yield f(x)
...
>>> [x for x in process_prime_numbers(range(1000)) if x > 10]
[11, 13, 17, 19, ...]



Другой способ предотвращения вычислений f(x) несколько раз, чтобы использовать @functools.lru_cache() (Python 3.2+) декоратор на f(x) . Таким образом , так как выход из f для входного x уже вычислен один раз, второй вызов функции из исходного списка понимания будет так быстро , как поиск в словаре. Этот подход использует мемоизация для повышения эффективности, что сопоставимо с использованием выражений генератора.

Скажем, вы должны сгладить список

 l = [[1, 2, 3], [4, 5, 6], [7], [8, 9]]

 

Некоторые из методов могут быть:

 reduce(lambda x, y: x+y, l)

sum(l, [])

list(itertools.chain(*l))

 

Однако понимание списка обеспечит лучшую временную сложность.

 [item for sublist in l for item in sublist]

 

Ярлыки, основанные на + (включая подразумеваемое использование в сумме), по необходимости, O (L ^ 2), когда есть L подсписков - поскольку список промежуточных результатов продолжает увеличиваться, на каждом шаге новый объект списка промежуточных результатов получает выделены, и все элементы в предыдущем промежуточном результате должны быть скопированы (а также несколько новых добавлены в конце). Поэтому (для простоты и без фактической потери общности) скажем, что у вас есть L подсписков из I элементов каждый: первые I элементы копируются туда-сюда L-1 раз, вторые I-элементы L-2 раза и т. Д .; общее количество копий равно I, умноженному на сумму x для x от 1 до L, т.е. I * (L ** 2) / 2.

Понимание списка только генерирует один список, один раз, и копирует каждый элемент (из его первоначального места жительства в список результатов) также ровно один раз.