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

Python, в целом, является языком pass-by-reference. Что это значит, и на что вам нужно обратить внимание?

Следите за @bascodes

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

В Python любая переменная, указывающая на объект, на самом деле не содержит копии его значения.

Давайте посмотрим, что это значит.

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)
Войти в полноэкранный режим Выход из полноэкранного режима

Интересно, что в итоге мы получаем и вашу, и мою пиццу, увенчанные ['Анчоусы', 'Оливки', 'Ананас', 'Ветчина'], что опять же, вероятно, не то, что мы хотели в первую очередь.

Причина неправильного заказа пиццы в том, что мы изначально создали объект (в нашем случае список, используя = []). Теперь, когда у нас есть две одинаковые переменные (my_pizza_toppings, и your_pizza_toppings), указывающие на этот один элемент, они оказываются одинаковыми. Как будто мы пишем на одном и том же листе бумаги при обращении к любой из этих переменных.

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

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

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)
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь у нас есть два разных заказа:

['Anchovies', 'Olives']
['Pineapple', 'Ham']
Войти в полноэкранный режим Выйти из полноэкранного режима

На что обратить внимание?

Хотя приведенный выше аргумент, возможно, не является умопомрачительным и простым для понимания, есть несколько распространенных подводных камней, на которые, как я вижу, время от времени попадаются разработчики Python (включая меня самого).

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

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

class Pizza:
    toppings = []

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

    ...

    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)
Войти в полноэкранный режим Выход из полноэкранного режима

И снова мы получим одну и ту же отвратительную пиццу: ['Анчоусы', 'Оливки', 'Ананас', 'Ветчина']. Почему так?

Обратите внимание, что на этот раз мы создали два экземпляра Pizza, и мы не присвоили две переменные одному объекту. Но все равно наши пиццы будут серьезно перепутаны.

Причина в том, что мы создали пустой список в теле нашего класса Pizza. Это создает, как и следовало ожидать, пустой список. Но интерпретатор Python создает его только один раз, в частности, при загрузке класса. Так что в итоге у вас будет экземпляр, использующий атрибут class 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('Anchovies')
['Anchovies']
Войти в полноэкранный режим Выйти из полноэкранного режима

Все выглядит хорошо, так что давайте снова закажем две пиццы.

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)
Войти в полноэкранный режим Выход из полноэкранного режима

О нет! Опять моя_пицца_топпингс, и ваша_пицца_топпингс одинаковые:

['Anchovies', 'Olives', 'Pineapple', 'Ham']
Войти в полноэкранный режим Выход из полноэкранного режима

Что здесь произошло? Опять же, кажется, что мы все сделали правильно, но все равно все перепуталось.

Причина в определении функции. Как и в случае с атрибутом class в нашем классе 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
Вход в полноэкранный режим Выход из полноэкранного режима

В отличие от определения пустого списка в определении функции, на этот раз при каждом вызове функции без этого необязательного параметра создается новый пустой список.

Дразнилка для мозгов

Теперь, когда мы узнали о предостережениях, связанных с передачей по ссылке, мы можем взглянуть на этот мозговой тизер, на который я наткнулся в Твиттере Реувена Лернера:

with open('some_file.txt') as f:
    for one_line in f:
        f = 6
        print(one_line)
Войти в полноэкранный режим Выход из полноэкранного режима

Похоже, что мы перезаписали переменную f, так что этот маленький скрипт должен каким-то образом остановиться на следующей итерации цикла. Однако он работает нормально и печатает весь файл от первой до последней строки.

См. объяснение Реувена, почему так происходит, в оригинале Tweet.

Борги, борги, борги

Иногда совместное использование одного и того же объекта в коде — это именно то, что вам нужно.

Например, вы можете создать класс, который де-факто ведет себя как синглтон, используя паттерн borg. Паттерн borg называется паттерном borg как отсылка к боргам в фильме Star Trek, где они связаны в ульевой разум под названием Коллектив.

Я подробно объяснял паттерн Borg в своем блоге здесь.

В этой статье, однако, мы узнали, как избежать подобных подводных камней, когда мы не хотим обмениваться информацией в предположительно различных переменных. Я иногда называю примеры в этой статье невольными боргами.

Оцените статью
Procodings.ru
Добавить комментарий