4

There are a great many existing Q&A on Stack Overflow on this general theme, but they are all either poor quality (typically, implied from a beginner's debugging problem) or miss the mark in some other way (generally by being insufficiently general). There are at least two extremely common ways to get the naive code wrong, and beginners would benefit more from a canonical about looping than from having their questions closed as typos or a canonical about what printing entails. So this is my attempt to put all the related information in the same place.

Suppose I have some simple code that does a calculation with a value x and assigns it to y:

y = x + 1

# Or it could be in a function:
def calc_y(an_x):
    return an_x + 1

Now I want to repeat the calculation for many possible values of x. I know that I can use a for loop if I already have a list (or other sequence) of values to use:

xs = [1, 3, 5]
for x in xs:
    y = x + 1

Or I can use a while loop if there is some other logic to calculate the sequence of x values:

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)

The question is: how can I collect these values and use them after the loop? I tried printing the value inside the loop, but it doesn't give me anything useful:

xs = [1, 3, 5]
for x in xs:
    print(x + 1)

The results show up on the screen, but I can't find any way to use them in the next part of the code. So I think I should try to store the values in a container, like a list or a dictionary. But when I try that:

xs = [1, 3, 5]
for x in xs:
    ys = []
    y = x + 1
    ys.append(y)

or

xs = [1, 3, 5]
for x in xs:
    ys = {}
    y = x + 1
    ys[x] = y

After either of these attempts, ys only contains the last result.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • In the past, I would typically close questions like this one as a duplicate of [Apply function to each element of a list](https://stackoverflow.com/questions/25082410), and I strongly considered simply adding my answer there (in order to have a comprehensive reference, and make sure the simple explicit loop approach is described). However, the question asked there is really very inadequate for these needs. It seems like OP may have specifically only been thinking of *methods* on the input list elements, and originally brought up `filter` in an irrelevant way. – Karl Knechtel Mar 07 '23 at 20:02
  • By asking the question this way, it also becomes possible to address a logical issue in beginner code that seems to be so common that treating it as a "typo" every time is no longer viable. – Karl Knechtel Mar 07 '23 at 20:03
  • Wasn't there already a canonical for "how can I collect the results from a loop in a list"? If this one focuses on the "new list created in every iteration" issue, I would try to make this more clear in the title. – mkrieger1 Mar 07 '23 at 21:58
  • @mkrieger1 that one is specifically about a) preparing a value to return from a function and b) only using the explicit loop approach. It should possibly be duped here instead (since I think it was written assuming that the `print`/`return` distinction was already conceptually established). The goal here is to consolidate, because it is a simple and coherent problem that can be conceptualized in multiple ways. – Karl Knechtel Mar 07 '23 at 22:06
  • @mkrieger1 I assume you mean [How can I use `return` to get back multiple values from a loop? Can I put them in a list?](https://stackoverflow.com/questions/44564414/). I cited it in the answer. On second thought, it definitely is only related and not a duplicate, since it addresses the separate and key issue that a function can only `return` once per call. – Karl Knechtel Mar 07 '23 at 22:12

3 Answers3

5

General approaches

There are three ordinary ways to approach the problem: by explicitly using a loop (normally a for loop, but while loops are also possible); by using a list comprehension (or dict comprehension, set comprehension, or generator expression as appropriate to the specific need in context); or by using the built-in map (results of which can be used to construct a list, set or dict explicitly).

Using an explicit loop

Create a list or dictionary before the loop, and add each value as it's calculated:

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

In both examples here, the loop was put into a function in order to label the code and make it reusable. These examples return the ys value so that the calling code can use the result. But of course, the computed ys could also be used later in the same function, and loops like these could also be written outside of any function.

Use a for loop when there is an existing input, where each element should be processed independently. Use a while loop to create output elements until some condition is met. Python does not directly support running a loop a specific number of times (calculated in advance); the usual idiom is to make a dummy range of the appropriate length and use a for loop with that.

Using a comprehension or generator expression

A list comprehension gives elegant syntax for creating a list from an existing sequence of values. It should be preferred where possible, because it means that the code does not have to focus on the details of how to build the list, making it easier to read. It can also be faster, although this will usually not matter.

It can work with either a function call or other calculation (any expression in terms of the "source" elements), and it looks like:

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]

Note that this will not replace a while loop; there is no valid syntax replacing for with while here. In general, list comprehensions are meant for taking existing values and doing a separate calculation on each - not for any kind of logic that involves "remembering" anything from one iteration to the next (although this can be worked around, especially in Python 3.8 and later).

