Что такое ленивая оценка в Python?

Оглавление

Быть ленивым - не всегда плохо. В каждой строке кода, которую вы пишете, есть по крайней мере одно выражение, которое Python должен вычислить. Ленивая оценка Python - это когда Python использует параметр lazy и откладывает обработку значения, возвращаемого выражением, до тех пор, пока это значение не понадобится.

Выражение в Python - это единица кода, которая преобразуется в значение. Примеры выражений включают имена объектов, вызовы функций, выражения с арифметическими операторами, литералы, которые создают встроенные типы объектов, такие как списки, и многое другое. Однако не все операторы являются выражениями. Например, операторы if и операторы цикла for не возвращают значения.

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

В этом тесте вы проверите свое понимание различий между ленивым и активным вычислением в Python. Выполняя этот тест, вы вернетесь к тому, как Python оптимизирует использование памяти и вычислительные издержки, решая, когда следует вычислять значения.

Вкратце: Ленивая оценка Python генерирует объекты только тогда, когда это необходимо

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

  1. Быстрая оценка
  2. Ленивая оценка

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

 1>>> 5 + 10
 215
 3
 4>>> import random
 5>>> random.randint(1, 10)
 64
 7
 8>>> [2, 4, 6, 8, 10]
 9[2, 4, 6, 8, 10]
10>>> numbers = [2, 4, 6, 8, 10]
11>>> numbers
12[2, 4, 6, 8, 10]

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

  • Строки 1 и 2: Первый пример содержит оператор сложения +, который Python вычисляет, как только встречает его. В REPL показано значение 15.
  • Строки с 4 по 6: Второй пример содержит две строки:
    • Инструкция import содержит ключевое слово import, за которым следует название модуля. Имя модуля random обрабатывается с готовностью.
    • Вызов функции random.randint() обрабатывается автоматически, и его значение возвращается немедленно. Все стандартные функции обрабатываются автоматически. Позже вы узнаете о функциях генератора, которые ведут себя по-разному.
  • Строки с 8 по 12: Последний пример содержит три строки кода:
    • Литерал для создания списка - это выражение, которое вычисляется автоматически. Это выражение содержит несколько целочисленных литералов, которые сами по себе являются выражениями, вычисляемыми немедленно.
    • Оператор присваивания присваивает объекту, созданному литералом списка, имя numbers. Этот оператор не является выражением и не возвращает значение. Однако он включает в себя литерал списка в правой части, который представляет собой выражение, которое быстро вычисляется.
    • В последней строке содержится имя numbers,, которое автоматически вычисляется для возврата объекта list.

Список, который вы создаете в последнем примере, создается полностью, когда вы его определяете. Python должен выделить память для списка и всех его элементов. Эта память не будет освобождена, пока этот список существует в вашей программе. Объем выделенной памяти в этом примере невелик и не повлияет на работу программы. Однако для более крупных объектов требуется больше памяти, что может вызвать проблемы с производительностью.

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

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

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

Пример отложенного вычисления выполняется в цикле for при выполнении итерации с использованием range():

for index in range(1, 1_000_001):
    print(f"This is iteration {index}")


Встроенный range() является конструктором для объекта range в Python. Объект range не хранит все из миллиона целых чисел, которые он представляет. Вместо этого цикл for создает range_iterator из объекта range, который генерирует следующее число в последовательности, когда это необходимо. Таким образом, программе никогда не нужно будет хранить все значения в памяти одновременно.

Отложенное вычисление также позволяет создавать бесконечные структуры данных, такие как живой поток аудио- или видеоданных, который постоянно пополняется новой информацией, поскольку программе не нужно сохранять все значения в памяти одновременно. Бесконечные структуры данных невозможны при быстром вычислении, поскольку они не могут быть сохранены в памяти.

У отложенного вычисления есть свои недостатки. Любые ошибки, вызванные выражением, также откладываются на более поздний этап работы программы. Эта задержка может затруднить отладку.

Отложенное вычисление целых чисел, представленных range() в цикле for, является одним из примеров отложенного вычисления. Вы узнаете о других примерах в следующем разделе этого руководства.

Каковы примеры отложенного вычисления в Python?

В предыдущем разделе вы узнали об использовании range() в цикле for, который приводит к отложенному вычислению целых чисел, представленных объектом range. В Python есть и другие выражения, которые приводят к отложенному вычислению. В этом разделе вы познакомитесь с основными из них.

Другие встроенные типы данных

Встроенные модули Python zip() и enumerate() создают два мощных встроенных типа данных. На следующем примере вы узнаете, как эти типы данных связаны с отложенной оценкой. Допустим, вам нужно составить еженедельное расписание, или rota, в котором указано, кто из членов команды будет приносить кофе по утрам.

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

>>> names = ["Sarah", "Matt", "Jim", "Denise", "Kate"]

>>> import random
>>> random.shuffle(names)
>>> names
['Sarah', 'Jim', 'Denise', 'Matt', 'Kate']

Вы также перетасовываете имена, используя random.shuffle(),, что изменяет список на месте. Пришло время составить нумерованный список, который можно будет прикреплять к доске объявлений каждую неделю:

>>> for index, name in enumerate(names, start=1):
...     print(f"{index}. {name}")
...
1. Sarah
2. Jim
3. Denise
4. Matt
5. Kate

Вы используете enumerate() для перебора списка имен, а также для доступа к индексу во время перебора. По умолчанию enumerate() начинает отсчет с нуля. Однако вы используете аргумент start, чтобы убедиться, что первое число равно единице.

