Змінні, посилання та оперативна памʼять

У цьому розділі:

Динамічна типізація в 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

У цьому рядку:

  1. Створюється обʼєкт ціле число 42.
  2. У памʼяті йому виділяється місце.
  3. Змінна 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.

Обʼєкти бувають:

Посилання — асоціація імені й обʼєкта

У Python оператор присвоювання = не копіює значення, а встановлює зв’язок (посилання) між іменем і обʼєктом, який зберігається в оперативній памʼяті. Цей зв’язок створюється під час виконання програми.

Як працює оператор присвоювання? Ось його загальний вигляд:

<імʼя> = <вираз>

Розгляньмо крок за кроком:

  1. Обчислення виразу (права частина). Інтерпретатор виконує вираз, що розташований праворуч від =, і отримує обʼєкт з певним значенням.
  2. Обробка імені (ліва частина). Інтерпретатор звертається до таблиці імен (списку змінних у поточному просторі імен):

    1. якщо імʼя не знайдено, то створюється новий запис з цим імʼям, і воно починає посилатися на обʼєкт.
    2. якщо імʼя вже існує, то оновлюється посилання — воно починає вказувати на новий обʼєкт. Попереднє посилання (на старий обʼєкт) затирається. Якщо на старий обʼєкт більше немає посилань, він стає непотрібним і може бути видалений автоматично — цей процес називається збиранням сміття (garbage collection).

Розглянемо, як інтерпретатор обробляє цей оператор крок за кроком:

x = 2 + 3
  1. Обчислює праву частину виразу (2+3).
  2. Створює обʼєкт типу int зі значенням 5.
  3. Розміщує цей обʼєкт в оперативній памʼяті.
  4. Шукає імʼя x в таблиці імен, та не знаходить його.
  5. Створює новий запис в таблиці імен і воно посилається на обʼєкт 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
  1. Вираз 10 створює обʼєкт типу int зі значенням 10, який зберігається в оперативній памʼяті.
  2. Імʼя a починає посилатися на цей обʼєкт.
  3. Вираз 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