Динамическая типизация
Contents
Динамическая типизация#
Следующий код python исполняется без ошибок.
a = 0 # a - переменная типа int
print(a, type(a))
a = 0.0 # теперь a - переменная типа float
print(a, type(a))
a = "zero" # теперь a - переменная типа str
print(a, type(a))
a = [0, 0.0, "zero"] # теперь a - переменная типа list
print(a, type(a))
0 <class 'int'>
0.0 <class 'float'>
zero <class 'str'>
[0, 0.0, 'zero'] <class 'list'>
Кажется, что переменная a меняет свой тип 3 раза. Как это возможно? Прежде чем ответить на этот вопрос, освежим в памяти, как устроена типизация в C/C++, и разберемся, почему это изначально может смущать.
Note
Функция type спрашивает у объекта его тип.
Статическая типизация в C++#
Чтобы разобрать, как устроена типизация в C/C++, рассмотрим следующий код на языке C/C++.
#include<iostream>
#define FIRSTBYTE 1
#define SECONDBYTE 256
#define THIRDBYTE 256*256
#define FOURTHBYTE 256*256*256
int main(){
// 1
int a = 65*FIRSTBYTE + 66*SECONDBYTE + 67*THIRDBYTE + 68*FOURTHBYTE;
std::cout << a << std::endl; // 1145258561
// 2
char *b = reinterpret_cast<char*>(&a);
// 3
for(size_t i=0; i<4; ++i)
std::cout << b[i]; // ABCD
std::cout << std::endl;
// 4
a = 'A';
std::cout << a; // 65
return 0;
}
Если его скомпилировать и запустить, то вы должны получить следующее в стандартном потоке вывода.
1145258561
ABCD
65
Note
Приведенный выше код опирается на предположение, что в системе целочисленный тип int занимает 4 байта, что не гарантируется стандартами C++.
Разберем по шагам, что происходит в этой программе.
// 1#
В начале объявляется целочисленная переменная a типа int, которая инициализируется значением \(65 \cdot 256^0 + 66 \cdot 256^1 + 67 \cdot 256^{2} + 68 \cdot 256^{3} = 1145258561\).
// 1
int a = 65*FIRSTBYTE + 66*SECONDBYTE + 67*THIRDBYTE + 68*FOURTHBYTE;
std::cout << a << std::endl; // 1145258561
В момент объявления выделяется достаточная для хранения переменной типа int (4 байта на большинстве платформ) область оперативной памяти, и переменная a связывается с этой областью памяти. Во момент инициализации в эту область памяти записывается число 1145258561. Это число намерено задано в разложении по степеням числа \(256\), чтобы явно выделить байты этого числа: 65, 66, 67 и 68 (см. статью в википедии про порядок байтов, если непонятно о чем идёт речь).
// 2#
Далее объявляется указатель b на символьную переменную типа char, который инициализируется адресом памяти переменной a.
// 2
char *b = reinterpret_cast<char*>(&a);
Чтобы провернуть такой трюк, необходимо сначала поменять тип указателя, т.к. переменная a имеет 32-битный целочисленный тип int, а переменная b является указателем 8-битного типа char (который формально тоже является целочисленным). Компилятор запретил бы такую операцию из-за несоответствия типов (и невозможности их приведения друг к другу безопасным образом), поэтому в коде используется reinterpret_cast, который вынуждает компилятор привести тип указателя.
Warning
Такие возможности языка C/C++ приводятся здесь только с целью демонстрации механизма работы переменных в C/C++. В настоящих программах рекомендуется избегать таких приёмов.
// 3#
Далее в цикле выводятся значения b[0], b[1], b[2] и b[3].
// 3
for(size_t i=0; i<4; ++i)
std::cout << b[i];
std::cout << std::endl;
В результате видим строку ABCD. Чтобы разобраться с тем, как это произошло, необходимо учесть
b— указатель, который хранит в себе адрес переменнойa;однако
b— указатель 8-битного типа, а значит при его разыменовании (b[0], что то же самое, что и*b) захватывается область размером всего в 1 байт.b[i]эквивалентно*(b + i), т.е. то же самое, что иb[0], но наiбайт правее.т.к. в и тоге выводится символьная переменная, то она выводится согласно таблице ASCII, в которой 65 соответствует символу “
A”, 66 — символу “B”, 67 — символу “C”, 68 — символу “D”.
Следующий рисунок иллюстрирует примерное устройство памяти в этой программе на этот момент времени.
// 4#
Далее в переменную a записывается значение “'A'”;
// 4
a = 'A';
std::cout << a;
Из-за того, что C/C++ — статически типизированный язык, то компилятор знает, что a — переменная типа int (с таким типом она была объявлена), и A — значение типа char, которое может быть без потерь приведено к int (8 бит меньше 32 бит). Исходя из этой информации, компилятор допускает эту операцию, неявно приводя символьное значение 'A' к целочисленному значению 65 и записывая в соответствующую переменной a область памяти это значение.
Итого#
Итого, опуская технические подробности:
переменная соответствует области в памяти;
у переменной всегда есть определенный тип, он указывается при объявлении переменной и известен на этапе компиляции;
в самой области памяти нет информации о том, как правильно прочитать последовательность бит, которую она содержит;
при обращении к переменной, её значение получается в результате интерпретации последовательности бит в соответствующей области памяти на основании типа этой переменной;
технически возможно прочитать одну и ту же область памяти по-разному, обращаясь к ней переменными разных типов;
оператор присвоения “
=” записывает значение справа от него в область памяти, именованную переменной слева от него с предварительной проверкой совместимости типов.
Динамическая типизация в python#
В python тип хранится не в переменной, а в самом объекте, а сама переменная всего лишь ссылается на объект (в python всё объекты), ничего не подозревая о типе этого самого объекта. На самом деле переменная в CPython представляет собой обертку над указателем C типа void, который всегда автоматически разыменовывается, а оператор присвоения “=” связывает переменную слева от него с объектом справа от него (изменяет значение указателя).
Note
В связи с этим, иногда термин “переменная” (variable) избегается в отношении python и заменяется терминами “имя” (name), “ссылка” (reference) или идентификатор.
Note
В python работать с адресами памяти напрямую невозможно. Единственное исключение составляет функция id, которая в некоторых реализациях возвращает адрес памяти (целое число), где хранится объект. Эта функция нужна только для того, чтобы всегда можно было выяснить, ссылаются ли два имени на один и тот же объект. Обратится по этому адресу средствами python невозможно.
Вернемся к примеру из начала раздела и объясним, как он работает.
a = 0 # a - переменная типа int
print(a, type(a))
a = 0. # теперь a - переменная типа float
print(a, type(a))
a = "zero" # теперь a - переменная типа str
print(a, type(a))
a = [0, 0., "zero"] # теперь a - переменная типа list
print(a, type(a))
# 1#
Первая инструкция a = 0.
Сначала вычисляется значение выражения справа: создается объект целочисленного типа
intсодержащий в себе значение0.Так как имени
aдо этого объявлено не было, то такое имя создаётся и связывается с объектом справа.
a = 0 # a - переменная типа int
print(a, type(a))
0 <class 'int'>
# 2#
Вторая инструкция a = 0.0.
Создаётся объект типа
float, содержащий значение0.0.Так как имя
aуже существует, то оно связывается с новым объектом.
a = 0.0 # теперь a - переменная типа float
print(a, type(a))
0.0 <class 'float'>
# 3#
Третья инструкция a="zero".
Создаётся объект строкового типа
str, содержащий значение"zero".Существующее имя
aсвязывается с новым объектом, который имеет другой тип.
a = "zero" # теперь a - переменная типа str
print(a, type(a))
zero <class 'str'>
# 4#
Четвертая инструкция a = [0, 0.0, "zero"].
a = [0, 0.0, "zero"] # теперь a - переменная типа list
print(a, type(a))
[0, 0.0, 'zero'] <class 'list'>
Попытайтесь сами объяснить, что происходит при её выполнении, а затем сверьтесь со спойлером ниже.
Создаётся три объекта: целочисленный объект
0, действительнозначный объект0.0и строковый объект"zero".Создаётся объект спискового объект длинны 3, ячейки которого связываются с объектами, созданными на предыдущем этапе.
Существующее имя
aсвязывается связывается с новым объектом, который имеет другой тип.
Note
Иногда говорят, что тип объекта “живёт” в самом объекте, а не в “переменной”.
Итого#
Итого, опуская технические подробности:
вместо переменных в
pythonимена;у имени нет типа;
имя всегда ссылается на какой-то объект (не бывает неинициализированных имён), внутри которого кроме полезных данных хранится служебная информация о типе объекта и т.п.;
оператор присвоения “
=” связывает имя слева от него с объектом с права от него: если такого имени ещё нет, то оно создаётся, если такое имя уже существует (связанно с каким-то объектом), то ссылка перекидывается на новый объект.