Но что делает enumerate() за кулисами? Чтобы разобраться в этом, вы можете вызвать enumerate() и присвоить объекту, который он возвращает, имя переменной:

>>> numbered_names = enumerate(names, start=1)
>>> numbered_names
<enumerate object at 0x11b26ae80>

Созданный объект является enumerate объектом, который является итератором. Итераторы - это один из ключевых инструментов, который позволяет Python быть ленивым, поскольку их значения создаются по требованию. Вызов enumerate() связывает каждый элемент в names с целым числом.

Однако эти пары создаются не сразу. Пары не сохраняются в памяти. Вместо этого они генерируются по мере необходимости. Одним из способов вычисления значения с помощью итератора является вызов встроенной функции next():

>>> next(numbered_names)
(1, 'Sarah')
>>> next(numbered_names)
(2, 'Jim')

Объект numbered_names содержит не все пары внутри себя. Когда ему нужно создать следующую пару значений, он выбирает следующее имя из исходного списка names и сопоставляет его со следующим целым числом. Вы можете подтвердить это, изменив третье имя в списке names перед выборкой следующего значения в numbered_names:

>>> names[2] = "The Coffee Robot"
>>> next(numbered_names)
(3, 'The Coffee Robot')

Даже если вы создали объект enumerate numbered_names до того, как вы изменили содержимое списка, вы получите третий элемент в names после того, как вы внесли изменения. Такое поведение возможно потому, что Python лениво вычисляет объект enumerate.

Посмотрите на нумерованный список, который вы создали ранее с помощью цикла for, который показывает, что Сара должна купить кофе первой. Сара - программист на Python, поэтому она поинтересовалась, означает ли 1 рядом с ее именем, что она должна купить кофе во вторник, поскольку понедельник должен быть 0.

Вы решаете не злиться. Вместо этого вы обновляете свой код, чтобы использовать zip() для сопоставления имен с днями недели вместо цифр. Обратите внимание, что вы повторно создаете и перетасовываете список, поскольку вы внесли в него изменения:

>>> names = ["Sarah", "Matt", "Jim", "Denise", "Kate"]
>>> weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
>>> random.shuffle(names)
>>> names
['Denise', 'Jim', 'Sarah', 'Matt', 'Kate']

>>> for day, name in zip(weekdays, names):
...     print(f"{day}: {name}")
...
Monday: Denise
Tuesday: Jim
Wednesday: Sarah
Thursday: Matt
Friday: Kate

Когда вы вызываете zip(), вы создаете объект zip, который является другим итератором. Программа не создает копии данных в weekdays и names для создания пар. Вместо этого она создает пары по требованию. Это еще один пример отложенного вычисления. Вы можете исследовать объект zip непосредственно, как вы это делали с объектом enumerate:

>>> day_name_pairs = zip(weekdays, names)
>>> next(day_name_pairs)
('Monday', 'Denise')
>>> next(day_name_pairs)
('Tuesday', 'Jim')

>>> # Modify the third item in 'names'
>>> names[2] = "The Coffee Robot"
>>> next(day_name_pairs)
('Wednesday', 'The Coffee Robot')

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

Итераторы в itertools

Итераторы являются ленивыми структурами данных, поскольку их значения вычисляются, когда они необходимы, а не сразу после определения итератора. В Python есть еще много итераторов, помимо enumerate и zip. Каждый итерируемый объект либо сам является итератором, либо может быть преобразован в итератор с помощью iter().

Однако в этом разделе вы познакомитесь с модулем itertools Python, который содержит несколько таких структур данных. Сейчас вы узнаете о двух из этих инструментов, а затем, после завершения этого руководства, сможете попробовать некоторые другие.

В предыдущем разделе вы работали со списком участников команды. Теперь вы объединяете усилия с другой командой для участия в викторине и хотите распечатать список имен всей команды, участвующей в викторине:

>>> import itertools

>>> first_team = ["Sarah", "Matt", "Jim", "Denise", "Kate"]
>>> second_team = ["Mark", "Zara", "Mo", "Jennifer", "Owen"]

>>> for name in itertools.chain(first_team, second_team):
...     print(name)
...
Sarah
Matt
Jim
Denise
Kate
Mark
Zara
Mo
Jennifer
Owen


Повторяемая строка, которую вы используете в цикле for, - это объект, созданный с помощью itertools.chain(),, который объединяет два списка в единую повторяемую строку. Однако itertools.chain() создает не новый список, а итератор, который вычисляется лениво. Таким образом, программа не создает копии строк с именами, но извлекает строки, когда они нужны, из списков first_name и second_name.

Вот еще один способ наблюдать взаимосвязь между итератором и исходными структурами данных:

>>> first_team = ["Sarah", "Matt", "Jim", "Denise", "Kate"]
>>> second_team = ["Mark", "Zara", "Mo", "Jennifer", "Owen"]

>>> import sys
>>> sys.getrefcount(first_team)
2

>>> quiz_team = itertools.chain(first_team, second_team)
>>> sys.getrefcount(first_team)
3

Функция sys.getrefcount() подсчитывает количество ссылок на объект в программе. Обратите внимание, что sys.getrefcount() всегда показывает еще одну ссылку на объект, которая появляется в результате вызова самой sys.getrefcount(). Поэтому, когда в остальной части программы есть только одна ссылка на объект, sys.getrefcount() показывает две ссылки.

