Изменяемый и неизменяемый (и Hashable) в Python
В 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!'
Ага! Увидеть? Мы можем изменить это!
Нет.Хотя, кажется , мы можем изменить строку с именем переменной stack , что мы на самом деле делаем, создает новый объект , чтобы содержать результат конкатенации. Мы одурачены, потому что при этом старый объект никуда не уходит, поэтому он уничтожается. В другой ситуации это было бы более очевидно:
>>> stack = "Stack" >>> stackoverflow = stack + "Overflow" >>> id(stack) 140128069348184 >>> id(stackoverflow) 140128123911480
В этом случае ясно, что если мы хотим сохранить первую строку, нам нужна копия. Но так ли это очевидно для других типов?
Упражнение
Теперь, зная, как работают неизменяемые типы, что бы вы сказали с приведенным ниже фрагментом кода? Это мудро?
s = "" for i in range(1, 1000): s += str(i) s +=","
Mutables
Объект изменяемом типа может быть изменен, и он изменяется на месте. Неявные копии не делаются.
В эту категорию входят: списки, словари, байтовые массивы и наборы.
Давайте продолжать играть с нашим маленьким 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!')
В самом деле. 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.
Изменчивый и неизменный как аргументы
Одним из основных вариантов использования, когда разработчику необходимо учитывать изменчивость, является передача аргументов функции. Это очень важно, потому что это будет определять способность функции изменять объекты, которые не принадлежат ее области действия, или, другими словами, если функция имеет побочные эффекты. Это также важно для понимания того, где должен быть доступен результат функции.
>>> 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 и 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 ? Что происходит, если функция вызывается снова с теми же параметрами?
Изменяемые и неизменяемые объекты в Python
Все в Python – это объект. Каждый новичок должен сразу усвоить, что все объекты в Python могут быть либо изменяемыми (мутабельным), либо неизменяемыми (иммутабельным).
Давайте углубимся в детали. Поскольку все в Python является объектом, то каждая переменная является экземпляром какого-то класса (объектом). Когда объект инстанцируется, ему присваивается уникальный id. Тип объекта определяется во время выполнения и после этого не меняется, однако состояние может меняться, если оно изменяемое. Проще говоря, изменяемый объект можно изменить после его создания, а неизменяемый – нет.
Объекты встроенных типов, таких как int, float, bool, str, tuple, unicode, являются неизменяемыми. Объекты встроенных типов, таких как list, set, dict, являются изменяемыми. Самописные классы, как правило, изменяемые. Если есть потребность сымитировать неизменяемость, нужно переопределить методы установки и удаления атрибутов класса так, чтобы они вызывали исключения.
Возникает вопрос, как узнать, что наша переменная изменяемый или неизменяемый объект. Для этого нам нужно понять, зачем нужны функции id и type.
Встроенная функция id() возвращается id объекта в виде целого числа. Число обычно коррелирует с местоположением объекта в памяти, но не всегда, поскольку все зависит от реализации Python и используемой платформы. Оператор is сравнивает id двух объектов.
Встроенная функция type() возвращает тип объекта. Давайте обратимся к примеру.
''' Example 1 ''' >>> x = "Holberton" >>> y = "Holberton" >>> id(x) 140135852055856 >>> id(y) 140135852055856 >>> print(x is y) '''comparing the types''' True ''' Example 2 ''' >>> a = 50 >>> type(a) >>> b = "Holberton" >>> type(b)
Теперь мы знаем, как сравнить две простые строковые переменные, чтобы определить их тип и id. С помощью этих двух функций мы можем узнать типы объектов связанных с переменными и их изменяемость/неизменяемость.
Изменяемые и неизменяемые объекты
Итак, как мы выяснили, изменяемый объект может изменять свое состояние или содержимое, а неизменяемый – нет.
Изменяемые объекты:
list, dict, set, byte array
Неизменяемые объекты:
int, float, complex, string, tuple, frozenset (неизменяемая версия set), bytes
Пример определения изменяемости объекта:
Мы создаем объект типа int. Id x и y указывают на один и тот же объект:
А теперь просто прибавим единицу:
Объект переменной x изменился. Объект 10 не может изменяться. Неизменяемые объекты не допускают изменений после создания.
Мы создаем объект типа list. Id переменных m и n привязаны к одному списку, в котором лежит коллекция из трех неизменяемых объектов типа int.
Удаление элемента из объекта списка изменяет сам объект:
Но id объекта не изменяется
Переменные m и n будут указывать на один и тот же объект списка после изменения. В объекте списка теперь лежит [1,2].
Итак, что же мы поняли из примеров выше?
- Python обрабатывает изменяемые и неизменяемые объекты по-разному.
- Доступ к неизменяемым объектам осуществляется быстрее, чем к изменяемым.
- Изменяемые объекты отлично подойдут, если вам нужно менять размер объектов, например, list, dict и т.д.
- Неизменяемые значения используются, когда вам нужно убедиться, что созданный вами объект никогда не будет меняться.
- Неизменяемые объект принципиально дорого менять, поскольку для этого требуется создать копию, а изменяемые менять легко.
Исключения в неизменяемости
Не все неизменяемые объекты на самом деле являются неизменяемыми. Запутались? Позвольте мне объяснить.
Как мы говорили ранее, такие коллекции в Python, как кортежи, неизменяемы. То есть значение кортежа нельзя менять после его создания. Но значение кортежа на самом деле – это последовательность имен с неизменяемыми привязками к объектам. Главное, что нужно понимать, так это то, что сами «привязки» менять нельзя, а вот объекты на их концах – можно.
Рассмотрим кортеж t = (‘holberton’, [1, 2, 3])
Он содержит элементы разных типов данных, первый из которых – неизменяемая строка, а второй – изменяемый список. Сам со себе кортеж неизменяемый, то есть для него нет методов изменения содержимого. Строка также неизменяемая по той же причине. Но у списка есть такие методы, поэтому его можно изменить. Здесь тонкий момент, но не менее важный: «значение» неизменяемого объекта не может измениться, а вот его элементы могут.
Как объекты передаются в функции
Для нас важно знать разницу между изменяемыми и неизменяемыми типами, чтобы понимать, как они обрабатываются при передаче в функции. Эффективность использования памяти сильно зависит от выбора соответствующих объектов.
Например, если изменяемый объект вызывается по ссылке в функции, он может изменить исходную переменную. Следовательно, чтобы избежать этого, исходную переменную надо скопировать в другую переменную. Неизменяемые объекты можно передавать по ссылке, потому что их значение в любом случае не может меняться.
def updateList(list1): list1 += [10] n = [5, 6] print(id(n)) # 140312184155336 updateList(n) print(n) # [5, 6, 10] print(id(n)) # 140312184155336
Как видно из примера выше, мы получили доступ к списку по ссылке, поэтому смогли изменить исходный список.
Давайте посмотрим на другой пример:
def updateNumber(n): print(id(n)) n += 10 b = 5 print(id(b)) # 10055680 updateNumber(b) # 10055680 print(b) # 5
В примере выше в функцию передается один и тот же объект, но значение переменных не меняется даже если их id одинаковые. Так работает передача по значению. Что же здесь происходит? Когда функция вызывает значение, передается только значение переменной, а не сам объект. Таким образом, переменная, ссылающаяся на объект, не изменяется, а сам объект изменяется, но только в пределах области видимости функции. Следовательно, изменение не видно «снаружи».
В завершение приглашаем всех на бесплатный урок, где поговорим про организацию рабочего места:
- что такое IDE и какие IDE существуют;
- как настроить самую полнофункциональную IDE для Python — PyCharm Community Edition;
- версии Python, почему их так много;
- как разрабатывать несколько проектов на одной машине (что нужно учитывать, зачем нужны виртуальные среды (venv) для проектов) и что это такое;
- как запустить приложение в Docker-контейнере (зачем это делать, как это можно сделать, что нужно учесть).
В результате вы сможете организовать изолированную среду для разработки и запуска приложения с помощью venv и docker, узнаете почему существуют различные версии Python, и как организовать свое рабочее место, чтобы разрабатывать несколько различных проектов, использующих разные версии Python и сторонних пакетов, одновременно, и без боли.