Кружок по программированию, 2017-2018
В питоне всё, что можно положить в переменную, является объектом. А сама переменная является ссылкой на этот объект.
a = 1
a = 2
b = a
Объекты «живут» в своём мире независимо от переменных, а переменные — один из способов обратиться извне к миру объектов. Так в примере выше объект Целое-число-1 продолжает жить даже после того, как мы поменяли переменную a .
Объекты в питоне бывают двух значительно отличающихся сортов: изменяемые (mutable) и неизменяемые (immutable). Неизменяемыми являются целые и действительные числа ( int , float ), строки ( str ), последовательности байтов (бинарные данные, bytes ), а также кортежи, все элементы которых неизменяемы ( tuple ). Напротив, списки ( list ), словари ( dict ) и множества ( set ) являются изменяемыми. Что всё это значит? Неизменяемые объекты обладают замечательным свойством: они не могут измениться в результате работы программы. Если некоторая переменная «смотрит» на неизменяемый объект, то можно быть уверенным, что её значение не поменяется, если только ей не присвоить ссылку на другой объект. Например, в результате выполнения кода a = 1 в переменной a хранится ссылка на объект Целое-число-1, и что бы не происходило с другими переменными, a будет всегда равно 1.
Изменяемые объекты не обладают таким постоянством. Они скорее напоминают контейнер для хранения: контейнер остаётся на месте, а вот содержимое может сильно измениться. Что всё это для нас значит? Ну, например, если передать список в «чужую» функцию, то его могут испортить до неузнаваемости:
def list_destroyer(l): l[0] = 'ha' l[1] = 'hi' l[2] = 'ho' a = [1, 2, 3] print(a) # -> [1, 2, 3] list_destroyer(a) print(a) # -> ['ha', 'hi', 'ho']
Здесь можно поиграться с этим примером и посмотреть на то, что происходит с ссылками:
Обратите внимание, при передаче списка в функцию можно модифицировать элементы списка при помощи присваиваний A[i] = . , методов pop , append , insert и т.д. Это не является модификацией списка, как целого, т.е. имя A по-прежнему указывает на тот же список, при этом содержимое списка меняется.
Не всегда такое поведение является ожидаемым:
a = [1, 2, 3] b = a b.insert(0, 'ops') b.pop() print(b) # -> ['ops', 1, 2] print(a) # -> ['ops', 1, 2]
Присваивание b = a не создает новый список, а всего лишь делает b ссылкой на уже существующий список. Для того, чтобы «сохранить» старый список, нужно создавать его копию:
a = [1, 2, 3] b = a.copy() b.insert(0, 'ops') b.pop() print(b) # -> ['ops', 1, 2] print(a) # -> [1, 2, 3]
Изменяемые и неизменяемые типы данных в Python
Python считается одним из самых удивительных языков программирования. Многие люди выбирают его в качестве первого языка из-за его элегантности и простоты. Благодаря широкому сообществу, избытку пакетов и согласованности синтаксиса, опытные профессионалы также используют Python. Тем не менее, существует одна вещь, которая раздражает как новичков, так и некоторых профессиональных разработчиков – объекты Python.
Изменяемые и неизменяемые объекты
Как известно, объектом в Python является абсолютно все, а каждый объект относится к какому-либо типу данных. Типы данных бывают изменяемые и неизменяемые (англ. mutable и immutable). К неизменяемым относятся целые числа (int), числа с плавающей запятой (float), булевы значения (bool), строки (str), кортежи (tuple). К изменяемым — списки (list), множества (set), байтовые массивы (byte arrays) и словари (dict).
Функции id() и type()
Разобраться с изменяемостью типов данных нам помогут встроенные функции и операторы Python.
Встроенный метод id() возвращает идентификатор объекта в виде целого числа. Это целое число обычно относится к месту хранения объекта в памяти. Встроенная функция type() возвращает тип объекта.
Что касается операторов, для определения идентичности переменных мы можем использовать == , а is используется для определения того, указывают ли переменные на один и тот же объект. Почитать подробнее об отличиях этих операторов можно здесь.
Если сравнить две переменные, x и y , имеющие одинаковое значение, при помощи оператора равенства ( x == y ), он выдаст True. Но если мы при помощи того же оператора сравним идентификаторы объектов x и y (полученные с использованием функции id() ) – мы получим False. Дело в том, что во втором случае мы сравнивали адреса памяти переменных, а они разные – расположены в разных местах. Хотя значения, которые содержат эти переменные, одинаковы.
x = 'Привет!' y = 'Привет!' x == y # Получим True id(x) == id(y) # Получим False
Создадим переменную z путем присвоения ей в качестве значения переменной x . Используя оператор is , мы обнаружим, что обе переменные указывают на один и тот же объект и, соответственно, имеют одинаковые идентификаторы.
x = 'Привет!' z = x x is z # Получим True id(x) == id(z) # Получим True
Неизменяемые типы данных
Давайте рассмотрим некоторые неизменяемые типы.
Целые числа (int)
Давайте определим переменную x , имеющую значение 10. Встроенный метод id() используется для определения местоположения x в памяти, а type() используется для определения типа переменной. Когда мы пытаемся изменить значение x , оно успешно изменяется.
Стоит заметить, что адрес памяти тоже изменяется. Так происходит потому, что фактически мы не изменили значение x , а создали другой объект с тем же именем x и присвоили ему другое значение. Мы связали имя x с новым значением. Теперь, когда вы вызываете x , он будет выводить новое значение и ссылаться на новое местоположение.
x = 10 print(x, type(x), id(x)) # Получим (10, int, 140727505991744)
x = 12 print(x, type(x), id(x)) # Получим (12, int, 140727505991808)
Строки (str)
То же самое верно и для строкового типа данных. Мы не можем изменить существующую переменную, вместо этого мы должны создать новую с тем же именем.
В данном примере мы определили строковую переменную x , но допустили ошибку в слове и теперь хотим исправить «ю» на «и». Однако мы получаем TypeError. Это показывает, что строковые объекты не подлежат обновлению.
x = 'Прювет!' x[2] = 'и' # Получим TypeError: 'str' object does not support item assignment
Кортежи (tuple)
Давайте разберем кортежи. Мы определили кортеж с 4 значениями. Воспользуемся функцией id() для вывода его адреса. Если мы захотим изменить значение первого элемента, то получим ошибку TypeError. Это означает, что кортеж не поддерживает присвоение или обновление элементов.
tuple1 = (1, 2, 3, 4) print(tuple1, id(tuple1)) # Получим (1, 2, 3, 4) 3240603720336
С другой стороны, мы можем обновить весь кортеж, задав его с нуля. После этого мы увидим новые значения элементов кортежа и новый адрес.
tuple1 = (5, 6, 7, 8) print(tuple1, id(tuple1)) # Получим (5, 6, 7, 8) 3240603720256
Числа с плавающей запятой (float)
У нас есть переменная x типа float. Используя функцию id() , мы можем узнать ее адрес. Если мы попробуем заменить элемент с индексом 1, то получим TypeError. Как и в предыдущих примерах видим, что float не поддерживает модификацию элемента.
x = 3.456 x[1] = 4 # Получим TypeError: 'float' object does not support item assignment
Если же мы обновим float, переопределив его, то при вызове получим новое значение и новый адрес.
x = 3.654 type(x), id(x) # Получим (float, 3240603166960)
Изменяемые типы данных
Теперь давайте рассмотрим некоторые изменяемые типы.
Списки (list)
Определим список с именем x и добавим в него некоторые значения. После этого обновим список: присвоим новое значение элементу с индексом 1. Можем заметить, что операция успешно выполнилась.
x = ['Яблоко', 'Груша', 'Слива'] x[1] = 'Ананас' x # выведет ['Яблоко', 'Ананас', 'Слива']
Вышеописанные действия являются простым и базовым примером модификации. Чтобы проверить изменчивость на более глубоком уровне, давайте рассмотрим тот же пример с небольшими изменениями.
Создадим новое имя y и свяжем его с тем же объектом списка. А теперь проверим, совпадает ли x с y . Нам вернется True. Кроме того, x и y имеют одинаковые адреса памяти.
x = y = ['Яблоко', 'Груша', 'Слива'] x is y # True id(x), id(y), id(x) == id(y) # (3240602862208, 3240602862208, True)
Теперь добавим новое значение к списку x и проверим обновленный вывод.
x.append('Персик') x # ['Яблоко', 'Груша', 'Слива', 'Персик']
Если мы теперь вызовем y , то получим тот же список, что и при вызове x . Хотя непосредственно в y мы ничего не добавляли. Это означает, что по сути мы обновляем один список объектов, у которого есть два разных имени: x и y . Оба они одинаковы и имеют один адрес в памяти даже после модификации.
y # ['Яблоко', 'Груша', 'Слива', 'Персик'] x is y, id(x) == id(y) # (True, True)
Словари (dict)
Словари — часто используемый тип данных в Python. Давайте посмотрим на их изменчивость.
Определим словарь под именем dict с тремя ключами и их значениями. Когда мы распечатаем его, отобразится все его содержимое. Можно распечатать каждое значение словаря отдельно, а также использовать ключи вместо индексов. Подробнее о добавлении элементов в словарь вы можете узнать тут.
dict = dict # Получим dict['Name'], dict['Age'], dict['Job'] # ('Алиса', 27, 'Senior Python Developer')
Давайте изменим какое-нибудь значение в нашем словаре. Например, обновим значение для ключа Name . Выведем обновленный словарь. Значение изменилось. При этом сами ключи словаря неизменяемы.
dict[‘Name’] = ‘Роберт’ dict #
Списки и кортежи: наглядный пример изменяемых и неизменяемых объектов
Давайте по отдельности определим список и кортеж. Убедимся, что в кортеже есть значение типа список, а в списке есть значение типа кортеж.
tuple1 = ([1, 1], 2, 3) list1 = [(1, 1), 2, 3] tuple1, list1 # (([1, 1], 2, 3), [(1, 1), 2, 3])
Нулевой элемент кортежа – список. Давайте попробуем изменить какой-то из элементов списка, указав его индекс. Например, можно поменять в нулевом элементе кортежа (т.е. в списке) нулевой элемент. Нам успешно удается это сделать, потому что список – изменяемый объект, даже если он находится в кортеже.
tuple1[0][0] = 'Поменялся' tuple1 # (['Поменялся', 1], 2, 3)
Если же, наоборот, кортеж находится в списке, то вы не сможете поменять элемент этого кортежа, хотя он и находится в изменяемом списке. Ведь сам кортеж неизменяем. Поэтому такие преобразования невозможны.
list1[0][0] = 'Поменялся' list1 # Получим TypeError: 'tuple' object does not support item assignment
Заключение
Мы разобрали различия между изменяемым и неизменяемым объектами в Python. Стоит понимать, что всё в Python называется объектами. И главное различие между ними – являются они изменяемыми или неизменяемыми.
Изменяемые и неизменяемые объекты в Python
Все в Python – это объект. Каждый новичок должен сразу усвоить, что все объекты в Python могут быть либо изменяемыми (мутабельным), либо неизменяемыми (иммутабельным).
Давайте углубимся в детали. Поскольку все в Python является объектом, то каждая переменная является экземпляром какого-то класса (объектом). Когда объект инстанцируется, ему присваивается уникальный id. Тип объекта определяется во время выполнения и после этого не меняется, однако состояние может меняться, если оно изменяемое. Проще говоря, изменяемый объект можно изменить после его создания, а неизменяемый – нет.
Объекты встроенных типов, таких как int, float, bool, str, tuple, unicode, являются неизменяемыми. Объекты встроенных типов, таких как list, set, dict, являются изменяемыми. Самописные классы, как правило, изменяемые. Если есть потребность сымитировать неизменяемость, нужно переопределить методы установки и удаления атрибутов класса так, чтобы они вызывали исключения.

