Понимание изменяемых объектов в 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. Мы сосредоточились на том, как изменяемые объекты, а точнее списки, могут привести к неожиданным последствиям, если вы не знаете, как они работают.
- Подпишитесь на обновления по электронной почте.
- Станьте участником Medium и получите полный доступ ко всем историям. Ваш членский взнос напрямую поддерживает авторов, которых вы читаете.
Другие работы того же автора.