Статьи

Всё, что нужно знать о непроизвольных боргах в Python

Базовый Python
В Python, в общем-то, реализована передача аргументов по ссылке. Что это значит, и почему нам это важно знать?

Передача по ссылке

В Python любая переменная, указывающая на объект, на самом деле не содержит копии его значения.
Давайте посмотрим, что это значит.
my_pizza_toppings = your_pizza_toppings = []

my_pizza_toppings.append('Анчоусы')
my_pizza_toppings.append('Оливки')

your_pizza_toppings.append('Ананас')
your_pizza_toppings.append('Ветчина')
Не будем осуждать никого, кто любит пиццу с ананасами, но в итоге мы получим пиццу, которую, вероятно, никто вообще бы есть не стал:
print(my_pizza_toppings)
print(your_pizza_toppings)
В обоих пиццах начинка будет: `['Анчоусы', 'Оливки', 'Ананас', 'Ветчина']`, и вряд ли мы этого хотели.
Причина создания такой странной пиццы в том, что мы изначально создали один объект (в нашем случае список с помощью `= []`). И у нас есть две переменные (`my_pizza_toppings` и `your_pizza_toppings`), указывающие на этот список (один и тот же!). И любые его изменения мы увидим, обратившись к нему по любому из этих имён.

Как исправить эту проблему?

К счастью, проблему легко исправить изначально создав два независимых списка. Напишем такой код:
my_pizza_toppings = []
your_pizza_toppings = []

my_pizza_toppings.append('Анчоусы')
my_pizza_toppings.append('Оливки')

your_pizza_toppings.append('Ананас')
your_pizza_toppings.append('Ветчина')

print(my_pizza_toppings)
print(your_pizza_toppings)
Теперь у нас две разные пиццы: `['Анчоусы', 'Оливки']` и `['Ананас', 'Ветчина']`.

Почему это так важно?

Казалось бы, все действия и результаты выше очень логичны, но ошибки, связанные с пониманием этой концепции, совершают очень часто не только начинающие специалисты.

Объектно-ориентированная пиццерия

Давайте смоделируем нашу пиццерию на основе объектно-ориентированного подхода. Предположим, что в нашем коде есть класс Pizza, который может выглядеть следующим образом:
class Pizza:
    toppings = []

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

    def add_topping(self, topping):
        ...
        self.toppings.append(topping)
        ...
Давайте посмотрим, что произойдёт, если мы вдвоём закажем пиццу:
my_pizza = Pizza()
my_pizza.add_topping('Анчоусы')
my_pizza.add_topping('Оливки')

your_pizza = Pizza()
your_pizza.add_topping('Ананас')
your_pizza.add_topping('Ветчина')
Интересно, что мы получим сейчас?
print(my_pizza.toppings)
print(your_pizza.toppings)
И снова эта отвратительная пицца: `['Анчоусы', 'Оливки', 'Ананас', 'Ветчина']`!
Почемуууу? 😫
Обратим особое внимание, что на этот раз мы создали два экземпляра класса `Pizza`, и не создавали сами две переменные, которые ссылались бы на один и тот же объект. Но всё равно наша пицца будет испорчена.

Причина в том, что список ингредиентов мы создали на уровне нашего класса `Pizza`, это атрибут класса. Интерпретатор Python создаст этот список ровно один раз, именно при объявлении класса. В итоге мы получаем экземпляры класса, которые оперируют с одним и тем же атрибутом класса `toppings`. Это всё будет одна и та же переменная.

Как это исправить?

Мы могли бы переписать наш класс `Pizza` следующим образом:
class Pizza:

    def __init__(self, ...):
        self.toppings = []
    
    ...

    def add_topping(self, topping):
        ...
        self.toppings.append(topping)
        ...
Мы перенесли первоначальное создание списка в метод `__init__` нашего класса. В чём разница, спросите вы? Что ж, метод `__init__` вызывается каждый раз, когда создается новый экземпляр, и, таким образом, для каждого нового экземпляра будет создаваться свой атрибут, вместо одного общего для всех.

А теперь про функции

Допустим, нам для какой-то цели нужна функция, которая добавляет что-то в список. А если список ещё не создан, то функция любезно создаст его. Рассмотрим этот код:
def add_topping(topping_name, toppings = []):
    toppings.append(topping_name)
    return toppings
Во-первых, давайте проверим, работает ли эта функция так, как ожидалось:
add_topping('Анчоусы')
# ['Анчоусы']
Выглядит аппетитно, так что давайте пойдём и снова закажем две пиццы.
my_pizza_toppings = add_topping('Анчоусы')
my_pizza_toppings = add_topping('Оливки', my_pizza_toppings)

your_pizza_toppings = add_topping('Ананас')
your_pizza_toppings = add_topping('Ветчина', your_pizza_toppings)
О, нет! `my_pizza_toppings` и `your_pizza_toppings` снова одинаковые: `['Анчоусы', 'Оливки', 'Ананас', 'Ветчина']`.
А здесь что произошло? Вроде всё ведь правильно делали?
Причина здесь заключается в самом определении функции. Так же, как это было в случае с атрибутом класса в `Pizza`, аргумент по умолчанию (`toppings = []`) создается Python единожды прямо при определении самой функции. Таким образом, любой вызов этой функции с таким параметром по умолчанию, в итоге будет работать с одним и тем же списком, который создался сразу при определении.

Как это исправить?

Мы можем изменить значение параметра toppings по умолчанию на `None` и сравнить значение с `None` внутри функции. Если значение равно `None`, то уже будем создавать нужный список.
def add_topping(topping_name, toppings=None):
    if toppings is None:
        toppings = []
    toppings.append(topping_name)
    return toppings
В отличие от создания пустого списка в определении функции, на этот раз новый пустой список создаётся только тогда, когда функции вызываются без этого необязательного параметра.

А при чём тут борги?

Иногда совместное использование (ссылка на один и тот же объект) одного и того же объекта в вашем коде — это именно то, что может быть нужно.
Например, можно создать класс, который де-факто ведет себя как синглтон (Singleton class). Такой паттерн создания классов и называется borg как отсылка к боргам в "Star Trek", где они связаны коллективным разумом.
Однако в этой статье мы узнали, как избегать непроизвольного использования боргов и не хотим создавать объект, на который будут ссылаться различные действия.

Источник: Bas Codes