Понимание изменяемых объектов в Python.

Я помню, как несколько лет назад, когда я переключился с R на Python, мне пришлось привыкать к изменяемым объектам, которых в R.

Изменяемые объекты - это объекты, которые можно изменять после создания. Некоторыми примерами изменяемых объектов в Python являются списки, словари и наборы. В приведенном ниже примере мы расширяем y после его создания.

y = [1, 2, 3]
id(y)
Out[7]: 4831904704
y.extend([4, 5, 6])
y
Out[10]: [1, 2, 3, 4, 5, 6]
id(y)
Out[11]: 4831904704

Мы видим, что он сохраняет свой уникальный идентификационный номер (заданный функцией id) - это означает, что мы все еще работаем с тем же объектом.

С другой стороны, неизменяемые объекты не могут быть изменены. Некоторыми примерами являются кортежи, строки, целые числа, логические значения и т. Д. Как вы можете видеть в примере ниже, когда мы изменим значение x, его идентичность изменится - мы переназначили x на 2.

x = 1
id(x)
Out[3]: 4564694112
x = 2
id(x)
Out[5]: 4564694144

Если вы не привыкли к изменяемым объектам, довольно легко написать код, который имеет… непредвиденные последствия.

Давайте посмотрим на несколько примеров (чего не следует делать… в большинстве случаев).

Список как аргумент по умолчанию?

Представьте, что вы пишете урок, чтобы отслеживать друзей каждого человека. Итак, вы делаете следующее.

class FriendBook:
    def __init__(self, name, friends=[]):
        self.name = name
        self.friends = friends

    def add_new_friend(self, friend):
        if friend not in self.friends:
            self.friends.append(friend)

Затем вы начинаете создавать FriendBooks для разных людей.

person_a_friendbook = FriendBook(name='Person A')
person_b_friendbook = FriendBook(name='Person B')
person_c_friendbook = FriendBook(
  name='Person C', 
  friends=['Person E'],
)
person_a_friendbook.add_new_friend('Person D')

Теперь мы ожидаем, что у Человека Б на данный момент не будет друзей. Но этого не происходит.

person_a_friendbook.friends
Out[3]: ['Person D']
person_b_friendbook.friends
Out[5]: ['Person D']
person_c_friendbook.friends
Out[7]: ['Person E']

Это потому, что наш аргумент по умолчанию friends=[] создается только один раз, как только мы создаем класс. Мы можем проверить это, используя оператор is, который сравнивает их личности.

person_a_friendbook.friends is person_b_friendbook.friends
Out[8]: True

Мы видим, что person_a_friendbook.friends имеет ту же идентичность, что и person_b_friendbook.friends. Каждый раз, когда вы возвращаетесь к использованию значения по умолчанию для friends, вы будете использовать тот же список - это означает, что список будет использоваться всеми объектами, которые были созданы с аргументом по умолчанию. Это редко (скорее всего, никогда) то, чего мы хотим. Большинство IDE предупредят вас об этом.

Решение простое.

class FriendBook:
    def __init__(self, name, friends=None):
        self.name = name
        self.friends = friends or []

    def add_new_friend(self, friend):
        if friend not in self.friends:
            self.friends.append(friend)

Таким образом мы получаем новый пустой список для каждого объекта.

Но я только что изменил список внутри функции?

Переходя к следующему способу, мы можем ошибиться.

x = [1, 2, None, 4, 5, None]

def fill_none(data, fill_value):
    n = len(data)
    for i in range(n):
        if data[i] is None:
            data[i] = fill_value

    return data


y = fill_none(data=x, fill_value=100)

Вышеупомянутая функция перебирает data и заменяет значения None на fill_value.

y
Out[1]: [1, 2, 100, 4, 5, 100]

Значение y - это то, что мы ожидаем. Вы можете догадаться, что сейчас x? Если вы думаете, что это то же самое, что y, тогда вы правы.

x
Out[2]: [1, 2, 100, 4, 5, 100]

Когда мы передаем изменяемый объект в качестве аргумента функции, мы даем этой функции право изменять его. Теперь мы можем исправить это двумя способами. Первый - дать понять, что функция изменит список, если мы действительно этого хотим. На мой взгляд, это можно сделать, удалив оператор return и добавив строку документации.

x = [1, 2, None, 4, 5, None]

def fill_none(data, fill_value):
    '''
    Replace None values with the fill_value. Modifies data in-place.
    '''
    n = len(data)
    for i in range(n):
        if data[i] is None:
            data[i] = fill_value

fill_none(data=x, fill_value=100)

Второй вариант - работать с копией списка внутри функции.

x = [1, 2, None, 4, 5, None]

def fill_none(data, fill_value):
    data = data[:]  # make a copy of data
    n = len(data)
    for i in range(n):
        if data[i] is None:
            data[i] = fill_value
    return data
y = fill_none(data=x, fill_value=100)

Теперь x и y разные.

y
Out[13]: [1, 2, 100, 4, 5, 100]
x
Out[14]: [1, 2, None, 4, 5, None]

Вы уверены, что кортежи неизменяемы?

Если вы помните, в начале этого поста мы говорили, что кортежи неизменяемы, и они есть, у них нет методов, которые позволили бы нам их изменить. Одна из вещей, которые могут вас заинтересовать, - это «Могу ли я поместить в них список?» а затем «могу ли я изменить список внутри кортежа?». Ответ - «да» и «да».

my_list = [1, 2, 3]
my_tuple = ('a', 'b', my_list)
my_tuple
Out[1]: ('a', 'b', [1, 2, 3])
my_list.append('surprise')
my_tuple
Out[2]: ('a', 'b', [1, 2, 3, 'surprise'])

Почему так могло случиться? Что ж, неизменяемость кортежа означает, что мы не можем изменить, какие объекты он содержит после того, как мы его создали (но мы можем изменять объекты в нем, если они являются изменяемыми). В самом деле, даже после того, как мы изменили список, my_tuple по-прежнему содержит те же три элемента, которые имеют те же идентификаторы, что и при его создании. Это не обязательно «промах» или что-то, что вы хотите сделать, но может сбивать с толку, если вы этого не знаете.

Заключение

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

Другие работы того же автора.