Trusted answers to developer questions
Trusted Answers to Developer Questions

Related Tags

python

What are involuntary borgs in Python?

Bas Steins

Overview

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.

Pass by reference

In Python, any variable that points to an object doesn’t actually hold a copy of its value.

Code example

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)
Passing by reference

Code explanation

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.

How to fix that problem

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)
Problem solution

Now, we have two distinct orders.

What to look out for?

The argument above has some common pitfalls that Python developers trap in from time to time.

The object-oriented pizza shop

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)
By using object-oriented approach

Code explanation

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.

How to fix that

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)
Problem solution

Code explanation

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.

Mess up in functions

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 toppings

print(add_topping('Anchovies'))

Now, let’s go and order our two pizzas again:

def add_topping(topping_name, toppings=[]):
    toppings.append(topping_name)
    return toppings

my_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)
By using function

Explanation

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.

How to fix that

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 toppings

my_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)
Problem solution

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.

A brain teaser

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 = 6
        print(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.

RELATED TAGS

python
RELATED COURSES

View all Courses

Keep Exploring