In this Answer, we will learn about how to avoid such pitfalls when we do not want to share information in presumably different variables.
For example, we can create a class that behaves de facto like implementation of a singleton behavior using the borg pattern. The only difference is that; it allows multiple instances unlike singleton and focuses more on sharing state instead of sharing instance identity.
The examples used in this Answer are referred to as involuntary borgs.
In Python, any variable that points to an object doesn’t actually hold a copy of its value.
Let’s look at the code below:
my_pizza_toppings = your_pizza_toppings = []my_pizza_toppings.append('Anchovies')my_pizza_toppings.append('Olives')your_pizza_toppings.append('Pineapple')your_pizza_toppings.append('Ham')print(my_pizza_toppings)print(your_pizza_toppings)
Interestingly, we end up having both pizzas topped with ['Anchovies', 'Olives', 'Pineapple', 'Ham']
, which probably isn’t what we wanted in the first place.
The reason for this misled pizza order is that we initially created an object (in our case, a list by using = []
). When two identical variables (my_pizza_toppings
, and your_pizza_toppings
) point to that one element, they are the same. It is as if we write on the same sheet of paper when accessing any of these variables.
Luckily, it is straightforward to fix the problem by intentionally creating two independent lists. Think of this as a sheet of paper for each of us to write down our pizza order. Our code looks like this:
my_pizza_toppings = []your_pizza_toppings = []my_pizza_toppings.append('Anchovies')my_pizza_toppings.append('Olives')your_pizza_toppings.append('Pineapple')your_pizza_toppings.append('Ham')print(my_pizza_toppings)print(your_pizza_toppings)
Now, we have two distinct orders.
The argument above has some common pitfalls that Python developers trap in from time to time.
Let’s say we have modeled our pizza shop with an object-oriented approach in mind. Chances are, we have a class Pizza
somewhere in our code, which might look like this:
class Pizza:toppings = []def add_topping(self, topping):self.toppings.append(topping)
Let’s see what happens if the two of us will order a pizza. Interestingly, we run into the same problem:
class Pizza:toppings = []def add_topping(self, topping):self.toppings.append(topping)my_pizza = Pizza()my_pizza.add_topping('Anchovies')my_pizza.add_topping('Olives')your_pizza = Pizza()your_pizza.add_topping('Pineapple')your_pizza.add_topping('Ham')print(my_pizza.toppings)print(your_pizza.toppings)
Again, we get the same pizza with ['Anchovies', 'Olives', 'Pineapple', 'Ham']
toppings. Why is that?
Note: We create two instances of
Pizza
this time, and we do not assign two variables to the same object. But still, we get the wrong pizza order.
The reason for this is that we create an empty list in the body of our class Pizza
. This creates an empty list. But the Python interpreter creates an empty list only once in particular when the class is loaded. So, in the end, we have an instance using the class
attribute toppings
. And it is the same.
We could rewrite our Pizza
class like the following code:
class Pizza:def __init__(self):self.toppings = []def add_topping(self, topping):self.toppings.append(topping)my_pizza = Pizza()my_pizza.add_topping('Anchovies')my_pizza.add_topping('Olives')your_pizza = Pizza()your_pizza.add_topping('Pineapple')your_pizza.add_topping('Ham')print(my_pizza.toppings)print(your_pizza.toppings)
We move the initial list creation to the __init__
method of our class. Well, the __init__
method is called every time a new instance is created, and therefore it creates a new instance attribute for each new object rather than each instance relying on the same class attribute.
Let’s say, we have a class that adds something to a list. For convenience, if we do not already have a list, the function kindly creates one for us. Consider the code below:
def add_topping(topping_name, toppings=[]):toppings.append(topping_name)return toppingsprint(add_topping('Anchovies'))
Now, let’s go and order our two pizzas again:
def add_topping(topping_name, toppings=[]):toppings.append(topping_name)return toppingsmy_pizza_toppings = add_topping('Anchovies')my_pizza_toppings = add_topping('Olives', my_pizza_toppings)your_pizza_toppings = add_topping('Pineapple')your_pizza_toppings = add_topping('Ham', your_pizza_toppings)print(my_pizza_toppings)print(your_pizza_toppings)
Oh no! Again, my_pizza_toppings
and your_pizza_toppings
are the same:
What happened here? Again, it looks like we have done everything correctly, but still, it all got messed up.
The reason here is the function’s definition. Just as was the case for the class attribute in our Pizza
class, the default argument (toppings=[]
) is evaluated only once by Python, which is when the function is defined. So any call to that function that omits the default argument will return that one instance of our initially empty list.
We can change the default value of the toppings
parameter to None
and check for None
inside the function. If we see a None
value, we can create the list right there.
def add_topping(topping_name, toppings=None):if toppings is None:toppings = []toppings.append(topping_name)return toppingsmy_pizza_toppings = add_topping('Anchovies')my_pizza_toppings = add_topping('Olives', my_pizza_toppings)your_pizza_toppings = add_topping('Pineapple')your_pizza_toppings = add_topping('Ham', your_pizza_toppings)print(my_pizza_toppings)print(your_pizza_toppings)
As opposed to the definition of the empty list in the function’s definition, this time, a new empty list gets created every time the functions is called without that optional parameter.
Now that we have learned about the caveats of pass by reference issues, we can now have a look at this brain teaser.
with open('some_file.txt') as f:for one_line in f:f = 6print(one_line)
It looks like we overwrite the f
variable so that this little script should somehow stop in the next iteration of the loop. However, it runs just fine and prints the whole file from its first to the last line.
Free Resources