2

Possible Duplicate:
“Least Astonishment” in Python: The Mutable Default Argument

Okay, so basically I have this code:

 # Our Rule type.

class Rule(object):
    """An object to represent a rule and include
    methods to apply these rules."""

    def __init__(self,bind="F",rule=["F"]):
        self.bind = bind
        self.rule = rule

    def show(self):
        try:
            print self.bind
            #print self.rule
            for i in range(len(self.rule)):
                print self.rule[i]
        except:
            print "Incomplete"

    def inflate(self,seq):
        for i in range(len(seq)):
            if seq[i] == self.bind:
                seq[i] = self.rule
            elif type(seq) == type(["X"]):
                seq[i] = self.inflate(seq[i])
        return seq

    def inflate_depth(self,seq,depth):
        while depth > 0:
            seq = self.inflate(seq)
            depth = depth - 1
            print self.rule
        return seq

I call it from another file with this code:

pity = Rule(rule=["R","F","L","F","L","F","R","F"])

seq = ["F"]

inf = pity.inflate_depth(seq,2)

So, I should end up with a list that looks like this:

[['R', [...], 'L', [...], 'L', [...], 'R', [...]]]

Which seems to work fine, but there is one fundamental error.

self.rule

has been altered to contain ['R', [...], 'L', [...], 'L', [...], 'R', [...]]

Why? There is no where whatsoever I assign a new value to that variable, but it changes nonetheless.

Community
  • 1
  • 1
  • @delnan: In this case the most immediate issue isn't the default argument, but rather that a reference to an attribute is being passed around when a copy of the attribute's value was desired (though the mutable default argument is also a potential problem). – Nathan Oct 27 '11 at 18:52

4 Answers4

2

Default arguments are evaluated once when the function is defined, and reused later.

def f(x, a = [])
    a.append(x)
    return a
print f(0)
print f(1)

prints

[0]
[0, 1]

The usual way to avoid this problem is to use None as default parameter:

def f(x, a = None):
    if a is None:
        a = []
    # ...

See the Python tutorial for a detailed explanation.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
2

I'm not sure if this is the problem, but you have to be very careful about setting a default list in a function like this:

def __init__(self,bind="F",rule=["F"]):
    self.bind = bind
    self.rule = rule

Each time you call this function, it's going to use the same list every time! Therefore, every time you modify self.rule in one Rule object, you are modifying it in every other object you have.

Instead, you should do this:

def __init__(self,bind="F",rule=None):
    self.bind = bind
    if not rule:
        self.rule = ['F']
    else:
        self.rule = rule

This way, you create a new list object every time, instead of reusing the same one.


For example:

class foo:
  def __init__(self, x=[]):
     self.x = x
     self.x.append(3)

d = foo()
e = foo()
f = foo()
print f.x   # prints [3,3,3]
Donald Miner
  • 38,889
  • 8
  • 95
  • 118
2

I'm not 100% sure about this but I suspect the problem is that variables in Python are references and not values. That is to say, when you set seq[i] = self.rule, seq[i] points to where self.rule is stored in memory and any changes made to seq[i]'s value results in a change to self.rule since their values are stored at the same location in memory.

What you probably should do is deep copy self.rule's value into seq[i] rather than simply assign it so that they use two different memory addresses to store their values (see http://docs.python.org/library/copy.html for more information), that way assigning to seq[i] won't affect self.rule.

Jeffrey Charles
  • 773
  • 5
  • 10
  • 1
    I'm not sure what you're stating exactly, and you may be completely right, but the way it's stated now is IMHO rather confusings. Python uses things called references, but those share only the name with C++ references and pass-by-reference. After `seq[i] = self.rule`, `seq[i] = ...` will not change `self.rule`, although `seq[i].attr = ...` will change *the object* `self.rule` *points to* (i.e. the change to `attr` is visible as `self.rule.attr`). –  Oct 27 '11 at 14:31
  • Deepcopy solved the problem. Thanks! – user1016655 Oct 27 '11 at 14:50
1

It changes because rule is a reference to a list. You then assign seq[i] to refer to the object pointed to by self.rule. So when you modify seq, you're modifying the same object referenced by self.rule.

>>> a = [1,2,3,4]
>>> b = a
>>> b.append(5)
>>> print a
[1, 2, 3, 4, 5]

I believe that what you are looking for is a copy of the list stored in rule. This can be done in a couple ways. In the first, you construct a new list from the current iterable.

seq[i] = list(self.rule) # Makes a new list that is a copy of self.rule

If self.rule contains a list of immutables (integers, strings, etc.), then that should be sufficient. If, however, self.rule contains mutables, like class instances, then what you'll end up with is a new list that references all of the same objects that were referenced in self.rule. What that means can be seen in the following code:

>>> class A(object):
...     def __init__(self, val):
...        self.x = val

>>> a = [A(i) for i in xrange(10)]
>>> print a[0].x
0
>>> print a[1].x
1
>>> b = list(a)
>>> b.append(A(10))
>>> print b[0].x
0
>>> b[0].x = 99
>>> print b[0].x
99
>>> print a[0].x
99
>>> print (len(a), len(b))
(10, 11)

Here we have two different lists, but two different lists that both have 10 references in common. List b has an additional reference, further cementing its difference. To avoid that issue, you can use the second method, the deepcopy.

>>> from copy import deepcopy
>>> a = [A(i) for i in xrange(10)]
>>> b = deepcopy(a)
>>> b[0].x = 99
>>> print b[0].x
99
>>> print a[0].x
0

This makes a copy of the list and all of the objects in that list.

Finally, I have to caution you against using a list as a default value for the rule keyword argument. That almost certainly will not work the way you expect. The keyword argument rule gets an instance of the list ["F"] at definition. All calls to __init__ will get the same list instance, so modifying that list will affect the default behavior as rule changes. See Default value of memberfunction for another example.

Community
  • 1
  • 1
Nathan
  • 4,777
  • 1
  • 28
  • 35