-2

Please don't close this question if you don't really understand what's happening in this code. This is different than "Least Astonishment" and the Mutable Default Argument

In the following code, I defined a clone method where it calls the "constructor" without parameters. But within the __init__ method, the value of the parameter keeps changing (see the WARNING lines of the output). Why is that?

Code:

#!/usr/bin/env python3

class Foo:
    def __init__(self, aDic = {}):
        self.aDic = aDic
        print("__init__")
        if aDic:
            print("WARNING: aDic is not empty {}".format(aDic))
        self.printme()

    def printme(self):
        print("aDic = {}".format(self.aDic))

    def clone(self, props):
        print("Before calling Foo() without parameters")
        newValue = Foo()
        print("after calling Foo() without parameters")
        newValue.printme()
        print("props = {}".format(props))
        print("props.get = {}".format(props.get("aDic", self.aDic)))
        for k, v in props.get("aDic", self.aDic).items():
            newValue.aDic[k] = v
        return newValue


a = Foo(aDic = {"b": 2})
a.printme()
print()

c = a.clone({"aDic": {"b": 222}})
c.printme()
print()

a.printme()
print()

d = a.clone({"aDic": {"b": 2222}})
d.printme()
print()

Output:

__init__
WARNING: aDic is not empty {'b': 2}
aDic = {'b': 2}
aDic = {'b': 2}

Before calling Foo() without parameters
__init__
aDic = {}
after calling Foo() without parameters
aDic = {}
props = {'aDic': {'b': 222}}
props.get = {'b': 222}
aDic = {'b': 222}

aDic = {'b': 2}

Before calling Foo() without parameters
__init__
WARNING: aDic is not empty {'b': 222}
aDic = {'b': 222}
after calling Foo() without parameters
aDic = {'b': 222}
props = {'aDic': {'b': 2222}}
props.get = {'b': 2222}
aDic = {'b': 2222}
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
zijuexiansheng
  • 337
  • 3
  • 14
  • 2
    Actually this is the same problem that happens in https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument. The default argument `aDict` is a mutable object and therefore if one call modifies it the next time you want the default value for `aDict` you will get the modified one – Matteo Zanoni Oct 24 '21 at 09:57

1 Answers1

1

This is related to "Least Astonishment" and the Mutable Default Argument.

In clone(), you call Foo() without arguments, so newValue.aDic will be the shared dict from the argument list.

You then internally mutate that shared copy (newValue.aDic[k] = v), so all Foo()s you get form the clone() function will see those changes.

A modified version of your example illustrates this. See how the ids are the same?

class Foo:
    def __init__(self, aDic = {}):
        self.aDic = aDic

    def printme(self):
        print(f"aDic({id(self.aDic)}) = {self.aDic}")

    def clone(self, props):
        newValue = Foo()
        newValue.printme()
        for k, v in props.get("aDic", self.aDic).items():
            newValue.aDic[k] = v
        return newValue


print("a")
a = Foo(aDic = {"b": 2})
a.printme()
print("c")
c = a.clone({"aDic": {"b": 222}})
c.printme()
print("d")
d = a.clone({"aDic": {"b": 2222}})
d.printme()

prints out (e.g.)

a
aDic(4494684352) = {'b': 2}
c
aDic(4494684544) = {}
aDic(4494684544) = {'b': 222}
d
aDic(4494684544) = {'b': 222}
aDic(4494684544) = {'b': 2222}
AKX
  • 152,115
  • 15
  • 115
  • 172