Динамическая типизация
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
имена;у имени нет типа;
имя всегда ссылается на какой-то объект (не бывает неинициализированных имён), внутри которого кроме полезных данных хранится служебная информация о типе объекта и т.п.;
оператор присвоения “
=
” связывает имя слева от него с объектом с права от него: если такого имени ещё нет, то оно создаётся, если такое имя уже существует (связанно с каким-то объектом), то ссылка перекидывается на новый объект.