Типы данных в Python. Что нужно о них знать?
Всем привет! Меня зовут Дима. Я являюсь Backend Python Developer’ом. Хочу оставить здесь скомпонованную информацию, которой когда-то давно не хватало мне. А именно, расскажу Вам про основные типы данных в Python
, как они устроены и в чём их отличие.
Оглавление
Что за язык такой, этот Python?
Как связана динамическая типизация и куча?
Что такое тип данных?
Виды типов данных.
Как устроены неизменяемые типы данных?
Как устроены изменяемые типы данных?
Почему не стоит использовать изменяемые объекты как параметры по умолчанию?
Итог
Что за язык такой, этот Python?
Далеко не секрет, что Python
— это объектно-ориентированный язык программирования со строгой динамической типизацией.
Под »строгой» подразумевается, что язык не производит неявные преобразования типов и не создаёт проблем при их случайном смешении.
Под «динамической» подразумевается, что типы объектов определяются в процессе исполнения программы (runtime
). Поэтому типы переменных указывать не обязательно, но не сказал бы я, что это хороший тон. Переменные в Python
— это всего лишь указатели на объекты, они не содержат информации о типе.
На пункте о динамичности хочу сделать акцент (см. ниже).
Как связана динамическая типизация и куча?
Куча (Heap
) — это хранилище памяти в ОЗУ, которое допускает динамическое выделение памяти, подобие склада для переменных. При выделении в куче участка памяти для хранения переменной, к ней можно обратиться не только в потоке, но и во всём приложении (так определяются глобальные переменные). По завершении работы приложения все выделенные участки памяти освобождаются.
Размер кучи устанавливается при запуске приложения (процесса) и ограничен лишь физически, что позволяет создавать динамические переменные. Также появляется возможность определять и изменять тип переменных во время выполнения программы.
В Python
объекты и структуры данных находятся в закрытой динамической выделенной области private heap
, которая управляется менеджером памяти Python
. Он делегирует часть работы программам распределения ресурсовallocators
, закреплённым за конкретными объектами, и одновременно следит, чтобы они не выходили за пределы динамически выделяемой области.
По факту данной областью управляет интерпретатор. Пользователь никак не контролирует данный процесс, даже когда манипулирует ссылками объектов на блоки памяти внутри динамической области. Менеджер памяти Python
распределяет пространство динамической области среди объектов и другие внутренние буферы по требованию.
Теперь можно сказать о типах данных (см. ниже).
Что такое тип данных?
Тип данных — это атрибут, определяющий, какого рода данные могут храниться в объекте. Это могут быть целые числа, символы, данные денежного типа, метки времени и даты, двоичные строки и так далее.
Выделим основные (и не только) типы данных (см. ниже).
Виды типов данных
Неизменяемые (немутабельные, immutable) типы данных:
None
,bool
,int
,float
,complex
,str
,tuple
. Такжеbytes
иfrozenset
;Изменяемые (мутабельные, muttable) типы данных:
list
,dict
,set
. Также байтовый массивbytearray
;
Тип данных | Описание |
| экземпляр типа объекта |
| булевы значения ( |
| представление целых чисел, как положительных, так и отрицательных |
| числа, которые могут иметь десятичную часть (с плавающей точкой) |
| комплексные числа |
| текстовая информация (строка, последовательность символов) |
| неизменяемые упорядоченные коллекции элементов (кортежи) |
| байтовые последовательности, которые используются для работы с бинарными файлами |
| функция, которая возвращает неизменяемый объект |
| изменяемые упорядоченные коллекции элементов (списки) |
| ассоциативный массив, пары »ключ-значение», где каждый ключ является уникальным |
| неупорядоченная и неиндексированная коллекция уникальных элементов |
| массив заданных байтов |
Как устроены неизменяемые типы данных?
Рассмотрим пример с неизменяемыми типами данных и функции id
.
Функция id()
позволяет получить уникальный целочисленный идентификатор объекта (его адрес в памяти).
Проверяем, что переменные ссылаются на одну ячейку в памяти.
def test_heap_function() -> None:
a = 100
b = a
print(f"a: id({id(a)})")
print(f"b: id({id(b)})")
test_heap_function()
# a: id(140088361431424)
# b: id(140088361431424)
В случае использования стандартного оператора присваивания =
или копирования copy
происходит копирование ссылки на объект. В результате получается, что две переменные будут ссылаться на одну и туже ячейку в памяти (см. изображение 01).
01. Разные переменные с одинаковым значением ссылаются на одну ячейку в памяти;
Если мы попытаемся переменной a
присвоить другое значение (в данном случае посредствам инкрементирования значения), то после операции +=
переменная a
будет ссылаться на другой объект в памяти.
Проверяем, что переменные ссылаются на разные ячейки в памяти.
def test_heap_function() -> None:
a = 100
b = a
print(f"a: id({id(a)})")
print(f"b: id({id(b)})")
a += 1
print(f"a: id({id(a)})")
test_heap_function()
# a: id(140088361431424)
# b: id(140088361431424)
# a: id(140088361431456)
При присвоении одной из переменных другого значения переменная станет ссылаться на другую ячейку в памяти (см. изображение 02). В результате в памяти создаётся новая ячейка со значением 101
, на которую переменная a
будет ссылаться.
02. Попытка изменить значение неизменяемого объекта;
Как устроены изменяемые типы данных?
Рассмотрим пример с изменяемыми типами данных и функции id
.
def test_heap_function() -> None:
list_a = [100]
list_b = list_a
print(f"list_a: id({id(list_a)})")
print(f"list_b: id({id(list_b)})")
list_a.append(101)
print(f"list_a: id({id(list_a)})")
print(f"list_b: id({id(list_b)})")
test_heap_function()
# list_a: id(140088361431311)
# list_b: id(140088361431311)
# list_a: id(140088361431311)
# list_b: id(140088361431311)
Две переменные ссылаются на один и тот же список. Оператор присваивания =
одинаково работает как с неизменяемыми, так и с изменяемыми типами данных.
03. Разные переменные с одинаковым значением ссылаются на одну ячейку в памяти;
Суть здесь в том, что при попытке добавления элемента в изменяемый список не будет создан новый объект, а лишь произойдёт дополнение текущего. А именно, в текущем списке появится ещё одна ссылка на объект с значением 101
.
04. Изменяемые типы данных ссылаются на один и тот же объект (например, при добавлении нового элемента в список ссылка останется та же);
Получается, что в случае с изменяемым типом данных, мы будем постоянно работать с одним объектом. Объект, в таком случае, мы можем модифицировать по любой из существующих ссылок. И по любой ссылке мы всегда будем получать актуальное состояние этого объекта.
Обратите внимание!
Копирование и глубокое копирование. Здесь стоит уточнить, что при использовании copy
и deepcopy
из модуля copy
указанный выше пример работать не будет. Так как при использовании copy
создастся копия объекта со всеми его ссылками на внутренние объекты. А при использовании deepcopy
создастся новый объект со всеми вложенными ссылками независимо от объекта, с которого он был скопирован.
Тема не совсем большая, поэтому стоит её изучить как можно быстрее:0
Ещё немного про изменяемые типы данных. Их не стоит (если Вы не уверены и не знаете как это правильно работает) использовать как параметры функции по умолчанию (см. ниже).
Почему не стоит использовать изменяемые объекты как параметры по умолчанию?
В Python не рекомендуется использовать изменяемые объекты в качестве значений параметров по умолчанию по причине:
Значения по умолчанию вычисляются 1 раз при определении функции, а не при каждом вызове;
Если использовать изменяемый объект (список, словарь), то изменения в нём будут сохраняться между вызовами функции. Это может привести к неочевидному поведению и трудноуловимым ошибкам при многократном вызове функции;
В качестве альтернативы можно задавать значение по умолчанию None
, в теле функции создавать новый изменяемый объект, если значение не передано. Такое решение делает поведение программы понятным и предсказуемым.
Рассмотри, как это работает на практике.
# Плохой пример
def test_function_one(listing=[]) -> None:
listing.append(1)
print(listing)
test_function_one()
# [1]
test_function_one()
# [1, 1]
test_function_one([8, 3, 6])
# [8, 3, 6, 1]
test_function_one()
# [1, 1, 1]
# Хороший пример
def test_function_two(listing=None) -> None:
if listing is None:
listing = []
listing.append(1)
print(listing)
test_function_two()
# [1]
test_function_two()
# [1]
test_function_two([8, 3, 6])
# [8, 3, 6, 1]
test_function_two()
# [1]
На этой ноте стоит подвести итоги, так как на мой взгляд тема раскрыта в полном объёме (см. ниже).
Итог
В общем, вот всё то, что я хотел рассказать. Материал достаточно глубокий, но я постарался достаточно доходчиво и в небольшом объёме донести суть изменяемых и неизменяемых типов данных, как они устроены и как с ними работать. Спасибо.