Similarly, a dictionary result can be created using a dict comprehension - as long as both a key and value are computed in each iteration. Depending on exact needs, set comprehensions (produce a set, which does not contain duplicate values) and generator expressions (produce a lazily-evaluated result; see below about map and generator expressions) may also be appropriate.

Using map

This is similar to a list comprehension, but even more specific. map is a built-in function that can apply a function repeatedly to multiple different arguments from some input sequence (or multiple sequences).

Getting results equivalent to the previous code looks like:

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))

As well as requiring an input sequence (it doesn't replace a while loop), the calculation needs to be done using a function or other callable, such as the lambda shown above (any of these, when passed to map, is a so-called "higher-order function").

In Python 3.x, map is a class, and calling it therefore creates an instance of that class - and that instance is a special kind of iterator (not a list) that can't be iterated more than once. (We can get something similar using a generator expression rather than a list comprehension; simply use () instead of [].)

Therefore, the code above explicitly creates a list from the mapped values. In other situations, it might not be necessary to do this (i.e., if it will only be iterated over once). On the other hand, if a set is necessary, the map object can be passed directly to set rather than list in the same way. To produce a dictionary, the map should be set up so that each output element is a (key, value) tuple; then it can be passed to dict, like so:

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}

Generally, map is limited and uncommon compared to list comprehensions, and list comprehensions should be preferred in most code. However, it does offer some advantages. In particular, it can avoid the need to specify and use an iteration variable: when we write list(map(calc_y, xs)), we don't need to make up an x to name the elements of xs, and we don't have to write code to pass it to calc_y (as in the list comprehension equivalent, [calc_y(x) for x in xs] - note the two xs). Some people find this more elegant.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
0

Common errors and pitfalls

Trying to append elements by assigning to a missing index

Sometimes people mistakenly try to implement the loop code with something like:

xs = [1, 3, 5]
ys = []

for i, x in enumerate(xs):
    ys[i] = x + 1

It is only possible to assign to indices in a list that are already present - but here, the list starts out empty, so there is nothing present yet. The first time through the loop will raise an IndexError. Instead, use the .append method to append the value.

There are other, more obscure ways, but there is no real point to them. In particular: "pre-allocating" the list (with something like ys = [None] * len(xs) may offer a small performance improvement in some cases, but it's ugly, more error prone, and only works if the number of elements can be known ahead of time (e.g., it won't work if xs actually comes from reading a file using the same loop).

Using append incorrectly

The append method of lists returns None rather than the list that was appended to. Sometimes people mistakenly try code like:

xs = [1, 3, 5]
ys = []
for x in xs:
    ys = ys.append(x) # broken!

The first time through the loop, ys.append(x) will modify the ys list, and evaluate to None, and then ys = will assign that None to ys. The second time through, ys is None, so the call to .append raises an AttributeError.

list.append in a comprehension

Code like this will not work:

# broken!
xs = [1, 3, 5]
y = []
y = [y.append(x + 1) for x in xs]

Sometimes this results from unclear thinking; sometimes it results from attempting to convert old code with a loop to use a comprehension, and not making all the necessary changes.

When done deliberately, it shows a misunderstanding of the list comprehension. The .append method returns None, so that is the value that ends up (repeatedly) in the list created by the comprehension. But more than that, it's conceptually wrong: the purpose of the comprehension is to build the list from the calculated values, so calling .append makes no sense - it's trying to do work that the comprehension is already responsible for. Although it's possible to skip the assignment here (and then y has already had the appropriate values appended), it is poor style to use a list comprehension for its side effects - and especially so when those side effects do something the comprehension could do naturally.

Re-creating a new list inside the loop

The key point in the explicit loop code is that ys is set to an initial empty or list or dictionary once. It does need to happen (so that elements can be added or keys can be inserted), but doing it inside the loop means that the result will keep getting overwritten.

That is, this code is broken:

def broken_list_with_inline_code_and_for():
    for x in [1, 3, 5]:
        ys = []
        ys.append(x + 1)
    return ys

This should be obvious once it's explained, but it's a very common logical error for new programmers. Each time through the loop, ys becomes [] again, and then one element is added - before becoming [] again, the next time through the loop.

Sometimes people do this because they think that ys should be "scoped to" the loop - but this isn't good reasoning (after all, the entire point is to be able to use ys after the loop has completed!), and anyway Python does not create separate scopes for loops.

Trying to use multiple inputs without zip

Code using a loop or a comprehension needs special handling in order to "pair up" elements from multiple input sources. These ways will not work:

# 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]

These attempts will raise a ValueError. The problem is that odds, evens creates a single tuple of lists; the loop or comprehension will try to iterate over that tuple (so the value will be [1, 3, 5] the first time through and [2, 4, 6] the second time through), and then unpack that value into the odd and even variables. Since [1, 3, 5] has three values in it, and odd and even are only two separate variables, this fails. Even if it did work (for example, if odds and evens were coincidentally the right length), the results would be wrong, since the iteration is in the wrong order.