Когда вы создаете объект chain, вы создаете еще одну ссылку на два списка, поскольку для quiz_team требуется ссылка на то, где хранятся исходные данные. Следовательно, sys.getrefcount() показывает дополнительную ссылку на first_team. Но эта ссылка исчезает, когда вы завершаете итератор:

>>> for name in quiz_team:
...     print(name)
...
Sarah
Matt
Jim
Denise
Kate
Mark
Zara
Mo
Jennifer
Owen

>>> sys.getrefcount(first_team)
2

Отложенные вычисления структур данных, таких как itertools.chain, основаны на этой ссылке между итератором, таким как itertools.chain, и структурой, содержащей данные, такой как first_team.

Еще один инструмент в itertools, который подчеркивает разницу между активной и ленивой оценкой, - это itertools.islice(), , который является версией slice для ленивой оценки в Python. Создайте список чисел и стандартный фрагмент этого списка:

>>> numbers = [2, 4, 6, 8, 10]
>>> standard_slice = numbers[1:4]
>>> standard_slice
[4, 6, 8]

Теперь вы можете создать итераторную версию среза, используя itertools.islice():

>>> iterator_slice = itertools.islice(numbers, 1, 4)
>>> iterator_slice
<itertools.islice object at 0x117c93650>

Аргументы в itertools.islice() включают в себя итерацию, которую вы хотите срезать, и целые числа для определения начального и конечного индексов среза, как в стандартном срезе. Вы также можете включить дополнительный аргумент, представляющий размер шага. В окончательном выводе не отображаются значения в срезе, поскольку они еще не были сгенерированы. Они будут созданы по мере необходимости.

Наконец, измените одно из значений в списке и выполните цикл по стандартному срезу и срезу итератора, чтобы сравнить выходные данные:

>>> numbers[2] = 999
>>> numbers
[2, 4, 999, 8, 10]

>>> for number in standard_slice:
...     print(number)
...
4
6
8

>>> for number in iterator_slice:
...     print(number)
...
4
999
8

Вы изменяете третий элемент в списке numbers. Это изменение не влияет на стандартный фрагмент, который по-прежнему содержит исходные номера. Когда вы создаете стандартный фрагмент, Python быстро оценивает этот фрагмент и создает новый список, содержащий подмножество данных из исходной последовательности.

Однако срез итератора вычисляется лениво. Следовательно, когда вы изменяете третье значение в списке перед циклическим прохождением фрагмента итератора, это также влияет на значение в iterator_slice.

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

Генераторные выражения и генераторные функции

Выражения, которые создают встроенные структуры данных, такие как списки, кортежи или словари, быстро обрабатываются. Они генерируют и сохраняют все элементы в этих структурах данных немедленно. Примером такого рода выражений является понимание списка:

>>> import random
>>> coin_toss = [
...     "Heads" if random.random() > 0.5 else "Tails"
...     for _ in range(10)
... ]

>>> coin_toss
['Heads', 'Heads', 'Tails', 'Tails', 'Heads', 'Tails', 'Tails', 'Heads', 'Heads', 'Heads']

Выражение в правой части оператора присваивания (=) создает представление о списке. Это выражение быстро вычисляется, и десять значений "орел" или "решка" создаются и сохраняются в новом списке.

Понимание списка включает в себя условное выражение, которое возвращает либо строку "Heads", либо "Tails" в зависимости if от значения условия между <<<ключевые слова 5>>> и else. Функция random.random() создает случайное значение float в диапазоне от 0 до 1. Таким образом, существует 50-процентная вероятность того, что созданное значение будет "Heads" или "Tails".

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

>>> coin_toss = (
...     "Heads" if random.random() > 0.5 else "Tails"
...     for _ in range(10)
... )

>>> coin_toss
<generator object <genexpr> at 0x117a43440>

Выражение, заключенное в круглые скобки, является генераторным выражением. Несмотря на то, что оно похоже на понимание списка, это выражение не вычисляется автоматически. Оно создает объект-генератор. Объект-генератор - это тип итератора, который генерирует значения, когда они необходимы.

Объект generator coin_toss не хранит никаких строковых значений. Вместо этого он будет генерировать каждое значение, когда это потребуется. Вы можете сгенерировать и извлечь следующее значение с помощью встроенного next():

>>> next(coin_toss)
Tails
>>> next(coin_toss)
Heads

Выражение, которое генерирует "Heads" или "Tails", вычисляется только при вызове next(). Этот генератор сгенерирует десять значений, поскольку вы используете range(10) в предложении генератора for. Поскольку вы дважды вызывали next(), осталось сгенерировать восемь значений:

>>> for toss_result in coin_toss:
...     print(toss_result)
...
Heads
Heads
Heads
Tails
Tails
Heads
Tails
Heads

Цикл for выполняется восемь раз, по одному разу для каждого из оставшихся элементов в генераторе. Генераторное выражение - это альтернатива отложенному вычислению для создания списка или кортежа. Он предназначен для однократного использования, в отличие от его активных аналогов, таких как списки и кортежи.

Вы также можете создать объект-генератор, используя функцию-генератор. Функция-генератор - это определение функции, содержащее инструкцию yield вместо инструкции return. Вы можете определить функцию-генератор для создания объекта-генератора, аналогичного тому, который вы использовали в примере с подбрасыванием монеты выше:

>>> def generate_coin_toss(number):
...     for _ in range(number):
...         yield "Heads" if random.random() > 0.5 else "Tails"
...

>>> coin_toss = generate_coin_toss(10)

>>> next(coin_toss)
'Heads'
>>> next(coin_toss)
'Tails'

