2

I have two classes, a main class which creates instances of my other class.

class Builder:

    def __init__(self, id):
        self.id = id

    def build_thing(self, main_ftr, main_attr, attrs = {}):
        # note the main ftr/attrs gets added to attrs no matter what
        attrs[main_ftr] = attrs.get(main_ftr, []) + [main_attr]

        return Thing(main_ftr, main_attr, attrs)

class Thing:

    def __init__(self, main_ftr, main_attr, attrs):
        self.main_ftr = main_ftr
        self.main_attr = main_attr
        self.attrs = attrs

The issue I'm having has to do with the attrs dictionary that gets passed to the Thing class. The problem is that each time I use the Builder class to create a Thing class, the attrs argument retains it's previous values

b = Builder('123')

t = b.build_thing('name', 'john')
print(t.attrs) # {'name': ['john'] }

# Goal is this creates a new "Thing" with only attrs = {'name':['mike']}
t2 = b.build_thing('name', 'mike')
print(t2.attrs) # {'name': ['john', 'mike']}

My Question is 2 part:

Why is this happening?

How do I fix it?

RSHAP
  • 2,337
  • 3
  • 28
  • 39
  • Possible duplicate of ["Least Astonishment" and the Mutable Default Argument](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) – Giacomo Alzetta Aug 22 '18 at 15:17

1 Answers1

3

Functions' optional arguments are initialized once. Since attrs is mutable, each time you call the function, you add new key-value pair to this dictionary and it is kept for further calls. If you need a mutable data structure as a default parameter, use something like:

def build_thing(self, main_ftr, main_attr, attrs=None):
    if attrs is None:
        attrs = {}
    attrs[main_ftr] = attrs.get(main_ftr, []) + [main_attr]
    return Thing(main_ftr, main_attr, attrs)
Sasha Tsukanov
  • 1,025
  • 9
  • 20
  • Weird, just to clarify - this applies to any function that takes a default mutable argument (nothing to do with classes)? Therefore that function has that arg value tied to it for the life of the function/script? – RSHAP Aug 22 '18 at 15:14
  • @RSHAP Exactly! – Sasha Tsukanov Aug 22 '18 at 15:16
  • 1
    @RSHAP For more scoping things check out: `funcs = [(lambda: x) for x in range(5)]` and `funcs = [(lambda x=x: x) for x in range(5)]` and see the difference between `for f in funcs: print(f())` with the two definitions... – Giacomo Alzetta Aug 22 '18 at 15:19