The solution is to use zip, like so:

# 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)]

This is not a problem when using map instead of the loop or comprehension - the pairing-up is done by map automatically:

numbers = list(map(lambda x, y: x * y, odds, evens))

Trying to modify an input list

List comprehensions create a new list from the input, and a map similarly iterates over the new results. Neither of these is appropriate for trying to modify the input list directly. However, it is possible to replace the original list with the new one:

xs = [1, 3, 5]
ys = xs # another name for that list
xs = [x + 1 for x in xs] # ys will be unchanged

Or replace its contents using slice assignment:

xs = [1, 3, 5]
ys = xs
# The actual list object is modified, so ys is changed too
xs[:] = [x + 1 for x in xs]

Given an input list, an explicit loop can be used to replace the list elements with the results from the calculation - however, it's not straightforward. For example:

numbers = [1, 2, 3]
for n in numbers:
    n += 1

assert numbers == [1, 2, 3] # the list will not change! 

This kind of list modification is only possible if the underlying objects are actually modified - for example, if we have a list of lists, and modify each one:

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]]

Another way is to retain the index and assign back to the original list:

numbers = [1, 2, 3]
for i, n in enumerate(numbers):
    numbers[i] = n + 1

assert numbers == [2, 3, 4]

However, in almost every normal circumstance it will be a better idea to create a new list.

A not-so-special case: lowercasing a list of strings

Many duplicates of this question specifically seek to convert an input list of strings all to lowercase (or all to uppercase). This is not special; any practical approach to the problem will involve solving the problems "lowercase a single string" and "repeat a calculation and collect the results" (i.e. this question). However, it is a useful demonstration case because the calculation involves using a method of the list elements.

The general approaches look like this:

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))

However, there are two interesting points to make here.

  1. Note how the map version differs. Although it is of course possible to make a function that takes in a string and returns the result of the method call, it is not necessary. Instead, we can directly look up the lower method from the class (here, str), which in 3.x results in a perfectly ordinary function (and in 2.x results in an "unbound" method which can then be called with the instance as an explicit parameter - which amounts to the same thing). When a string is passed to str.lower, then, the result is a new string which is the lowercase version of the input string - i.e., exactly the function needed for map to work.
    The other approaches do not allow this kind of simplification; looping or using a comprehension / generator expression requires choosing a name (s in these examples) for the iteration (loop) variable.

  2. Sometimes, when writing the explicit loop version, people expect to be able to just write s.lower() and thus transform the string in-place, within the original strings list. As stated above, it is possible to modify lists with this general sort of approach - but only with methods that actually modify the object. Python's strings are immutable, so this doesn't work.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
-1

When the input is a string

Strings can be iterated directly. However, usually when the input is a string, a single string is also expected as output. A list comprehension will produce a list instead, and a generator expression similarly will produce a generator.

There are many possible strategies to join up the results into a string; but for the common case of "translating" or "mapping" each character in the string to some output text, it is simpler and more efficient to use built-in string functionality: the translate method of the string, along with the static method maketrans provided by the string class.

The translate method directly creates a string based on the characters in the input. It requires a dictionary where the keys are Unicode code point numbers (the result of applying ord to a single-character string), and the values are either Unicode code point numbers, strings, or None. It will iterate over the input string, looking it up by number. If the input character isn't found, it's copied to the output string (it will use a buffer internally, and only create a string object at the end). If the mapping does contain an entry for the character's code point:

  • If it's a string, that string will be copied.
  • If it's another code point, the corresponding character will be copied.
  • If it's None, nothing is copied (the same effect as an empty string).

Since these mappings are hard to create by hand, the str class provides a method maketrans to help. It can take a dictionary, or else either two or three strings.

  • When given a dictionary, it should be like the one that the translate method expects, except it can also use single-character strings as keys. maketrans will replace those with the corresponding code points.
  • When given two strings, they need to be of the same length. maketrans will use each character of the first string as a key, and the corresponding character in the second string as the corresponding value.
  • When given three strings, the first two strings work like before, and the third string contains characters that will be mapped to None.

For example, here is a demonstration of a simple ROT13 cipher implementation at the interpreter prompt:

>>> 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!'

The code produces rotated and normal versions of the uppercase and lowercase alphabets, then uses str.maketrans to map letters to the corresponding letter shifted 13 positions in the same case. Then .translate applies this mapping. For reference, the mapping looks like:

>>> 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}

which is not very practical to create by hand.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153