>>> for toss_result in coin_toss:
...     print(toss_result)
...
Tails
Heads
Tails
Heads
Tails
Tails
Heads
Tails

Вы создаете новый объект generator при каждом вызове функции generator. В отличие от стандартных функций с return, которые вычисляются полностью, генератор вычисляется лениво. Следовательно, когда требуется первое значение, код в функции генератора выполняет код вплоть до первого оператора yield. Он выдает это значение и приостанавливается, ожидая следующего раза, когда потребуется значение.

Этот процесс продолжается до тех пор, пока не закончатся инструкции yield и функция генератора не завершится, вызвав исключение StopIteration. Протокол итерации в цикле for фиксирует эту ошибку StopIteration, которая используется для сигнализации об окончании цикла for.

Ленивая итерация в Python также позволяет создавать несколько версий структуры данных, которые независимы друг от друга:

>>> first_coin_tosses = generate_coin_toss(10)
>>> second_coin_tosses = generate_coin_toss(10)

>>> next(first_coin_tosses)
'Tails'
>>> next(first_coin_tosses)
'Tails'
>>> next(first_coin_tosses)
'Heads'

>>> second_as_list = list(second_coin_tosses)
>>> second_as_list
['Heads', 'Heads', 'Heads', 'Heads', 'Heads', 'Tails', 'Tails', 'Tails', 'Tails', 'Heads']
>>> next(second_coin_tosses)
Traceback (most recent call last):
  ...
  File "<input>", line 1, in <module>
StopIteration

>>> next(first_coin_tosses)
'Tails'

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

Затем вы преобразуете второй генератор в список. При этом вычисляются все его значения, чтобы сохранить их в second_as_list. Существует десять значений, поскольку значения, которые вы получили от первого генератора, не влияют на второй.

Вы подтверждаете, что во втором генераторе больше не осталось значений, когда вызываете next() и получаете сообщение об ошибке StopIteration. Однако у первого генератора, first_coin_tosses, все еще есть значения для оценки, поскольку он не зависит от второго генератора.

Генераторы и итераторы в целом являются основными инструментами, используемыми для отложенного вычисления в Python. Это связано с тем, что они выдают значения только тогда, когда они необходимы, и не сохраняют все их значения в памяти.

Оценка короткого замыкания

Примеры отложенного вычисления, которые вы видели до сих пор, были посвящены выражениям, создающим структуры данных. Однако это не единственные типы выражений, которые могут быть обработаны в ленивом режиме. Рассмотрим операторы and и or. Распространенным заблуждением является то, что эти операторы возвращают True или False. Как правило, это не так.

Вы можете начать изучать and с нескольких примеров:

>>> True and True
True
>>> True and False
False

>>> 1 and 0
0
>>> 0 and 1
0

>>> 1 and 2
2
>>> 42 and "hello"
'hello'

Первые два примера содержат логические операнды и возвращают логическое значение. Результат равен True только в том случае, если оба операнда равны True. Однако третий пример не возвращает логическое значение. Вместо этого он возвращает 0, который является вторым операндом в 1 and 0. И 0 and 1 также возвращает 0, но на этот раз это первый операнд. Целое число 0 является ложным, что означает, что bool(0) возвращает False.

Аналогично, целое число 1 является истинным, что означает, что bool(1) возвращает True. Все ненулевые целые числа являются истинными. Когда Python требуется логическое значение, например, в инструкции if или с помощью таких операторов, как and и or, он преобразует объект в логическое значение, чтобы определить, следует ли рассматривать его как true или false.

Когда вы используете оператор and, программа вычисляет первый операнд и проверяет, является ли он истинным или ложным. Если первый операнд является ложным, то нет необходимости вычислять второй операнд, поскольку оба они должны быть достоверными, чтобы общий результат был достоверным. Это то, что происходит в выражении 0 and 1, где оператор and возвращает первое значение, которое является ложным. Следовательно, все выражение является ложным.

Python не вычисляет второй операнд, если первый является ложным. Это называется вычисление по короткому замыканию и является примером отложенного вычисления. Python вычисляет второй операнд только в том случае, если он ему нужен.

Если первый операнд верен, Python вычисляет и возвращает второй операнд, независимо от его значения. Если первый операнд является истинным, то истинность второго операнда определяет общую истинность выражения and.

Последние два примера содержат операнды, которые являются достоверными. В обоих случаях возвращается второй операнд, чтобы сделать все выражение достоверным. Вы можете подтвердить, что Python не вычисляет второй операнд, если первый является ложным, на следующих примерах:

>>> 0 and print("Do you see this text?")
0
>>> 1 and print("Do you see this text?")
Do you see this text?

В первом примере первым операндом является 0, и функция print() никогда не вызывается. Во втором примере первый операнд является истинным. Таким образом, Python вычисляет второй операнд, вызывая функцию print(). Обратите внимание, что результатом выражения and является значение, возвращаемое print(), которое является None.

Еще одной яркой демонстрацией короткого замыкания является использование недопустимого выражения в качестве второго операнда в выражении and:

>>> 0 and int("python")
0

>>> 1 and int("python")
Traceback (most recent call last):
  ...
  File "<input>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'python'

Вызов int("python") вызывает ValueError, поскольку строка "python" не может быть преобразована в целое число. Однако в этом первом примере выражение and возвращает 0 без возникновения ошибки. Второй операнд так и не был вычислен!

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

>>> 1 or 2
1
>>> 1 or 0
1
>>> 1 or int("python")
1

