В 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`, это атрибут класса. Интерпретатор 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 единожды прямо при определении самой функции. Таким образом, любой вызов этой функции с таким параметром по умолчанию, в итоге будет работать с одним и тем же списком, который создался сразу при определении.
Причина здесь заключается в самом определении функции. Так же, как это было в случае с атрибутом класса в `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