Python Language
Mutable vs Immutable (и Hashable) в Python
Поиск…
Mutable vs Immutable
В Python существует два типа типов. Неизменяемые типы и изменяемые типы.
Immutables
Объект неизменяемого типа нельзя изменить. Любая попытка изменить объект приведет к созданию копии.
Эта категория включает в себя: целые числа, поплавки, сложные, строки, байты, кортежи, диапазоны и фризоздаты.
Чтобы выделить это свойство, давайте поиграем с встроенным id
. Эта функция возвращает уникальный идентификатор объекта, переданного как параметр. Если идентификатор один и тот же, это тот же объект. Если он изменится, это другой объект. (Некоторые говорят, что это на самом деле адрес памяти объекта, но остерегайтесь их, они из темной стороны силы ...)
>>> a = 1
>>> id(a)
140128142243264
>>> a += 2
>>> a
3
>>> id(a)
140128142243328
Ладно, 1 не 3 ... Ломать новости ... Может быть, нет. Однако это поведение часто забывается, когда речь идет о более сложных типах, особенно в строках.
>>> stack = "Overflow"
>>> stack
'Overflow'
>>> id(stack)
140128123955504
>>> stack += " rocks!"
>>> stack
'Overflow rocks!'
Ага! Увидеть? Мы можем изменить его!
>>> id(stack)
140128123911472
Нет. Хотя кажется, что мы можем изменить строку, названную stack
переменных, то, что мы на самом деле делаем, создает новый объект, содержащий результат конкатенации. Мы обманываем, потому что в процессе старый объект никуда не уходит, поэтому он уничтожается. В другой ситуации это было бы более очевидно:
>>> stack = "Stack"
>>> stackoverflow = stack + "Overflow"
>>> id(stack)
140128069348184
>>> id(stackoverflow)
140128123911480
В этом случае ясно, что если мы хотим сохранить первую строку, нам нужна копия. Но разве это так очевидно для других типов?
Упражнение
Теперь, зная, как работают неизменные типы, что бы вы сказали с помощью кода ниже? Это мудро?
s = ""
for i in range(1, 1000):
s += str(i)
s += ","
Mutables
Объект изменчивого типа может быть изменен и изменен на месте . Никаких неявных копий не производится.
В эту категорию входят: списки, словари, bytearrays и sets.
Давайте продолжим играть с нашей маленькой функцией id
.
>>> b = bytearray(b'Stack')
>>> b
bytearray(b'Stack')
>>> b = bytearray(b'Stack')
>>> id(b)
140128030688288
>>> b += b'Overflow'
>>> b
bytearray(b'StackOverflow')
>>> id(b)
140128030688288
(В качестве побочного примечания я использую байты, содержащие данные ascii, чтобы сделать мой вопрос ясным, но помните, что байты не предназначены для хранения текстовых данных. Молитесь, простите меня.)
Что у нас есть? Мы создаем bytearray, модифицируем его и используем id
, мы можем гарантировать, что это один и тот же объект, модифицированный. Не копия.
Конечно, если объект будет часто изменяться, изменяемый тип выполняет гораздо лучшую работу, чем неизменяемый тип. К сожалению, реальность этого свойства часто забывается, когда это больно больше всего.
>>> c = b
>>> c += b' rocks!'
>>> c
bytearray(b'StackOverflow rocks!')
Хорошо...
>>> b
bytearray(b'StackOverflow rocks!')
Вайит второй ...
>>> id(c) == id(b)
True
В самом деле. c
не является копией b
. c
- b
.
Упражнение
Теперь вам лучше понять, какой побочный эффект подразумевается изменчивым типом, можете ли вы объяснить, что в этом примере происходит неправильно?
>>> ll = [ [] ]*4 # Create a list of 4 lists to contain our results
>>> ll
[[], [], [], []]
>>> ll[0].append(23) # Add result 23 to first list
>>> ll
[[23], [23], [23], [23]]
>>> # Oops...
Mutable and Immutable как аргументы
Одним из основных вариантов использования, когда разработчику необходимо учитывать изменчивость, является передача аргументов функции. Это очень важно, потому что это определит способность функции изменять объекты, которые не относятся к ее области действия, или, другими словами, если функция имеет побочные эффекты. Это также важно понять, где должен быть доступен результат функции.
>>> def list_add3(lin):
lin += [3]
return lin
>>> a = [1, 2, 3]
>>> b = list_add3(a)
>>> b
[1, 2, 3, 3]
>>> a
[1, 2, 3, 3]
Здесь ошибка заключается в том, что lin
, как параметр функции, может быть изменен локально. Вместо этого, lin
и ссылка тот же объект. a
Поскольку этот объект изменен, модификация выполняется на месте, что означает, что объект, на который ссылаются как lin
и a
, изменяется. lin
действительно не нужно возвращать, потому что у нас уже есть ссылка на этот объект в виде a
. a
и b
ссылающиеся на один и тот же объект.
Это не похоже на кортежи.
>>> def tuple_add3(tin):
tin += (3,)
return tin
>>> a = (1, 2, 3)
>>> b = tuple_add3(a)
>>> b
(1, 2, 3, 3)
>>> a
(1, 2, 3)
В начале функции, tin
и a
качестве ссылки и тот же объект. Но это непреложный объект. Поэтому, когда функция пытается ее изменить, tin
получает новый объект с модификацией, а a
сохраняет ссылку на исходный объект. В этом случае возврат tin
является обязательным, или новый объект будет потерян.
Упражнение
>>> def yoda(prologue, sentence):
sentence.reverse()
prologue += " ".join(sentence)
return prologue
>>> focused = ["You must", "stay focused"]
>>> saying = "Yoda said: "
>>> yoda_sentence = yoda(saying, focused)
Примечание: reverse
работает на месте.
Что вы думаете об этой функции? Имеет ли он побочные эффекты? Нужно ли возвращение? После звонка, какова ценность saying
? focused
? Что произойдет, если функция снова вызвана с теми же параметрами?