Во всех этих примерах возвращается первый операнд, поскольку он является истинным. Второй операнд игнорируется и никогда не вычисляется. Вы подтверждаете это с помощью последнего примера, в котором не возникает ValueError. Это вычисление с помощью короткого замыкания в выражении or. Python ленив и не вычисляет выражения, которые не влияют на конечный результат.

Однако, если первый операнд является ложным, результат выражения or определяется вторым операндом:

>>> 0 or 1
1

>>> 0 or int("python")
Traceback (most recent call last):
  ...
  File "<input>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'python'

Встроенные функции any() и all() также обрабатываются лениво с использованием метода оценки короткого замыкания . Функция any() возвращает True, если какой-либо из элементов в iterable соответствует действительности:

>>> any([0, False, ""])
False
>>> any([0, False, "hello"])
True

Список, который вы используете при первом вызове any(), содержит целое число 0, логическое значение False и пустую строку. Все три объекта являются ложными, и any() возвращает False. Во втором примере последним элементом является непустая строка, которая соответствует действительности. Функция возвращает True.

Функция прекращает вычислять элементы итерируемой переменной, когда находит первое достоверное значение. Вы можете подтвердить это, используя трюк, аналогичный тому, который вы использовали с операторами and и or, с помощью функции генератора, о которой вы узнали в предыдущем разделе:

>>> def lazy_values():
...     yield 0
...     yield "hello"
...     yield int("python")
...     yield 1
...

>>> any(lazy_values())
True

Вы определяете функцию генератора lazy_values() с помощью четырех инструкций yield. Третья инструкция недопустима, поскольку "python" не может быть преобразована в целое число. Вы создаете генератор, когда вызываете эту функцию в вызове any().

Программа не выдает никаких ошибок, а any() возвращает True. Вычисление генератора остановилось, когда any() встретило строку "hello", которая является первым достоверным значением в генераторе. Функция any() выполняет отложенное вычисление.

Однако, если недопустимое выражение не содержит перед собой никаких достоверных значений, оно вычисляется и выдает ошибку:

>>> def lazy_values():
...     yield 0
...     yield ""
...     yield int("python")
...     yield 1
...

>>> any(lazy_values())
Traceback (most recent call last):
  ...
  File "<input>", line 1, in <module>
  File "<input>", line 4, in lazy_values
ValueError: invalid literal for int() with base 10: 'python'

Первые два значения являются ложными. Следовательно, any() вычисляет третье значение, которое приводит к ValueError.

Функция all() ведет себя аналогично. Однако all() требует, чтобы все элементы итерируемой переменной были достоверными. Следовательно, all() происходит короткое замыкание при обнаружении первого ложного значения. Вы обновляете функцию генератора lazy_values(), чтобы проверить это поведение:

>>> def lazy_values():
...     yield 1
...     yield ""
...     yield int("python")
...     yield 1
...

>>> all(lazy_values())
False

Этот код не выдает ошибку, поскольку all() возвращает False при вычислении пустой строки, которая является вторым элементом в генераторе.

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

Функциональные инструменты программирования

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

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

Тремя основными инструментами функционального программирования являются встроенные в Python map() и filter() функций и reduce(),, которые являются частью модуля functools. Технически, первые две являются не функциями, а конструкторами классов map и filter. Однако вы используете их так же, как и функции, особенно в парадигме функционального программирования.

Вы можете изучить map() и filter() на следующем примере. Создайте список строк, содержащих имена. Сначала вы хотите преобразовать все имена в верхний регистр:

>>> original_names = ["Sarah", "Matt", "Jim", "Denise", "Kate"]
>>> names = map(str.upper, original_names)
>>> names
<map object at 0x117ad31f0>

Функция map() применяет функцию str.upper() к каждому элементу в итерационной таблице. Каждое имя в списке передается в str.upper(), и используется возвращаемое значение.

Однако, map() не создает новый список. Вместо этого он создает объект map, который является итератором. Неудивительно, что итераторы часто встречаются в руководстве по отложенному вычислению, поскольку они являются одним из основных инструментов для отложенного вычисления значений!

Вы можете оценить каждое значение по очереди, используя next():

>>> next(names)
'SARAH'
>>> next(names)
'MATT'

Вы также можете преобразовать объект map в список. При этом значения будут вычислены таким образом, чтобы их можно было сохранить в списке:

>>> list(names)
['JIM', 'DENISE', 'KATE']

В этом списке всего три имени. Вы уже вычислили и использовали первые два имени, когда дважды вызывали next(). Поскольку значения вычисляются, когда они необходимы, а не сохраняются в структуре данных, вы можете использовать их только один раз.

Теперь вы хотите сохранить только те имена, которые содержат хотя бы одну букву a. Для этой задачи вы можете использовать filter(). Во-первых, вам нужно будет воссоздать объект map, представляющий заглавные буквы, поскольку вы уже использовали этот генератор в сеансе REPL:

>>> names = map(str.upper, original_names)
>>> names = filter(lambda x: "A" in x, names)
>>> names
<filter object at 0x117ad0610>

Каждый элемент во втором аргументе filter(), который является map объектом names, передается в функцию lambda , которую вы указываете в качестве первого аргумента. Сохраняются только те значения, для которых функция lambda возвращает True. Остальные отбрасываются.

Вы повторно используете переменную с именем names на каждом этапе. При желании вы можете использовать разные идентификаторы переменных, но если вам не нужно сохранять промежуточные результаты, лучше всего использовать одну и ту же переменную. Объект, который возвращает filter(), является другим итератором, объектом filter. Следовательно, его значения еще не были вычислены.

