Всё, что нужно знать о непроизвольных боргах в 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