Search code examples
pythonlisttuplespython-itertools

How does the re-assignment behaviour emerge?


I'm generating here all possible arrangements of two possible outcomes over 6 positions:

paths = list(it.product([['ax','r',[],0], ['ax','l',[],0]], repeat=6))

What it should generate is something like:

(['ax', 'r', [], 0], ['ax', 'r', [], 0], ['ax', 'r', [], 0], ['ax',    
'r', [], 0], ['ax', 'r', [], 0], ['ax', 'r', [], 0])

Which is one element of the list which contains all outcomes.

I'd like to change now the 'ax' string according to the repetitive pattern
'yp', 'p', 'xp'

For this I've written something like this:

for path in paths:
    axis = 1
    for axis_slice in path:
        if(axis%3 == 1):
            axis_slice[0] = 'yp'
            print(axis_slice[0])
        elif(axis%3 == 2):
            axis_slice[0] = 'p'
            print(axis_slice[0])
        elif(axis%3 == 0):
            axis_slice[0] = 'xp'
            print(axis_slice[0])
        axis+=1
    print("axis: "+str(axis))
    print(path)

However the outcome is something like this:

(['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0])

Yet the print-statements in the lines above show me that they are correctly assigned, yet going one 'level' back the assignment ('yp', 'p', 'xp') diverges

yp
p
xp
yp
p
xp
axis: 7
(['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0], ['xp', 'r', [], 0])

How does this behaviour emerge? Why is it not visible in the inner loop but in the outer loop? I sense it may have something to do with the order of evaluation and the way tuples/lists are packed.

How can the aimed output be achieved?


Solution

  • Your problem is that all the members of the lists within each path in paths are the same object. To verify this for yourself - try:

    import itertools as it
    paths = list(it.product([['ax','r',[],0], ['ax','l',[],0]], repeat=6))
    for path in paths:
        print([id(p) for p in path])
    

    This prints the unique identifier for each list in the path. You will see you only ever have two ids - the ids of the original two lists you passed to it.product.

    So, for example, if you take the first path:

    (['ax', 'r', [], 0], ['ax', 'r', [], 0], ['ax', 'r', [], 0], ['ax', 'r', [], 0], ['ax', 'r', [], 0], ['ax', 'r', [], 0])
    

    It is actually a tuple of 6 references to the same list. Thus, as you iterate through them, you change the same list each time. If you print(path) instead of axis_slice[0] each time you alter it, you will see clearly what is happening - all 6 members of path will change each time. This is a feature of list mutability and using itertools.product on a list with mutable members.

    This effect is covered really well by Python and SO veteran Ned Batchelder here. In particular - look at the slide called "Mutable aliasing" and the following - although the whole article is really worth a read.

    How you get around this problem will be highly dependent on the context of this code.