Анафорическое понимание списка в Python

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

>>> def square(x): return x*x
... 
>>> [square(x) for x in range(12) if square(x) > 50]
[64, 81, 100, 121]

Мне нужно дважды называть квадрат (x) в понимании списка. Дублирование является уродливым, подверженным ошибкам (легко изменить только один из двух вызовов при изменении кода) и неэффективно.

Конечно, я могу это сделать:

>>> squares = [square(x) for x in range(12)]
>>> [s for s in squares if s > 50]
[64, 81, 100, 121]

или это:

[s for s in [square(x) for x in range(12)] if s > 50]

Они оба пригодны для жизни, но кажется, что может быть способ сделать все это в одном заявлении, не вставляя два понимания списка, и я знаю, что мне придется долго смотреть на него в следующий раз, Чтение кода просто для выяснения того, что происходит. Есть ли способ?

Я думаю, что справедливый вопрос, чтобы спросить меня, будет таким, как я себе представляю, такой синтаксис мог бы выглядеть. Вот две идеи, но они не кажутся идиоматическими в Python (и они не работают). Они вдохновлены анафорическими макросами в Lisp.

[square(x) for x in range(12) if it > 50]
[it=square(x) for x in range(12) if it > 50]

Ответ 1

Вы должны использовать генератор:

[s for s in (square(x) for x in range(12)) if s > 50]

Это позволяет избежать создания промежуточного нефильтрованного списка квадратов.

Ответ 2

Другая альтернатива, использующая "скованные" списки, а не вложенные:

[s for n in range(12) for s in [square(n)] if s > 50]

Возможно, это странное чтение.

Ответ 3

Ниже приведено сравнение вложенных генераторов vs "закованных в цепочку" списков и вычислений дважды

$ python -m timeit "[s for n in range(12) for s in [n * n] if s > 50]"
100000 loops, best of 3: 2.48 usec per loop
$ python -m timeit "[s for s in (x * x for x in range(12)) if s > 50]"
1000000 loops, best of 3: 1.89 usec per loop
$ python -m timeit "[n * n for n in range(12) if n * n > 50]"
1000000 loops, best of 3: 1.1 usec per loop

$ pypy -m timeit "[s for n in range(12) for s in [n * n] if s > 50]"
1000000 loops, best of 3: 0.211 usec per loop
$ pypy -m timeit "[s for s in (x * x for x in range(12)) if s > 50]"
1000000 loops, best of 3: 0.359 usec per loop
$ pypy -m timeit "[n * n for n in range(12) if n * n > 50]"
10000000 loops, best of 3: 0.0834 usec per loop

Я использовал n * n вместо square(n), потому что это было удобно и удаляет служебные данные вызова функции из benckmark

TL;DR: для простых случаев лучше всего просто продублировать вычисления.

Ответ 4

[square(s) for s in range(12) if s >= 7]  # sqrt(50) = 7.071...

Или даже проще (без ветвления, woo!)

[square(s) for s in range(7, 12)]  # sqrt(50) = 7.071...

Ответ 5

EDIT: я слепой, дублированный ответ Eevee.

Можно использовать итерацию по списку из 1 элемента для "связывания" промежуточных переменных:

[s for x in range(12) for s in [square(x)] if s > 50]

Я не решаюсь рекомендовать это как читаемое решение.

Pro: По сравнению с вложенным пониманием, я предпочитаю порядок здесь - наличие for x in range(12) снаружи. Вы можете просто читать его последовательно, вместо того, чтобы увеличивать масштаб, а затем отступать...

Con: for s in [...] является неидиоматическим взломом и может дать читателям паузу. Вложенное понимание, возможно, более трудное для расшифровки, по крайней мере, использует языковые функции "очевидным образом".

  • Идея: переименовать промежуточную переменную, что-то вроде tmp, я мог бы сделать ее более ясной.

Суть в том, что я тоже не доволен. Вероятно, наиболее читаемым является обозначение промежуточного генератора:

squares = (square(x) for x in range(12))
result = [s for s in squares if s > 50]

[Боковое примечание: наименования наименований генераторов немного редки. Но прочитайте лекцию Дэвида Бэйсли, и она может расти на вас.]

OTOH, если вы собираетесь писать такие конструкции много, пойдите для шаблона for tmp in [expr(x)] - он станет "локально идиоматическим" в вашем коде и когда-то знакомым, его компактность окупится. Моя озабоченность читабельностью больше связана с одноразовым использованием...