Возникает вопрос, как узнать, что наша переменная изменяемый или неизменяемый объект. Для этого нам нужно понять, зачем нужны функции id и type.
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
Пример определения изменяемости объекта:
x = 10 x = y
Мы создаем объект типа int. Id x и y указывают на один и тот же объект:
id(x) == id(y) id(y) == id(10)
А теперь просто прибавим единицу:
x = x + 1
id(x) != id(y) id(x) != id(10)
Объект переменной x изменился. Объект 10 не может изменяться. Неизменяемые объекты не допускают изменений после создания.
А если объект изменяемый:
m = list([1, 2, 3]) n = m
Мы создаем объект типа list. Id переменных m и n привязаны к одному списку, в котором лежит коллекция из трех неизменяемых объектов типа int.
id(m) == id(n)
Удаление элемента из объекта списка изменяет сам объект:
m.pop()
Но id объекта не изменяется
id(m) == id(n)
Переменные 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 и сторонних пакетов, одновременно, и без боли.
Неизменяемые типы данных
В этой лекции мы начнем знакомиться с основными типами данных в Python. О данных хорошо написано в книге «How to Design Programs»:
Every programming language comes with a language of data and a language of operations on data. The first language always provides some forms of atomic data; to represent the variety of information in the real world as data, a programmer must learn to compose basic data and to describe such compositions. Similarly, the second language provides some basic operations on atomic data; it is the programmer’s task to compose these operations into programs that perform the desired computations.
Типы данных можно разделить на изменяемые (mutable), то есть, значение которых можно изменить после создания, и неизменяемые (immutable), соответственно, значение которых нельзя изменить после создания. Эта лекция посвящена неизменяемым типам данных.
Все является объектом
В Python все является объектом («Everything is an Object»): числа, последовательности, функции, классы, модули и т.д. Каждый объект обладает уникальным идентификатором, который никогда не изменяется после создания объекта (в CPython идентификатором объекта является его адрес в памяти, который можно получить с помощью встроенной функции id() ), типом, который определяет «чем является объект» (числом, строкой, списком и т.д.) и какие действия над ним можно выполнять, а также значением.
Каждый объект «наследуется» от Си-структуры PyObject или PyVarObject для объектов переменной (variable) длинны (списки, кортежи и т.д.):
typedef struct _object _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type; > PyObject;
- _PyObject_HEAD_EXTRA — макрос, который определяет два поля _ob_next и _ob_prev — указатели на следующий и предыдущий объекты, соответственно. Будут ли эти поля включены в структуру PyObject или нет — зависит от флага Py_TRACE_REFS , который по умолчанию не установлен;
- ob_refcnt — счетчик ссылок на объект, который увеличивается или уменьшается, при копировании или удалении указателя на объект; когда счетчик ссылок достигает нуля, то объект удаляется. Про подсчет ссылок и «сборку мусора» мы будем говорить в одной из следующих лекций;
- ob_type — указатель на структуру _typeobject , которая задает тип объекта.
Структура PyVarObject включает одно дополнительное поле ob_size — количество элементов в объекте (например, для списка из пяти элементов ob_size будет равен 5):
typedef struct PyObject ob_base; Py_ssize_t ob_size; /* Number of items in variable part */ > PyVarObject;
Связи между соответствующими структурами показаны на следующем рисунке:
Итак, если вы решили ввести свой тип, то он должен «наследоваться» от PyObject или PyVarObject с помощью макросов PyObject_HEAD и PyObject_VAR_HEAD :
#define PyObject_HEAD PyObject ob_base; . #define PyObject_VAR_HEAD PyVarObject ob_base;
typedef struct _myobject PyObject_HEAD . > PyMyObject;
Таким образом, PyMyObject будет содержать все поля, которые есть в PyObject .
Следует помнить, что макрос PyObject_HEAD должнен идти первым в структуре. Это связано с «наследованием», о котором говорилось ранее. Как утверждается в object.h :
Objects are always accessed through pointers of the type PyObject * . The type PyObject is a structure that only contains the reference count and the type pointer. The actual memory allocated for an object contains other data that can only be accessed after casting the pointer to a pointer to a longer structure type. This longer type must start with the reference count and type fields; the macro PyObject_HEAD should be used for this (to accommodate for future changes). The implementation of a particular object type can cast the object pointer to the proper type and back.
и означает, что должна быть возможность приведения (casting) указателя на PyMyObject к указателю на PyObject , то есть:
PyObject *obj = (PyObject*)my_py_type_variable;
Итак, PyObject и PyVarObject являются наиболее общими структурами для представления объектов в CPython, но пока мы не говорили о том как создаются новые объекты. В одной из последующих лекций мы вернемся к этому вопросу.
Целочисленный тип данных и числа с плавающей точкой
Без использования стандартной библиотеки языка нам доступны целые числа ( int ), вещественные числа ( float ) и комплексные числа ( complex ):
>>> year = 2021 >>> year 2021 >>> type(year) int >>> type(2021) int
Процесс создания новой переменной называется name binding, то есть, связывание имени с некоторым объектом, в данном случае именем выступает year , а объектом целое число 2021 .
У каждого типа обычно есть «конструктор»:
>>> zero = int() >>> zero 0 >>> zero = float() >>> zero 0.0
Основные арифметические операции:
>>> year + 1 2022 >>> year - 1 2020 >>> year * 12 24252 >>> year * 365.25 738170.25 >>> year / 100 20.21
Взятие целой части и остатка от деления:
>>> year // 100 20 >>> year % 100 21
Для записи очень больших или очень маленьких чисел удобно использовать экспоненциальную форму записи чисел. Сравните:
>>> 2.021 * 10**3 2021.0 >>> 2.021E3 2021.0
И не стоит забывать про ошибки округления при работе с вещественными числами:
>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 0.9999999999999999
Длинная арифметика в Python
Может ли произойти переполнение при работе с целыми числами в Python? Нет, если мы не говорим о таких пакетах как Numpy и Pandas, так как при работе с целыми числами в Python используется длинная арифметика 1 .
Структура PyLongObject отвечает за представление целых чисел:
struct _longobject PyObject_VAR_HEAD digit ob_digit[1]; > PyLongObject;
Если «раскрыть» макрос PyObject_VAR_HEAD , то стурктура будет выглядеть следующим образом:
struct _longobject ssize_t ob_refcnt; struct _typeobject *ob_type; ssize_t ob_size; uint32_t ob_digit[1]; > PyLongObject;
Связи между соответствующими структурами показаны на следующем рисунке:
Вы должны были заметить, что PyLongObject «наследуется» от PyVarObject , то есть является объектом переменной длины, и, таким образом, включает поле ob_size , которое в данном случае содержит размер массива ob_digit .
Все поля вам уже должны быть знакомы, PyLongObject добавляет лишь одно новое поле ob_digit — массив беззнаковых целых чисел по основанию 2^ . Давайте разберемся с назначением этого поля.
Представление произвольно больших целых чисел
Как хранить произвольно большое целое число? Одним из решений является представление целого числа в виде массива отдельных цифр. Для наиболее эффективного использования памяти мы можем конвертировать наше число из десятичной системы счисления в систему счисления по основанию 2^ , в таком случае каждый элемент представлен «цифрой» в диапазоне от 0 до 2^ — 1 . В зависимости от платформы Python использует или 32-битные беззнаковые массивы с 30-битными цифрами или 16-битные беззнаковые массивы с 15-битными цифрами. Такой подход представления больших целых чисел связан с дополнительными ограничениями, которые и не позволяют использовать все биты. Поле ob_digit структуры показанной выше, содержит такие массивы цифр.
Для избежания лишних вычислений в CPython есть эффективный способ представления целых чисел в диапазоне от -2^ до 2^ . Такие целые числа хранятся как массивы с одним элементом, то есть, состоящие из одной цифры.
Также следует отметить, что в отличие от классического представления знака числа (т.е. использования знакового бита), знак целого числа хранится в поле ob_size , которое также содержит размер массива ob_digit . Например, если мы хотим изменить знак целого с размером ob_size=2 (две цифры), то ob_size станет равным -2 .
Комментарий из исходных текстов по представлению целых чисел:
/* Long integer representation. The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) Negative numbers are represented with ob_size < 0; zero is represented by ob_size == 0. In a normalized number, ob_digit[abs(ob_size)-1] (the most significant digit) is never zero. Also, in all cases, for all valid i, 0 The allocation function takes care of allocating extra memory so that ob_digit[0] . ob_digit[abs(ob_size)-1] are actually available. CAUTION: Generic code manipulating subtypes of PyVarObject has to aware that integers abuse ob_size's sign bit. */
Давайте рассмотрим конкретный пример преобразования длинного целого в массив и обратно. Пусть у нас имеется следующее число: 123456789101112131415 . Переведем его в систему счисления по основанию 2^ , путем последовательного деления и записи остатка от деления:
Конвертировать число обратно также достаточно просто:
(437976919 ∗ 2^<30 ∗ 0>) + (87719511 ∗ 2^<30 ∗ 1>) + (107 ∗ 2^<30 ∗ 2>) = 12345678910111213141530>
Преобразования длинного целого в массив
Ниже приведен упрощенный вариант алгоритма представления произвольно больших чисел:
SHIFT = 30 # Число бит под каждую «цифру» MASK = (2 ** SHIFT) def split_number(bignum): t = abs(bignum) num_list = [] while t != 0: # Взятие остатка от деления small_int = t % MASK # Побитовый аналог: (t & (MASK-1)) num_list.append(small_int) # Взятие целой части от деления t = t // MASK # Побитовый аналог: t >>= SHIFT return num_list def restore_number(num_list): bignum = 0 for i, n in enumerate(num_list): bignum += n * (2 ** (SHIFT * i)) return bignum
>>> bignum = 123456789101112131415 >>> num_list = split_number(bignum) >>> num_list [437976919, 87719511, 107] >>> bignum == restore_number(num_list) True
Если мы хотим убедиться, что нигде не ошиблись, то можем посмотреть на внутреннее представление целого числа с помощью модуля ctypes, который позволяет взаимодействовать с Си-кодом из Python:
import ctypes class PyLongObject(ctypes.Structure): _fields_ = [("ob_refcnt", ctypes.c_ssize_t), ("ob_type", ctypes.c_void_p), ("ob_size", ctypes.c_ssize_t), ("ob_digit", ctypes.c_uint * 3)]
>>> bignum = 123456789101112131415 >>> for i,d in enumerate(PyLongObject.from_address(id(bignum)).ob_digit): . print(f"ob_digit[] = ") ob_digit[0] = 437976919 ob_digit[1] = 87719511 ob_digit[2] = 107 >>> print("ob_size:", PyLongObject.from_address(id(bignum)).ob_size) ob_size: 3
Оптимизации
Небольшие целые числа в диапазоне от -5 до 256 преаллоцируются в процессе инициализации интерпретатора. Так как целые числа являются неизменяемыми, то мы можем воспринимать их как синглтоны. Каждый раз, когда нам необходимо создать небольшое целое число (например, как результат некоторой арифметической операции), то вместо создания нового объекта, Python просто возвращает указатель на уже преаллоцированный объект. Это позволяет сократить количество потребляемой памяти и время затрачиваемое на вычисления при работе с небольшими целыми числами.
Давайте рассмотрим простой пример:
>>> a = 2 >>> id(a) 94220163919104 >>> a = a + 1 >>> id(a) 94220163919136 >>> b = 2 >>> id(b) 94220163919104
Следует иметь ввиду, что структура PyLongObject занимает не менее 28 байт для каждого целого числа, то есть в три раза больше чем требуется под 64-битное целое в языке C.
>>> import sys >>> sys.getsizeof(1) 28
Из чего складывается такой размер? Указатель на структуру _typeobject занимает восемь байт, также по восемь байт занимают поля ob_refcnt и ob_size , что уже в сумме дает нам 24 байта. Каждый элемент массива ob_digit это еще четыре байта. Итого для небольших целых чисел требуется 28 байт. Но есть одно исключение — ноль:
>>> import sys >>> sys.getsizeof(0) 24
Выполнение арифметических операций
Базовые арифметические операции выполняются аналогично тому, как мы это делали когда-то в школе, с одним исключением: каждый элемент массива считается «цифрой».
Давайте рассмотрим вариант алгоритма сложения с переносом:
def add_bignum(a, b): z = [] if len(a) len(b): # Убедимся, что в «a» наибольшее из двух значений a, b = b, a carry = 0 for i in range(0, len(b)): carry += a[i] + b[i] z.append(carry % MASK) carry = carry // MASK for i in range(i + 1, len(a)): carry += a[i] z.append(carry % MASK) carry = carry // MASK z.append(carry) # Удалим завершающие нули i = len(z) while i > 0 and z[i-1] == 0: i -= 1 z = z[0:i] return z
>>> a = 8223372036854775807 >>> b = 100037203685477 >>> restore_number(add_bignum(split_number(a), split_number(b))) == a + b True
Замечание про Numpy и Pandas
В тех случаях, когда мы пользуемся библиотеками numpy/scipy/pandas и т.д., может произойти переполнение при работе с целыми числами, так как структуры, лежащие в основе этих библиотек, для более эффективного использования памяти, полагаются на соответствующие С-типы ограниченной точности 2 :
>>> import numpy as np >>> ar = np.array([2**63 - 1, 2**63 - 1]) >>> ar array([9223372036854775807, 9223372036854775807]) >>> ar.dtype dtype('int64')
Элементами ndarray являются 64-битные знаковые целые, таким обрзаом, 2^-1 наибольшее положительное значение, которое мы можем хранить в ndarray . Добавление 1 приведет к переполнению (overflow):
>>> ar + 1 array([-9223372036854775808, -9223372036854775808]) >>> np.sum(ar) -2
При вычислении среднего элементы массива сначала приводятся к типу float и переполнения не возникает:
>>> np.mean(ar) 9.2233720368547758e+18
Числа с плавающей точкой и стандарт IEEE-754
Вещественные числа в CPython представлены структурой PyFloatObject :
typedef struct PyObject_HEAD double ob_fval; > PyFloatObject;
Легко заметить, что поле ob_fval это обычное вещественное число двойной точности. Все арифметические операции над вещественными числами в Python являются простыми обертками над соответствующими арифметическими операциями в Си, например, операция сложения определена следующим образом:
static PyObject * float_add(PyObject *v, PyObject *w) double a,b; CONVERT_TO_DOUBLE(v, a); CONVERT_TO_DOUBLE(w, b); PyFPE_START_PROTECT("add", return 0) a = a + b; PyFPE_END_PROTECT(a) return PyFloat_FromDouble(a); >
Следует помнить, что все вычисления в вещественных числах делаются компьютером с некоторой ограниченной точностью (см. стандарт IEEE-754), поэтому зачастую вместо «честных» ответов получаются приближенные (к этому надо быть готовым), например:
>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 0.9999999999999999
Если вы не понимаете почему мы не получили единицу, то попробуйте перевести число 0.1 в двоичную систему счисления:
0.1 = \frac<1> = 0*2^ + 0*2^ + 0*2^ + 1*2^ + 1*2^ + . = 00011(0011)1>
В некоторых случаях на помощь может придти модуль fmath :
>>> from math import fsum >>> sum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) 0.9999999999999999 >>> fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) 1.0
Булевый тип
>>> to_be = True >>> to_be or not to_be True
>>> is_leap = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0) >>> is_leap False
>>> True or abrakadabra_or_lazy_evaluation True
>>> isinstance(True, bool) and isinstance(True, int) True
Строки
Строки в Python версии 3 представляют собой последовательность символов Юникод (code point’ов). Если вы никогда не слышали про Юникод и кодировки или плохо представляете, что это такое, то советую прочитать исчерпывающую статью David C. Zentgraf из серии «Что каждый программист должен знать о. ».
Новую строку можно создать с помощью одинарных или двойных кавычек (можно использовать и тройные кавычки, но чаще они используются для документирования функций, классов, модулей):
>>> first_name = 'Dmitrii' >>> last_name = 'Sorokin' >>> email = 'Dementiy@yandex.ru'
Для строк определена операция сложения (конкантенации):
>>> full_name = first_name + ' ' + last_name >>> full_name 'Dmitrii Sorokin'
Следует иметь ввиду, что в приведенном примере, будет создан промежуточный объект, состоящий из имени и пробела. Если возникает необходимость объединить большое число строк, то вместо последовательного сложения строк следует использовтаь метод join() , который работает следующим образом — осуществляется проход по всем строкам суммируя их длины, выделяется память под новый размер строки, осуществляется еще один проход по всем строкам копируя их содержимое в новый объект, таким образом, не создается промежуточных объектов:
>>> full_name = ' '.join([first_name, last_name]) >>> full_name 'Dmitrii Sorokin'
Здесь мы первый раз сталкиваемся с вызовом метода у объекта. Каждый объект предоставляет интерфейс (методы) взаимодействия с ним, а также хранит внутреннее состояние посредством переменных. Обращение к методам и переменным объекта (атрибутам) происходит через точку, как в примере выше с методом join() .
Строку можно разбить на список подстрок по заданному разделителю, который в свою очередь также является строкой:
>>> email.split('@') ['Dementiy', 'yandex.ru'] >>> username, domain = email.split('@') >>> username 'Dementiy' >>> domain 'yandex.ru'
Заканчивается ли строка данной подстрокой:
>>> email.endswith('yandex.ru') True
У строк (как и у большинства контейнеров) можно получить длину (число элементов в контейнере):
>>> len(full_name) # --> full_name.__len__() 15
Можно обращаться к отдельным элементам строки, которые представляют собой строку из одного символа:
>>> first_name[0] 'D' >>> first_name[-1] 'i'
Строки являются неизменяемыми, то есть мы не можем изменить отдельный элемент строки:
>>> first_name[-1] = 'y' . TypeError: 'str' object does not support item assignment
И наконец мы можем брать подмножество (срез) элементов строки:
>>> email[:email.index('@')] 'Dementiy'
Мы рассмотрели лишь небольшую часть операций, которые можно выполнять над строками. Полный список операций вы всегда можете найти в документации.
Представление строк
Как уже было сказано, строки в Python являются Юникод строками. Для внтуреннего представления строк в Python, начиная с версии 3.3 (см. PEP-393), используются кодировки Latin-1 (1 байт на символ), UCS-2 (2 байта на символ) и UCS-4 (4 байта на символ). Упрощенно процесс определения кодировки следующий: когда необходимо создать строковый объект (тексты программ обычно в кодировке UTF-8), Python находит самый старший кодовый знак (code point) в строке и выбирает кодироку, в которой кодовый знак может быть представлен «как есть».
Строки представлены не одной структурой, а «иерархией» из трех структур, не считая PyObject . Мы рассмотрим одну структуру — PyASCIIObject , которая содержит большую часть информации о строке, например, какая кодировка используется для хранения строки, длину строки (число кодовых знаков), состоит ли строка только из ASCII-символов, интернирована строка или нет и т.д.
Опишем структуру PyASCIIObject с помощью модуля ctypes :
import ctypes class PyASCIIObject(ctypes.Structure): _fields_ = [("ob_refcnt", ctypes.c_ssize_t), ("ob_type", ctypes.py_object), ("length", ctypes.c_ssize_t), ("hash", ctypes.c_ssize_t), ("interned", ctypes.c_uint, 2), ("kind", ctypes.c_uint, 3), ("compact", ctypes.c_uint, 1), ("ascii", ctypes.c_uint, 1), ("ready", ctypes.c_uint, 1), ('wstr', ctypes.c_wchar_p)] def get_string_kind(string): return PyASCIIObject.from_address(id(string)).kind
Создадим несколько строковых объектов:
>>> greet = 'Hello, world' >>> greet 'Hello, world' >>> len(greet) 12 >>> sys.getsizeof(greet) 61 >>> get_string_kind(greet) 1 >>> greet = 'Hello, 世界' >>> greet 'Hello, 世界' >>> len(greet) 9 >>> sys.getsizeof(greet) 92 >>> get_string_kind(greet) 2 >>> greet = 'Hello, \U0001F30D' >>> greet 'Hello, ' >>> len(greet) 8 >>> sys.getsizeof(greet) 108 >>> get_string_kind(greet) 4
Обратите внимание, что хотя длина строк и становится меньше, но занимаемый размер наоборот растет, так как используются разные кодировки для внутреннего представления. Об этом важно помнить, особенно когда вы работаете с большими объемами текстов, так как один символ эмодзи может увеличить занимаемый размер строки в 4 раза.
Почему для внутреннего представления строк не используется UTF-8? Кодировка UTF-8 подразумевает, что может использоваться варьируемое число байт (от одного до четырех) для кодирования одного символа. UTF-8 является оптимальной кодировкой с точки зрения хранения строк (то есть кодировка эффективна по памяти), но при обращении к отдельным элементам строки (при индексировании), необходимо пройтись по всем символам строки пока мы не дойдем до нужного символа. При фиксированном размере такой проблемы не возникает, для перехода к нужной позиции символа достаточно индекс умножить на размер кодового знака (1, 2 или 4 в зависимости от используемой кодировки). Тем не менее есть мнение, что индексация это не проблема.
Интернирование строк
Дополнительно про интернирование можно почитать тут и тут.
Для экономии памяти в Python реализовано интернирование строк (string interning). Давайте рассмотрим такой пример, пусть у нас есть два строковых объекта с одинаковым содержимым:
>>> s1 = "foo!" >>> s2 = "foo!" >>> s1 is s2 False
Хотя содержимое строк совпадает это два разных объекта. С другой стороны:
>>> s1 = "a" >>> s2 = "a" >>> s1 is s2 True
получим, что адреса s1 и s2 совпадают. Все строки длиной 0 или 1 интернированы, кроме того интернируются все строковые литералы, состоящие из символов латинского алфавита, цифр или нижнего подчеркивания, также интернируются имена переменных, функций, классов и т.д.
interned = None def intern(string): global interned if string is None or not type(string) is str: raise TypeError if interned is None: interned = <> t = interned.get(string) if t is not None: return t interned[string] = string return string
Если мы хотим интернировать строку, то следует воспользоваться функцией intern из модуля sys :
>>> import sys >>> s1 = sys.intern("foo!") >>> s2 = sys.intern("foo!") >>> s1 is s2 True
Использование интернирования строк гарантирует, что не будет создано двух одинаковых строковых объектов. Когда вы создаете второй объект с тем же значением, что и у существующего объекта, то вы получаете ссылку на уже существующий объект. Таким образом, интернирование строк позволяет экономить память и повышает скорость сравнения строк, путем сравнения их адресов (хешей), а не содержимого.
Кортежи
Последний неизменяемый тип, который мы рассмотрим в этой лекции это кортежи, которые фактически являются статическими массивами, то есть, имеют фиксированный размер, и представлены структурой PyTupleObject :
typedef struct PyObject_VAR_HEAD /* ob_item contains space for 'ob_size' elements. Items must normally not be NULL, except during construction when the tuple is not yet visible outside the function that builds it. */ PyObject *ob_item[1]; > PyTupleObject;
Рассмотрим простой пример создания кортежа из трех элементов:
>>> point = (1.0, 2.0, 3.0) >>> point (1.0, 2.0, 3.0)
Как уже было отмечено, важной особенностью кортежей является то, что это неизменяемая структура:
>>> point[0] = 4 . TypeError: 'tuple' object does not support item assignment
Когда мы говорим, что кортежи неизменяемые, то имеем ввиду, что мы не можем заменить один элемент кортежа на другой, но сам объект изменить мы можем:
>>> t = (1, [2]) >>> t[1].append(3) >>> t (1, [2, 3])
- Значительная часть материала про представление целых чисел взята из статьи Артема Голубина: Python Integer Implementation. ↩
- Can integer operations overflow in Python?. ↩