Вы можете преобразовать объект filter в список, как вы делали в предыдущем примере. Но в этом случае попробуйте выполнить цикл, используя вместо этого цикл for:

>>> for name in names:
...     print(name)
...
SARAH
MATT
KATE

При первом вызове функции map() имена преобразуются в верхний регистр. При втором вызове, на этот раз filter(), сохраняются только имена, содержащие букву a. Вы используете заглавные буквы A в коде, поскольку вы уже преобразовали все имена в верхний регистр.

Наконец, вы сохраняете только имена, состоящие из четырех букв. В приведенном ниже коде показаны все операции map() и filter(), поскольку вам нужно каждый раз заново создавать эти итераторы:

>>> names = map(str.upper, original_names)
>>> names = filter(lambda x: "A" in x, names)
>>> names = filter(lambda x: len(x) == 4, names)
>>> list(names)
['MATT', 'KATE']

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

>>> original_names = ["Sarah", "Matt", "Jim", "Denise", "Kate", "Andy"]
>>> names = filter(lambda x: ("a" in x) or ("A" in x), original_names)
>>> names = filter(lambda x: len(x) == 4, names)
>>> names = map(str.upper, names)
>>> list(names)
['MATT', 'KATE', 'ANDY']

Первый вызов filter() теперь проверяет, есть ли в имени заглавная или строчная буква a. Поскольку более вероятно, что буква a не является первой буквой в имени, вы устанавливаете для первого операнда значение ("a" in x) в выражении or, чтобы воспользоваться преимуществами короткого-подключение с помощью оператора or.

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

Операции чтения файлов

Последний пример выражений, которые вычисляются лениво, будет посвящен чтению данных из файла значений, разделенных запятыми, обычно называемого CSV-файлом. CSV-файлы представляют собой базовый формат электронных таблиц. Это текстовые файлы с расширением .csv, в которых значения, разделяемые запятыми, относятся к разным ячейкам электронной таблицы. Каждая строка заканчивается символом новой строки "\n", чтобы показать, где заканчивается каждая строка.

Вы можете использовать для этого раздела любой CSV-файл, который пожелаете, или скопировать приведенные ниже данные и сохранить их в виде нового текстового файла с расширением .csv. Назовите CSV-файл superhero_pets.csv и поместите его в папку вашего проекта:

superhero_pets.csv

Pet Name,Species,Superpower,Favorite Snack,Hero Owner
Whiskertron,Cat,Teleportation,Tuna,Catwoman
Flashpaw,Dog,Super Speed,Peanut Butter,The Flash
Mystique,Squirrel,Illusion,Nuts,Doctor Strange
Quackstorm,Duck,Weather Control,Bread crumbs,Storm
Bark Knight,Dog,Darkness Manipulation,Bacon,Batman

Вы изучите два способа считывания данных из этого CSV-файла. В первой версии вы откроете файл и будете использовать метод .readlines() для файловых объектов:

>>> import pprint

>>> with open("superhero_pets.csv", encoding="utf-8") as file:
...     data = file.readlines()
...
>>> pprint.pprint(data)
['Pet Name,Species,Superpower,Favorite Snack,Hero Owner\n',
 'Whiskertron,Cat,Teleportation,Tuna,Catwoman\n',
 'Flashpaw,Dog,Super Speed,Peanut Butter,The Flash\n',
 'Mystique,Squirrel,Illusion,Nuts,Doctor Strange\n',
 'Quackstorm,Duck,Weather Control,Bread crumbs,Storm\n',
 'Bark Knight,Dog,Darkness Manipulation,Bacon,Batman\n']

>>> print(type(data))
<class 'list'>

Вы импортируете pprint, чтобы обеспечить удобную печать больших структур данных. Как только вы откроете CSV-файл с помощью контекстного менеджераwith, указав кодировку файла, вы вызовете метод .readlines() для открытого файла. Этот метод возвращает список, содержащий все данные электронной таблицы. Каждый элемент в списке представляет собой строку, содержащую все элементы в строке.

Эта проверка очень важна, поскольку .readlines() извлекает все содержимое электронной таблицы и сохраняет его в виде списка. Эта электронная таблица содержит не так много данных. Однако этот способ может привести к существенной нагрузке на ресурсы памяти при чтении больших объемов данных.

Вместо этого вы можете использовать модуль Python csv, который with является частью стандартной библиотеки. Чтобы упростить этот код в REPL, вы можете открыть файл без использования контекстного менеджера. Однако при этом не забудьте закрыть файл. Как правило, для открытия файлов, когда это возможно, следует использовать with:

>>> import csv
>>> file = open("superhero_pets.csv", encoding="utf-8", newline="")
>>> data = csv.reader(file)
>>> data
<_csv.reader object at 0x117a830d0>

Вы добавляете именованный аргумент newline="" при открытии файла для использования с модулем csv, чтобы убедиться, что все новые строки в полях обрабатываются правильно. Объект, возвращаемый csv.reader(), является не списком, а итератором. В этой статье вы уже достаточно часто сталкивались с итераторами, чтобы знать, чего ожидать.

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

>>> next(data)
['Pet Name', 'Species', 'Superpower', 'Favorite Snack', 'Hero Owner']
>>> next(data)
['Whiskertron', 'Cat', 'Teleportation', 'Tuna', 'Catwoman']
>>> next(data)
['Flashpaw', 'Dog', 'Super Speed', 'Peanut Butter', 'The Flash']

