Как я могу собрать результаты повторного вычисления в список, словарь и т.д. (или сделать копию списка с измененным каждым элементом)?
Существует множество существующих вопросов и ответов по Stack Overflow на эту общую тему, но все они либо низкого качества (как правило, подразумеваются из-за проблемы отладки новичка), либо каким-либо другим образом не попадают в цель (как правило, из-за недостаточно общего характера). Существует по крайней мере два чрезвычайно распространенных способа ошибиться в наивном коде, и новичкам больше пригодится канонический ответ о циклировании, чем закрытие их вопросов как опечаток или канонический ответ о том, что влечет за собой печать. Итак, это моя попытка поместить всю связанную информацию в одно и то же место.
Предположим, у меня есть какой-то простой код, который выполняет вычисление со значением x
и присваивает его y
:
y = x + 1
# Or it could be in a function:
def calc_y(an_x):
return an_x + 1
Теперь я хочу повторить вычисление для многих возможных значений x
. Я знаю, что могу использовать for
цикл, если у меня уже есть список (или другая последовательность) значений для использования:
xs = [1, 3, 5]
for x in xs:
y = x + 1
Или я могу использовать while
цикл, если есть какая-то другая логика для вычисления последовательности x
значений:
def next_collatz(value):
if value % 2 == 0:
return value // 2
else:
return 3 * value + 1
def collatz_from_19():
x = 19
while x != 1:
x = next_collatz(x)
Вопрос в следующем: как я могу собрать эти значения и использовать их после цикла? Я пробовал print
вводить значение внутри цикла, но это не дает мне ничего полезного:
xs = [1, 3, 5]
for x in xs:
print(x + 1)
Результаты отображаются на экране, но я не могу найти никакого способа использовать их в следующей части кода. Поэтому я думаю, что мне следует попытаться сохранить значения в контейнере, например, в списке или словаре. Но когда я пытаюсь это:
xs = [1, 3, 5]
for x in xs:
ys = []
y = x + 1
ys.append(y)
или
xs = [1, 3, 5]
for x in xs:
ys = {}
y = x + 1
ys[x] = y
После любой из этих попыток ys
содержит только последний результат.
Переведено автоматически
Ответ 1
Общие подходы
Есть три обычных способа решения проблемы: явно используя цикл (обычно это for
цикл, но while
также возможны циклы); используя понимание списка (или понимание dict, понимание set или выражение генератора в зависимости от конкретной потребности в контексте); или используя встроенный map
(результаты которого могут быть использованы для явного построения списка, set или dict).
Используя явный цикл
Создайте список или словарь перед циклом и добавляйте каждое значение по мере его вычисления:
def make_list_with_inline_code_and_for():
ys = []
for x in [1, 3, 5]:
ys.append(x + 1)
return ys
def next_collatz(value):
if value % 2 == 0:
return value // 2
else:
return 3 * value + 1
def make_dict_with_function_and_while():
x = 19
ys = {}
while x != 1:
y = next_collatz(x)
ys[x] = y # associate each key with the next number in the Collatz sequence.
x = y # continue calculating the sequence.
return ys
В обоих приведенных здесь примерах цикл был помещен в функцию, чтобы пометить код и сделать его повторно используемым. В этих примерах return
указано ys
значение , чтобы вызывающий код мог использовать результат. Но, конечно, вычисленное ys
также может быть использовано позже в той же функции, и циклы, подобные этим, также могут быть написаны вне любой функции.
Используйте for
цикл, когда есть существующий ввод, где каждый элемент должен обрабатываться независимо. Используйте while
цикл для создания выходных элементов до тех пор, пока не будет выполнено какое-либо условие. Python не напрямую поддерживает выполнение цикла определенное количество раз (рассчитанное заранее); обычная идиома - создать фиктивный файл range
соответствующей длины и использовать с ним for
цикл.
Используя выражение понимания или генератора
Понимание списка предоставляет элегантный синтаксис для создания списка из существующей последовательности значений. Это должно быть предпочтительнее, где это возможно, потому что это означает, что коду не нужно сосредотачиваться на деталях построения списка, что облегчает его чтение. Это также может быть быстрее, хотя обычно это не имеет значения.
Это может работать либо с вызовом функции, либо с другим вычислением (любым выражением в терминах "исходных" элементов), и это выглядит как:
xs = [1, 3, 5]
ys = [x + 1 for x in xs]
# or
def calc_y(an_x):
return an_x + 1
ys = [calc_y(x) for x in xs]
Обратите внимание, что это не заменит while
цикл; здесь нет допустимого синтаксиса, заменяющего for
на while
. В общем, понимание списков предназначено для получения существующих значений и выполнения отдельного вычисления для каждого - не для какой-либо логики, которая включает в себя "запоминание" чего-либо от одной итерации к следующей (хотя это можно обойти, особенно в Python 3.8 и более поздних версиях).
Аналогично, результат словаря может быть создан с использованием понимания dict - при условии, что на каждой итерации вычисляются и ключ, и значение. В зависимости от конкретных потребностей, также могут подойти set comprehensions (создать set
, который не содержит повторяющихся значений) и выражения генератора (выдать результат с ленивой оценкой; см. Ниже о map
и выражениях генератора).
Используя map
Это похоже на понимание списка, но даже более конкретно. map
это встроенная функция, которая может многократно применять функцию к нескольким различным аргументам из некоторой входной последовательности (или нескольких последовательностей).
Получение результатов, эквивалентных предыдущему коду, выглядит следующим образом:
xs = [1, 3, 5]
def calc_y(an_x):
return an_x + 1
ys = list(map(calc_y, xs))
# or
ys = list(map(lambda x: x + 1, xs))
Помимо того, что требуется последовательность ввода (она не заменяет while
цикл), вычисление должно выполняться с использованием функции или другого вызываемого элемента, такого как лямбда-выражение, показанное выше (любой из них при передаче в map
является так называемой "функцией высшего порядка").
В Python 3.x, map
это класс, и поэтому его вызов создает экземпляр этого класса - и этот экземпляр представляет собой особый вид итератора (не список), который не может быть повторен более одного раза. (Мы можем получить нечто подобное, используя выражение генератора, а не понимание списка; просто используйте ()
вместо []
.)
Следовательно, приведенный выше код явно создает список из отображенных значений. В других ситуациях может не потребоваться делать это (т. Е. Если это будет повторено только один раз). С другой стороны, если set
необходимо, map
объект может быть передан непосредственно в set
, а не list
таким же образом. Для создания словаря, map
следует настроить так, чтобы каждый выходной элемент представлял собой (key, value)
кортеж; затем его можно передать в dict
, вот так:
def dict_from_map_example(letters):
return dict(map(lambda l: (l, l.upper()), letters))
# equivalent using a dict comprehension:
# return {l:l.upper() for l in letters}
Как правило, map
ограничен и необычен по сравнению с пониманием списков, и понимание списков следует предпочесть в большинстве кода. Однако он предлагает некоторые преимущества. В частности, это может избежать необходимости указывать и использовать переменную итерации: когда мы пишем list(map(calc_y, xs))
, нам не нужно составлять x
для именования элементов xs
, и нам не нужно писать код для передачи его в calc_y
(как в эквиваленте понимания списка, [calc_y(x) for x in xs]
- обратите внимание на два x
s). Некоторые люди находят это более элегантным.
Ответ 2
Распространенные ошибки и подводные камни
Попытка добавить элементы, присвоив отсутствующему индексу
Иногда люди ошибочно пытаются реализовать код цикла с помощью чего-то вроде:
xs = [1, 3, 5]
ys = []
for i, x in enumerate(xs):
ys[i] = x + 1
Возможно назначить только те индексы в списке, которые уже присутствуют - но здесь список начинается пустым, поэтому пока ничего не присутствует. При первом выполнении цикла возникнет IndexError
. Вместо этого используйте .append
метод для добавления значения.
Есть другие, более непонятные способы, но в них нет реального смысла. В частности: "предварительное выделение" списка (с чем-то вроде ys = [None] * len(xs)
может предложить небольшое улучшение производительности в некоторых случаях, но это уродливо, более подвержено ошибкам и работает только в том случае, если количество элементов может быть известно заранее (например, это не сработает, если xs
на самом деле происходит из чтения файла с использованием того же цикла).
Неправильное использование append
append
Метод списков возвращает None
, а не список, к которому был добавлен. Иногда люди ошибочно пытаются использовать код, подобный:
xs = [1, 3, 5]
ys = []
for x in xs:
ys = ys.append(x) # broken!
При первом выполнении цикла ys.append(x)
изменит ys
список и вычислит значение None
, а затем ys =
присвоит это None
значение ys
. Во второй раз, ys
есть None
, поэтому вызов .append
вызывает AttributeError
.
list.append
для понимания
Подобный код работать не будет:
# broken!
xs = [1, 3, 5]
y = []
y = [y.append(x + 1) for x in xs]
Иногда это является результатом неясного мышления; иногда это является результатом попытки преобразовать старый код с помощью цикла для использования понимания и не внесения всех необходимых изменений.
Когда это делается намеренно, это показывает неправильное понимание понимания списка. .append
Метод возвращает None
, так что это значение, которое заканчивается (повторно) в списке, созданном пониманием. Более того, это концептуально неправильно: цель понимания - построить список из вычисленных значений, поэтому вызов .append
не имеет смысла - он пытается выполнить работу, за которую понимание уже отвечает. Хотя здесь можно пропустить назначение (и тогда y
уже были добавлены соответствующие значения), это плохой стиль - использовать понимание списка для его побочных эффектов - и особенно когда эти побочные эффекты делают то, что понимание могло бы сделать естественным образом.
Повторное создание нового списка внутри цикла
Ключевым моментом в явном коде цикла является то, что ys
устанавливается начальное значение empty или list или dictionary один раз. Это действительно должно произойти (чтобы можно было добавлять элементы или вставлять ключи), но выполнение этого внутри цикла означает, что результат будет продолжать перезаписываться.
То есть этот код нарушен:
def broken_list_with_inline_code_and_for():
for x in [1, 3, 5]:
ys = []
ys.append(x + 1)
return ys
Это должно быть очевидно после объяснения, но это очень распространенная логическая ошибка для начинающих программистов. Каждый раз в цикле ys
становится []
снова, а затем добавляется один элемент - прежде чем стать []
снова, в следующий раз в цикле.
Иногда люди делают это, потому что они думают, что ys
должно быть "ограничено" циклом - но это не очень разумное рассуждение (в конце концов, весь смысл в том, чтобы иметь возможность использовать ys
после завершения цикла!), И в любом случае Python не создает отдельных областей для циклов.
Попытка использовать несколько входных данных без zip
Код, использующий цикл или понимание, нуждается в специальной обработке для "объединения в пары" элементов из нескольких источников ввода. Эти способы не будут работать:
# broken!
odds = [1, 3, 5]
evens = [2, 4, 6]
numbers = []
for odd, even in odds, evens:
numbers.append(odd * even)
# also broken!
numbers = [odd * even for odd, even in odds, evens]
Эти попытки вызовут ValueError
. Проблема в том, что odds, evens
создается единственный кортеж списков; цикл или понимание попытаются выполнить итерацию по этому кортежу (таким образом, значение будет [1, 3, 5]
с первого раза и [2, 4, 6]
со второго раза), а затем распаковать это значение в переменные odd
и even
. Поскольку в нем [1, 3, 5]
есть три значения, а odd
и even
являются только двумя отдельными переменными, это не удается. Даже если бы это сработало (например, если odds
и evens
по совпадению были правильной длины), результаты были бы неправильными, поскольку итерация выполняется в неправильном порядке.
Решение заключается в использовании zip
, вот так:
# broken!
odds = [1, 3, 5]
evens = [2, 4, 6]
numbers = []
for odd, even in zip(odds, evens):
numbers.append(odd * even)
# or
numbers = [odd * even for odd, even in zip(odds, evens)]
Это не проблема при использовании map
вместо цикла или понимания - сопряжение выполняется map
автоматически:
numbers = list(map(lambda x, y: x * y, odds, evens))
Попытка изменить входной список
Понимание списка создает новый список на основе входных данных, и map
аналогичным образом выполняется итерация по новым результатам. Ни то, ни другое не подходит для попытки изменить входной список напрямую. Однако, есть возможность заменить исходный список новым:
xs = [1, 3, 5]
ys = xs # another name for that list
xs = [x + 1 for x in xs] # ys will be unchanged
Или заменить его содержимое с помощью назначения фрагмента:
xs = [1, 3, 5]
ys = xs
# The actual list object is modified, so ys is changed too
xs[:] = [x + 1 for x in xs]
Учитывая входной список, для замены элементов списка результатами вычисления можно использовать явный цикл - однако это не просто. Например:
numbers = [1, 2, 3]
for n in numbers:
n += 1
assert numbers == [1, 2, 3] # the list will not change!
Такого рода модификация списка возможна только в том случае, если базовые объекты действительно изменены - например, если у нас есть список списков, и мы изменяем каждый из них:
lol = [[1], [3]]
for l in lol:
# the append method modifies the existing list object.
l.append(l[0] + 1)
assert lol == [[1, 2], [3, 4]]
Другой способ - сохранить индекс и присвоить обратно исходному списку:
numbers = [1, 2, 3]
for i, n in enumerate(numbers):
numbers[i] = n + 1
assert numbers == [2, 3, 4]
Однако, почти в любых обычных обстоятельствах будет лучше создать новый список.
Не такой уж особый случай: список строк в нижнем регистре
Многие дубликаты этого вопроса специально направлены на преобразование входного списка строк all в нижний регистр (или all в верхний регистр). Это не является чем-то особенным; любой практический подход к проблеме будет включать решение проблем "ввести в нижний регистр одну строку" и "повторить вычисление и собрать результаты" (т. Е. Этот вопрос). Однако это полезный демонстрационный пример, потому что вычисление включает использование метода элементов списка.
Общие подходы выглядят следующим образом:
def lowercase_with_explicit_loop(strings):
result = []
for s in strings:
result.append(s.lower())
return result
def lowercase_with_comprehension(strings):
return [s.lower() for s in strings]
def lowercase_with_map(strings):
return list(map(str.lower, strings))
Однако здесь есть два интересных момента.
Обратите внимание, чем отличается
map
версия. Хотя, конечно, возможно создать функцию, которая принимает строку и возвращает результат вызова метода, это не обязательно. Вместо этого мы можем напрямую искатьlower
метод из класса (здесь,str
), который в 3.x приводит к совершенно обычной функции (а в 2.x приводит к "несвязанному" методу, который затем может быть вызван с экземпляром в качестве явного параметра - что означает то же самое). Когда строка передается вstr.lower
, результатом является новая строка, которая является строчной версией входной строки, то есть именно той функцией, которая необходима дляmap
работы.
Другие подходы не допускают такого упрощения; выполнение цикла или использование выражения понимания / генератора требует выбора имени (s
в этих примерах) для переменной итерации (цикла).Иногда, при написании явной версии цикла, люди ожидают, что смогут просто записать
s.lower()
и, таким образом, преобразовать строку на месте, в исходномstrings
списке. Как указано выше, списки можно изменять с помощью этого общего подхода - но только с помощью методов, которые фактически изменяют объект. Строки Python неизменяемы, поэтому это не работает.
Ответ 3
Когда входные данные представляют собой строку
Строки могут быть повторены напрямую. Однако обычно, когда входными данными является строка, в качестве выходных данных также ожидается одна строка. Вместо этого понимание списка создаст список, а выражение генератора аналогичным образом создаст генератор.
Существует множество возможных стратегий для объединения результатов в строку; но для обычного случая "перевода" или "сопоставления" каждого символа в строке с некоторым выходным текстом проще и эффективнее использовать встроенную функциональность string: translate
метод string вместе со статическим методом, maketrans
предоставляемым классом string .
Метод translate
напрямую создает строку на основе символов во входных данных. Для этого требуется словарь, где ключами являются номера кодовых точек Unicode (результат применения ord
к односимвольной строке), а значениями являются либо номера кодовых точек Unicode, строки, либо Нет. Он будет выполнять итерацию по входной строке, просматривая ее по номеру. Если входной символ не найден, он копируется в выходную строку (он будет использовать внутренний буфер и создавать объект string только в конце). Если сопоставление содержит запись для кодовой точки символа.:
- Если это строка, эта строка будет скопирована.
- Если это другая кодовая точка, будет скопирован соответствующий символ.
- Если это
None
, ничего не копируется (тот же эффект, что и у пустой строки).
Поскольку эти сопоставления сложно создать вручную, str
класс предоставляет метод maketrans
, который поможет. Для этого может потребоваться словарь, либо две или три строки.
- Когда предоставляется словарь, он должен быть похож на тот, который ожидает
translate
метод, за исключением того, что он также может использовать односимвольные строки в качестве ключей.maketrans
заменит их соответствующими кодовыми точками. - Когда заданы две строки, они должны быть одинаковой длины.
maketrans
будет использовать каждый символ первой строки в качестве ключа, а соответствующий символ во второй строке - в качестве соответствующего значения. - При задании трех строк первые две строки работают как раньше, а третья строка содержит символы, которые будут сопоставлены с
None
.
Например, вот демонстрация простой реализации шифра ROT13 в командной строке интерпретатора:
>>> import string
>>> u, l = string.ascii_uppercase, string.ascii_lowercase
>>> u_rot, l_rot = u[13:] + u[:13], l[13:] + l[:13]
>>> mapping = str.maketrans(u+l, u_rot+l_rot)
>>> 'Hello, World!'.translate(mapping)
'Uryyb, Jbeyq!'
Код создает повернутые и обычные версии заглавного и строчного алфавитов, затем использует str.maketrans
для сопоставления букв с соответствующей буквой, сдвинутой на 13 позиций в том же регистре. Затем .translate
применяет это сопоставление. Для справки, сопоставление выглядит следующим образом:
>>> mapping
{65: 78, 66: 79, 67: 80, 68: 81, 69: 82, 70: 83, 71: 84, 72: 85, 73: 86, 74: 87, 75: 88, 76: 89, 77: 90, 78: 65, 79: 66, 80: 67, 81: 68, 82: 69, 83: 70, 84: 71, 85: 72, 86: 73, 87: 74, 88: 75, 89: 76, 90: 77, 97: 110, 98: 111, 99: 112, 100: 113, 101: 114, 102: 115, 103: 116, 104: 117, 105: 118, 106: 119, 107: 120, 108: 121, 109: 122, 110: 97, 111: 98, 112: 99, 113: 100, 114: 101, 115: 102, 116: 103, 117: 104, 118: 105, 119: 106, 120: 107, 121: 108, 122: 109}
что не очень практично создавать вручную.