Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Copied list passed to generator reflects changes made to original

In answering this question, I stumbled across some unexpected behavior:

from typing import List, Iterable


class Name:
    def __init__(self, name: str):
        self.name = name


def generator(lst: List[Name]) -> Iterable[str]:
    lst_copy = lst.copy()
    for obj in lst_copy:
        yield obj.name

When modifying the list that is passed to the generator, even though a copy is made, changes to the original list are still reflected:

lst = [Name("Tom"), Name("Tommy")]
gen = generator(lst)
lst[0] = Name("Andrea")
for name in gen:
    print(name)

Output:

Andrea
Tommy

Simply returning a generator expression works as expected:

def generator(lst: List[Name]) -> Iterable[str]:
    return (obj.name for obj in lst.copy())

Output:

Tom
Tommy

Why doesn't the lst.copy() in the first generator function work as expected?

like image 508
ddejohn Avatar asked Oct 24 '25 18:10

ddejohn


2 Answers

I think the behavior is best understood with the addition of some extra print statements:

def generator(lst: List[Name]) -> Iterable[str]:
    print("Creating list copy...")
    lst_copy = lst.copy()
    print("Created list copy!")
    for obj in lst_copy:
        yield obj.name
        
lst = [Name("Tom"), Name("Tommy")]
print("Starting assignment...")
gen = generator(lst)
print("Assignment complete!")

print("Modifying list...")
lst[0] = Name("Andrea")
print("Modification complete!")

for name in gen:
    print(name)

Notice that the copy does not happen at assignment time -- it happens after the list is modified!

Starting assignment...
Assignment complete!
Modifying list...
Modification complete!
Creating list copy...
Created list copy!
Andrea
Tommy

Nothing in the generator's body is executed until the for loop attempts to extract an element. Since this extraction attempt occurs after the list is mutated, the mutation is reflected in the results from the generator.

like image 177
BrokenBenchmark Avatar answered Oct 27 '25 09:10

BrokenBenchmark


The body of a generator does not start executing until the first item is requested. So in this code:

def generator(lst: List[Name]) -> Iterable[str]:
    lst_copy = lst.copy()
    for obj in lst_copy:
        yield obj.name

lst = [Name("Tom"), Name("Tommy")]
gen = generator(lst)
lst[0] = Name("Andrea")
for name in gen:
    print(name)

... First, the lst[0] = Name("Andrea") is executed. Then, you have a for loop, which starts executing the generator. That's when lst_copy = lst.copy() is executed, which is too late to get in before the lst[0] assignment.

The generator expression works, because the iterable portion of the generator (lst.copy(), the last part) must be evaluated before creating the iterator.

like image 28
luther Avatar answered Oct 27 '25 09:10

luther



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!