Змінні, посилання та оперативна памʼять
У цьому розділі:
Динамічна типізація в Python
У багатьох мовах програмування (наприклад, у C або Java) перед тим, як використовувати змінну, програміст має оголосити її: указати компілятору, скільки оперативної памʼяті потрібно виділити, і як інтерпретувати значення, яке буде зберігатися в цій памʼяті. Такий підхід називається статичною типізацією.
У Python — навпаки: немає потреби попередньо оголошувати змінні, інтерпретатор автоматично визначає тип даних під час виконання програми. Цей підхід має назву динамічна типізація.
Що це означає на практиці?
- можна створити змінну без вказання її типу;
- тип змінної визначається автоматично залежно від присвоєного значення;
- можна переприсвоїти цій самій змінній значення іншого типу — і це не буде помилкою.
x = 10 # x — це ціле число (int)
x = "привіт" # тепер x — це рядок (str)
x = [1, 2, 3] # а тепер — список (list)
Перший крок:
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
x
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
10
}
x -> 10
}
Другий крок:
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
x
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
v2 [label="10" color="gray" fontcolor="gray" style="dashed"]
v3 [label="\"привіт\""]
}
x -> v3
}
Третій крок:
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
x
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
"[1, 2, 3]"
v2 [label="10" color="gray" fontcolor="gray" style="dashed"]
v3 [label="\"привіт\"" color="gray" fontcolor="gray" style="dashed"]
}
x -> "[1, 2, 3]"
}
Примітка: тут і надалі сіра пунктирна лінія позначає обʼєкти в памʼяті, на які немає посилань та котрі або видалені або будуть видалені згодом.
Динамічна типізація спрощує написання коду, пришвидшує розробку та зменшує кількість формальностей. Однак, вона вимагає від програміста уважності: типи змінних можуть змінюватися непомітно, що іноді призводить до помилок, які видно лише під час виконання програми.
У цьому прикладі ми спочатку присвоїли змінній x
значення "5"
(тип str
), потім переприсвоїли значення 5
(тип int
), що при виконанні операції +
призводить до помилки TypeError: unsupported operand type(s) for +: 'int' and 'str'
— неможливо виконати операцію + для цілого та рядка:
x = "5" # x — рядок (str)
x = 5 # x — ціле (int)
print(x + " днів") # помилка при виконанні TypeError
Тут Python не зміг скласти число і рядок. Хоча типи визначаються автоматично, перетворення типів не виконується автоматично, якщо це небезпечно або неоднозначно. Програміст має сам подбати про це:
x = 5
print(str(x) + " днів") # OK: перетворили число в рядок
Змінні, імена, посилання, обʼєкти
Змінна у Python — це не комірка памʼяті, як у C. Це — іменоване посилання на обʼєкт, що існує в оперативній памʼяті.
У Python важливо розуміти, що змінна не зберігає значення безпосередньо. Змінна — це імʼя, яке вказує на обʼєкт, розміщений у памʼяті.
Приклад:
x = 42
У цьому рядку:
- Створюється обʼєкт ціле число
42
. - У памʼяті йому виділяється місце.
- Змінна
x
стає іменем, яке посилається на цей обʼєкт.
Ідентифікатор у Python — це ім'я, яке використовується для позначення змінних, функцій, класів, модулів тощо. Він має відповідати певним правилам:
- Ідентифікатор може містити лише літери
a-z
,A-Z
, цифри0-9
і знак підкреслення_
. - Він не може починатися з цифри.
- Python розрізняє великі та малі літери (наприклад,
name
іName
— різні ідентифікатори). - Деякі зарезервовані слова (наприклад,
def
,class
,if
) не можуть бути ідентифікаторами.
Приклади правильних ідентифікаторів:
user_name = "Alice"
age = 30
_total = 100
def process_data():
pass
Приклади неправильних ідентифікаторів:
2name = "Bob" # Починається з цифри
class = "student" # Використання зарезервованого слова class
user-name = "Charlie" # Містить недозволений символ "-"
У Python усе — це обʼєкт: число, рядок, список, функція, клас, модуль, навіть сам тип int.
Обʼєкти бувають:
- простими (скалярними): числа (int, float), логічні значення (bool), рядки (str);
- структурованими: списки (list) та кортежі (tuple), словники (dict), множини (set);
- функціональними: функції, методи;
- структурними: класи, обʼєкти класів;
- системними: модулі, файли, винятки.
Посилання — асоціація імені й обʼєкта
У Python оператор присвоювання =
не копіює значення, а встановлює зв’язок (посилання) між іменем і обʼєктом, який зберігається в оперативній памʼяті. Цей зв’язок створюється під час виконання програми.
Як працює оператор присвоювання? Ось його загальний вигляд:
<імʼя> = <вираз>
Розгляньмо крок за кроком:
- Обчислення виразу (права частина). Інтерпретатор виконує вираз, що розташований праворуч від
=
, і отримує обʼєкт з певним значенням. -
Обробка імені (ліва частина). Інтерпретатор звертається до таблиці імен (списку змінних у поточному просторі імен):
- якщо імʼя не знайдено, то створюється новий запис з цим імʼям, і воно починає посилатися на обʼєкт.
- якщо імʼя вже існує, то оновлюється посилання — воно починає вказувати на новий обʼєкт. Попереднє посилання (на старий обʼєкт) затирається. Якщо на старий обʼєкт більше немає посилань, він стає непотрібним і може бути видалений автоматично — цей процес називається збиранням сміття (garbage collection).
Розглянемо, як інтерпретатор обробляє цей оператор крок за кроком:
x = 2 + 3
- Обчислює праву частину виразу (
2+3
). - Створює обʼєкт типу
int
зі значенням5
. - Розміщує цей обʼєкт в оперативній памʼяті.
- Шукає імʼя
x
в таблиці імен, та не знаходить його. - Створює новий запис в таблиці імен і воно посилається на обʼєкт
5
,
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
x
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
5
}
x -> 5
}
У Python оператор =
не копіює значення, а лише копіює посилання на обʼєкт у памʼяті.
Розглянемо приклад:
a = 10
b = a
- Вираз
10
створює обʼєкт типуint
зі значенням10
, який зберігається в оперативній памʼяті. - Імʼя
a
починає посилатися на цей обʼєкт. - Вираз
b = a
не створює нове число, а лише створює нове імʼяb
, яке теж посилається на той самий обʼєкт, що йa
.
Тобто a
і b
— це два імені, які посилаються на один і той самий обʼєкт у памʼяті.
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
a
b
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
10
}
a -> 10
b -> 10
}
Операції з посиланнями
Ідентичність обʼєкта: id
Функція id(<імʼя_змінної>)
повертає ідентифікатор обʼєкта — унікальне ціле число, яке зберігається постійно протягом життя обʼєкта.
У більшості реалізацій Python (зокрема CPython) це число збігається з адресою обʼєкта в оперативній памʼяті.
Приклад:
x = 42
print(id(x)) # Наприклад, 140724960578320
Це значення дозволяє:
- перевірити, чи два імені вказують на один і той самий обʼєкт;
- лідити механізми управління памʼяттю в Python.
Порівняння ідентифікаторів обʼєктів: is
У Python оператор is
і функція id()
дозволяють перевірити, чи два імені посилаються на один і той самий обʼєкт у памʼяті.
Приклад 1: Незмінні обʼєкти — цілі числа:
a = 100
b = 100
print(a is b) # True
print(id(a) == id(b)) # True
Python зберігає в памʼяті один обʼєкт для “малих” цілих чисел (від -5 до 256), тому a
і b
посилаються на один обʼєкт.
Приклад 2: Змінні обʼєкти — списки:
a = [1, 2]
b = [1, 2]
print(a == b) # True, тому що однакові за змістом
print(a is b) # False, тому що різні посилання на різні оʼєкти
print(id(a) == id(b)) # False, тому що різні посилання на різні оʼєкти
Списки a
і b
мають однакове значення (елементи), але це два різні обʼєкти в памʼяті. Тому порівняння через is
або id()
покаже, що це не одне й те саме.
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
a
b
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
list1 [label="[1, 2]"]
list2 [label="[1, 2]"]
}
a -> list1
b -> list2
}
Приклад 3: Один обʼєкт — два імені:
x = [9, 8, 7]
y = x
print(x is y) # True
print(id(x) == id(y)) # True
Оператор y = x
не створює копію списку, а просто додає ще одне посилання на той самий обʼєкт у памʼяті.
Приклад 4: Рядки (з оптимізацією):
a = "hello"
b = "hello"
print(a is b) # True у більшості реалізацій
print(id(a) == id(b)) # True
Python оптимізує зберігання рядкових літералів, тому однакові рядки часто розміщуються в памʼяті лише один раз. Проте, це не гарантується для рядків, створених динамічно.
Приклад 5: Рядки, створені явно як різні обʼєкти:
a = ''.join(['h', 'e', 'l', 'l', 'o'])
b = 'hello'
print(a == b) # True, тому що однаковий зміст
print(a is b) # Може бути False, тому що різні обʼєкти
print(id(a) == id(b)) # Може бути False, тому що різні обʼєкти
Результат залежить від контексту виконання та реалізації інтерпретатора.
Видалення посилання: del
Оператор del
видаляє імʼя змінної з таблиці імен (простору імен). Це не обов’язково видаляє сам обʼєкт — лише якщо більше немає інших посилань на цей обʼєкт, він буде автоматично видалений збирачем сміття.
Приклад:
a = [1, 2, 3]
b = a
del a # видаляємо посилання a, але b ще існує
print(b) # [1, 2, 3]
print(a) # Помилка NameError
Стан памʼяті після видалення del a
:
digraph {
rankdir=LR
node [shape=record fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
edge [fontname="Helvetica" fontsize="12" color="#87D0F5" penwidth=2];
subgraph cluster_name {
style = filled;
color = "#f9f9f9";
label = "Імена";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
a [color="gray" fontcolor="gray" style="dashed"]
b
}
subgraph cluster_value {
style = filled;
color = "#f9f9f9";
label = "Обʼєкти";
fontname = "Helvetica"
fontsize = "10"
labeljust = "l"
list_value [label="[1, 2, 3]"];
}
b -> list_value
}
Помилка при зверненні до імені, що не існує
Якщо спробувати звернутися до імені, яке вже було видалене, або ніколи не існувало, виникає помилка NameError
:
print(a) # NameError: name 'a' is not defined