Первый вызов next() запускает вычисление первого элемента итератора data. Это первая строка электронной таблицы, которая является строкой заголовка. Вы вызываете next() еще два раза, чтобы получить первые две строки данных.

Вы можете использовать цикл for для выполнения итерации по остальной части итератора и вычисления оставшихся элементов:

>>> for row in data:
...     print(row)
...
['Mystique', 'Squirrel', 'Illusion', 'Nuts', 'Doctor Strange']
['Quackstorm', 'Duck', 'Weather Control', 'Bread crumbs', 'Storm']
['Bark Knight', 'Dog', 'Darkness Manipulation', 'Bacon', 'Batman']

>>> file.close()

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

Функция reader() в модуле csv позволяет вам выполнять ленивую оценку строк электронной таблицы, извлекая каждую строку только тогда, когда это необходимо. Однако вызов .readlines() для открытого файла быстро вычисляет строки, извлекая их все сразу.

Как Структура Данных может содержать бесконечное Количество Элементов?

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

В модуле itertools есть несколько инструментов, которые можно использовать для создания бесконечных итераций. Одним из них является itertools.count(), который позволяет получать последовательные числа бесконечно. Вы можете задать начальное значение и размер шага при создании итератора count:

>>> import itertools
>>> quarters = itertools.count(start=0, step=0.25)
>>> for _ in range(8):
...     print(next(quarters))
...
0
0.25
0.5
0.75
1.0
1.25
1.5
1.75

Итератор quarters выдаст значения, на 0,25 больше, чем предыдущий, и будет выдавать значения вечно. Однако ни одно из этих значений не генерируется при определении quarters. Каждое значение генерируется, когда это необходимо, например, при вызове next() или как часть итерационного процесса, например, в цикле for.

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

>>> names = ["Sarah", "Matt", "Jim", "Denise", "Kate"]

>>> rota = itertools.cycle(names)
>>> rota
<itertools.cycle object at 0x1156be340>

Объект, возвращаемый itertools.cycle(), является итератором. Следовательно, он не создает все свои элементы при первом создании. Вместо этого он генерирует значения, когда они необходимы:

>>> next(rota)
'Sarah'
>>> next(rota)
'Matt'
>>> next(rota)
'Jim'
>>> next(rota)
'Denise'
>>> next(rota)
'Kate'
>>> next(rota)
'Sarah'
>>> next(rota)
'Matt'

Итератор cycle rota начинает выдавать каждое имя из исходного списка names. Когда все имена будут получены один раз, итератор снова начнет выдавать имена с начала списка. У этого итератора никогда не закончатся значения для выдачи, поскольку он будет перезапускаться с начала списка каждый раз, когда достигнет последнего имени.

Это объект с бесконечным числом элементов. Однако в памяти хранится только пять строк, поскольку в исходном списке всего пять имен.

Итератор rota является повторяемым, как и все итераторы. Следовательно, вы можете использовать его как часть инструкции цикла for. Однако теперь это создает бесконечный цикл, поскольку цикл for никогда не получает исключение StopIteration, которое запускает завершение цикла.

Вы также можете создать бесконечные структуры данных, используя функции-генераторы. Вы можете воссоздать итератор rota, предварительно определив функцию-генератор generate_rota():

>>> def generate_rota(iterable):
...     index = 0
...     length = len(iterable)
...     while True:
...         yield iterable[index]
...         if index == length - 1:
...             index = 0
...         else:
...             index += 1
...

>>> rota = generate_rota(names)
>>> for _ in range(12):
...     print(next(rota))
...
Sarah
Matt
Jim
Denise
Kate
Sarah
Matt
Jim
Denise
Kate
Sarah
Matt

В функции генератора generate_rota() вы вручную управляете индексом для извлечения элементов из итерационной таблицы, увеличивая значение после получения каждого элемента и сбрасывая его до нуля, когда достигаете конца итерационной таблицы. Функция генератора включает в себя оператор while True, который превращает эту структуру данных в бесконечную.

В этом примере функция generator воспроизводит поведение, которого вы можете достичь с помощью itertools.cycle(). Однако вы можете создать любой генератор с пользовательскими требованиями, используя этот метод.

В чем преимущества отложенного вычисления в Python?

Вы можете вернуться к предыдущему примеру, чтобы изучить одно из главных преимуществ отложенной оценки. Ранее в этом руководстве вы создали список и объект-генератор с несколькими результатами подбрасывания монеты. В этой версии вы сделаете по миллиону подбрасываний монет в каждом из них:

>>> import random
>>> coin_toss_list = [
...     "Heads" if random.random() > 0.5 else "Tails"
...     for _ in range(1_000_000)
... ]
>>> coin_toss_gen = (
...     "Heads" if random.random() > 0.5 else "Tails"
...     for _ in range(1_000_000)
... )

>>> import sys
>>> sys.getsizeof(coin_toss_list)
8448728
>>> sys.getsizeof(coin_toss_gen)
200

Вы создаете список и объект-генератор. Оба объекта представляют собой миллион строк, содержащих либо "Heads", либо "Tails". Однако список занимает более восьми миллионов байт памяти, в то время как генератор использует только 200 байт. Количество байт может немного отличаться в зависимости от используемой версии Python.

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

Этот пример также демонстрирует еще одно преимущество использования отложенного вычисления при создании структуры данных. Вы можете использовать условное выражение, которое возвращает "Heads" или "Tails" случайным образом непосредственно в коде, когда вам это нужно. Однако создание генератора могло бы быть лучшим вариантом.

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

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

