Search code examples
pythoniteratorpython-2.x

Loop iterator unexpected behavior


I have two functions of very similar construction. The first creates a 3D list of dicts and adds data (the data consisting of the values of the three iterators used to construct the list). The second simply displays the dict data. The issue is that data displayed by the second function is wrong. Instead of showing values of the iterators increasing as expected:

(0, 9, 19)
(0, 9, 20)
(0, 10, 0)
(0, 10, 1)
(0, 10, 2)

I end up with the maximum value of the iterators being displayed:

(0, 12, 20)
(0, 12, 20)
(0, 12, 20)
(0, 12, 20)
(0, 12, 20)

Code as follows:

xCount = 13
yCount = 21
numPages = 1
skip = [[4, 7], [4, 13], [8, 7], [8, 13]]

def createFrames():
  frameData = {'textFrameName': ''}
  framesData = [[[frameData for y in range(yCount)] for x in range(xCount)] for p in range(numPages)]

  for p in range(numPages):
    for x in range(xCount):
      for y in range(yCount):

        skipFrame = False

        for entry in skip:
          if entry[0] == x and entry[1] == y:
            skipFrame = True

        if skipFrame == False:
          framesData[p][x][y]['textFrameName'] = str(p) + ', ' + str(x) + ', ' + str(y)
          print(framesData[p][x][y]['textFrameName']) # shows p, x, y incrementing

  return framesData

def displayData(framesData):
  for p in range(numPages):
    for x in range(xCount):
      for y in range(yCount):

        skipFrame = False

        for entry in skip:
          if entry[0] == x and entry[1] == y:
            skipFrame = True

        if skipFrame == False:
          print(framesData[p][x][y]['textFrameName']) # shows 0, 12, 20 only


# Create text frames.
framesData = createFrames()

# Add data to dictionaries.
displayData(framesData)

Using python 2.7.16.


Solution

  • The problem comes from the list comprehension. You use frameData which is mutable, and so everytime you update it, you update it everywhere.

    Below is a simplified process that shows the same behaviour:

    >>> a = {"a" : 1}
    >>> l = [a for _ in range(3)]
    >>> l
    [{'a': 1}, {'a': 1}, {'a': 1}]
    >>> l[0]["a"] = 2
    >>> l
    [{'a': 2}, {'a': 2}, {'a': 2}] 
    

    You can use .copy() to create a copy, or just create the object everytime in the comprehension

    >>> a = {"a" : 1}
    >>> l2 = [a.copy() for _ in range(3)]
    >>> l2[0]["a"] = 2
    >>> l2
    [{'a': 2}, {'a': 1}, {'a': 1}]
    >>> # or
    >>> l3 = [{"a" : 1} for _ in range(3)]
    >>> l3[0]["a"] = 2
    >>> l3
    [{'a': 2}, {'a': 1}, {'a': 1}]