Вы можете продемонстрировать это преимущество в производительности, используя timeit модуль в стандартной библиотеке Python. Вы можете проверить это с помощью вычисления короткого замыкания при использовании оператора and. Следующие два выражения похожи и возвращают одинаковую достоверность:

>>> import random
>>> random.randint(0, 1) and random.randint(0, 10)
1
>>> random.randint(0, 10) and random.randint(0, 1)
8

Эти выражения возвращают истинное значение, если оба вызова random.randint() возвращают ненулевые значения. Они вернут 0, если хотя бы одна функция вернет 0. Однако более вероятно, что random.randint(0, 1) вернет 0 по сравнению с random.randint(0, 10).

Поэтому, если вам нужно многократно вычислять это выражение в вашем коде, первая версия более эффективна из-за короткого замыкания. Вы можете рассчитать, сколько времени потребуется для многократного вычисления этих выражений:

>>> import timeit
>>> timeit.repeat(
...     "random.randint(0, 1) and random.randint(0, 10)",
...     number=1_000_000,
...     globals=globals(),
... )
[0.39701350000177626, 0.37251866700171377, 0.3730850419997296,
 0.3731833749989164, 0.3740811660027248]

>>> timeit.repeat(
...     "random.randint(0, 10) and random.randint(0, 1)",
...     number=1_000_000,
...     globals=globals(),
... )
[0.504747375001898, 0.4694556670001475, 0.4706860409969522,
 0.4841222920003929, 0.47349566599950776]

Выходные данные показывают время, необходимое для одного миллиона вычислений каждого выражения. Для каждого выражения существует пять отдельных таймингов. Первая версия - это та, в которой в качестве первого операнда используется random.randint(0, 1), и она выполняется быстрее, чем вторая, в которой операнды меняются местами.

Вычисление выражения and завершается коротким замыканием, когда первый вызов random.randint() возвращает 0. Поскольку random.randint(0, 1) имеет 50-процентную вероятность возврата 0, примерно половина вычислений выражения and вызовет только первое random.randint().

Когда random.randint(0, 10) является первым операндом, вычисление выражения завершается коротким замыканием только один раз из каждых одиннадцати раз, когда оно выполняется, поскольку существует одиннадцать возможных значений, возвращаемых random.randint(0, 10).

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

Каковы недостатки отложенной оценки в Python?

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

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

>>> players = [
...     {"Name": "Sarah", "Games": 4, "Points": 23},
...     {"Name": "Matt", "Games": 7, "Points": 42},
...     {"Name": "Jim", "Games": 1, "Points": 7},
...     {"Name": "Denise", "Games": 0, "Points": 0},
...     {"Name": "Kate", "Games": 5, "Points": 33},
... ]

Вы создаете список игроков, и каждый элемент в этом списке является словарем. Каждый словарь содержит три пары ключ-значение для хранения имени игрока, количества сыгранных им игр и общего количества набранных очков.

Вас интересует среднее количество очков за игру для каждого игрока, поэтому вы создаете генератор с таким значением для каждого игрока:

>>> average_points_per_game = (
...     item["Points"] / item["Games"]
...     for item in players
... )
>>> average_points_per_game
<generator object <genexpr> at 0x11566a880>

Генераторное выражение вычисляется медленно. Поэтому требуемые значения вычисляются не сразу. Теперь вы можете начать вызывать next(), чтобы получить среднее количество очков за игру для каждого игрока:

>>> next(average_points_per_game)
5.75
>>> next(average_points_per_game)
6.0
>>> next(average_points_per_game)
7.0
>>> next(average_points_per_game)
Traceback (most recent call last):
  ...
  File "<input>", line 1, in <module>
  File "<input>", line 1, in <genexpr>
ZeroDivisionError: division by zero

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

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

>>> average_points_per_game = [
...     item["Points"] / item["Games"]
...     for item in players
... ]
Traceback (most recent call last):
  ...
  File "<input>", line 1, in <module>
  File "<input>", line 1, in <listcomp>
ZeroDivisionError: division by zero

В этом случае ошибка возникает немедленно. Из-за задержки ошибок их труднее идентифицировать и исправить, что приводит к увеличению сложности отладки кода. Популярная сторонняя библиотека Python, TensorFlow, переключилась с отложенной оценки на оперативную оценку в качестве опции по умолчанию для облегчения отладки. После завершения процесса отладки пользователи могут включить отложенную оценку с помощью декоратора.

Заключение

В этом руководстве вы узнали, что такое отложенное вычисление в Python и чем оно отличается от активного вычисления. Некоторые выражения не вычисляются, когда программа впервые сталкивается с ними. Вместо этого они вычисляются, когда в программе требуются соответствующие значения.

Этот тип вычисления называется отложенным вычислением и может привести к созданию более удобочитаемого кода, который также более экономичен с точки зрения памяти и производительности. В отличие от этого, оперативное вычисление - это когда выражение вычисляется полностью немедленно.

Идеальный режим вычисления зависит от нескольких факторов. Для небольших наборов данных использование отложенного вычисления не дает заметных преимуществ с точки зрения эффективности использования памяти и производительности. Однако преимущества отложенного вычисления становятся более важными для больших объемов данных. Отложенная оценка также может затруднить выявление и исправление ошибок.

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

В Python отложенное вычисление часто происходит за кулисами. Однако вам также нужно решить, когда использовать выражения, которые вычисляются быстро или лениво, например, когда вам нужно создать список или объект-генератор. Теперь вы обладаете необходимыми знаниями, чтобы понять, как работать с обоими типами оценки.

Back to Top