Практика · 03 · Змінні та обʼєкти¶

Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md

У лекції ми домовились: імʼя — це ярлик, а не коробка. Тут ти перевіриш це руками.

Що зробимо:

  1. подивимось на id(), type() і на те, що присвоєння нічого не копіює;
  2. побачимо різницю між незмінним числом і змінюваним списком — по номерах обʼєктів;
  3. передбачимо результат восьми присвоєнь до запуску й звіримо з тим, що зробив Python;
  4. розберемо is проти == і кеш малих цілих;
  5. поміняємо значення місцями;
  6. зловимо класичну пастку зі спільним списком;
  7. подивимось на лічильник посилань і del.

Запускай клітинки згори вниз: кожна друкує результат.

1 · Обʼєкт має тип, значення й номер¶

Створимо один обʼєкт і подивимось на всі три його властивості одразу. Функція id() повертає номер обʼєкта — у CPython це його адреса в памʼяті. Число буде своє при кожному запуску, і це нормально.

In [1]:
привітання = "привіт, Python"

print("значення:", привітання)
print("тип:     ", type(привітання))
print("номер:   ", id(привітання))
значення: привіт, Python
тип:      <class 'str'>
номер:    140219463709536

2 · b = a не копіює, а чіпляє другий ярлик¶

Створимо список і дамо йому друге імʼя. Якщо модель «коробки» була б правильною, номери обʼєктів мали б відрізнятися. Перевіримо.

In [2]:
покупки = ["хліб", "молоко"]
другий_ярлик = покупки          # НЕ копія: те саме, лише інше імʼя

print("покупки      :", покупки, "| номер:", id(покупки))
print("другий_ярлик :", другий_ярлик, "| номер:", id(другий_ярлик))
print("це той самий обʼєкт?", другий_ярлик is покупки)

# перевіряємо наше передбачення з лекції: обʼєкт один на два імені
assert другий_ярлик is покупки, "присвоєння раптом почало копіювати!"
assert id(другий_ярлик) == id(покупки), "is і id() мають казати те саме"
print("✅ передбачення справдилось: обʼєкт один, ярликів два")
покупки      : ['хліб', 'молоко'] | номер: 140219464022592
другий_ярлик : ['хліб', 'молоко'] | номер: 140219464022592
це той самий обʼєкт? True
✅ передбачення справдилось: обʼєкт один, ярликів два

3 · id() до і після зміни¶

Тепер найважливіше. Змінимо список через друге імʼя й подивимось на дві речі: що побачить перше імʼя і чи змінився номер обʼєкта.

In [3]:
номер_до = id(покупки)
другий_ярлик.append("сир")      # чіпаємо лише другий ярлик

print("покупки після зміни:", покупки)
print("номер до :", номер_до)
print("номер після:", id(покупки))
print("номер той самий?", номер_до == id(покупки))

assert покупки == ["хліб", "молоко", "сир"], "зміна через друге імʼя має бути видна першому"
assert номер_до == id(покупки), "список змінюється в місці, новий обʼєкт не створюється"
print("✅ обʼєкт той самий — змінився його вміст, а не адреса")
покупки після зміни: ['хліб', 'молоко', 'сир']
номер до : 140219464022592
номер після: 140219464022592
номер той самий? True
✅ обʼєкт той самий — змінився його вміст, а не адреса

4 · Як зробити справжню копію¶

Якщо копія потрібна — про це треба сказати прямо. Метод .copy() створює новий список із тим самим вмістом. Порівняємо обидва оператори.

In [4]:
копія = покупки.copy()          # тепер це справді окремий обʼєкт

print("одразу після copy():")
print("  однакові за значенням (==):", покупки == копія)
print("  той самий обʼєкт (is)     :", покупки is копія)

копія.append("яблука")
print()
print("після зміни копії:")
print("  покупки:", покупки)
print("  копія  :", копія)

assert покупки is not копія, "copy() зобовʼязаний дати новий обʼєкт"
assert "яблука" not in покупки, "зміна копії не має чіпати оригінал"
print("✅ копія живе своїм життям")
одразу після copy():
  однакові за значенням (==): True
  той самий обʼєкт (is)     : False

після зміни копії:
  покупки: ['хліб', 'молоко', 'сир']
  копія  : ['хліб', 'молоко', 'сир', 'яблука']
✅ копія живе своїм життям

5 · Незмінне число: рухається стрілка, а не обʼєкт¶

Ті самі три дії, але з числом. Дивись на номери: після y = y + 1 номер у y буде інший, бо пʼятірку змінити неможливо — Python зробив нову шістку.

In [5]:
x = 5
y = x
print("x =", x, "| номер:", id(x))
print("y =", y, "| номер:", id(y), "| той самий обʼєкт?", x is y)

y = y + 1                        # не «змінили число», а створили нове й перечепили ярлик
print()
print("після y = y + 1")
print("x =", x, "| номер:", id(x))
print("y =", y, "| номер:", id(y), "| той самий обʼєкт?", x is y)

assert x == 5, "число незмінне: x не може постраждати від дій над y"
print("✅ x лишилось 5 — незмінний обʼєкт не постраждав")
x = 5 | номер: 140219749626632
y = 5 | номер: 140219749626632 | той самий обʼєкт? True

після y = y + 1
x = 5 | номер: 140219749626632
y = 6 | номер: 140219749626664 | той самий обʼєкт? False
✅ x лишилось 5 — незмінний обʼєкт не постраждав

6 · Рядок теж незмінний¶

Методи рядків нічого не «правлять» — вони повертають новий рядок. Найчастіша помилка новачка: викликати .upper() і забути зберегти результат.

In [6]:
місто = "Полтава"
голосно = місто.upper()

print("місто  :", місто,   "| номер:", id(місто))
print("голосно:", голосно, "| номер:", id(голосно))
print("це той самий обʼєкт?", місто is голосно)

місто.upper()                    # результат нікуди не зберегли — він просто зникає
print("а що з містом після виклику .upper() без збереження?", місто)

assert місто == "Полтава", "рядок незмінний, метод не міг його переписати"
print("✅ рядок не змінився — методи повертають НОВИЙ рядок")
місто  : Полтава | номер: 140219464138576
голосно: ПОЛТАВА | номер: 140219464137376
це той самий обʼєкт? False
а що з містом після виклику .upper() без збереження? Полтава
✅ рядок не змінився — методи повертають НОВИЙ рядок

7 · Передбач результат: вісім присвоєнь¶

Головна вправа теми. Нижче вісім коротких шматків коду. Спершу випиши на папері (або тут у клітинці нижче), що надрукує кожен. Лише потім запускай клітинки й звіряй.

# 1
a = [1, 2]; b = a; b.append(3); print(a)

# 2
a = [1, 2]; b = a.copy(); b.append(3); print(a)

# 3
x = 5; y = x; y = y + 1; print(x)

# 4
s = "кіт"; t = s; t = t + "и"; print(s)

# 5
a = [1, 2]; b = [1, 2]; print(a == b, a is b)

# 6
a = 100; b = 100; print(a is b)

# 7
пара = ([1, 2], "текст"); пара[0].append(3); print(пара)

# 8
слова = ["сонце"]; копія = слова; слова = слова + ["місяць"]; print(копія)

Підказка до всіх восьми: щоразу питай себе два питання — скільки тут обʼєктів і що саме робить рядок: чіпляє ярлик чи міняє обʼєкт.

Присвоєння 1. Зміна через друге імʼя видно першому — обʼєкт один.

In [7]:
a = [1, 2]
b = a
b.append(3)
print("1 →", a)
1 → [1, 2, 3]

Присвоєння 2. Те саме, але через .copy(). Обʼєктів тепер два.

In [8]:
a = [1, 2]
b = a.copy()
b.append(3)
print("2 →", a)
2 → [1, 2]

Присвоєння 3. Число незмінне: y перечепилось, x лишилось на місці.

In [9]:
x = 5
y = x
y = y + 1
print("3 →", x)
3 → 5

Присвоєння 4. Рядок теж незмінний — склеювання створює новий обʼєкт.

In [10]:
s = "кіт"
t = s
t = t + "и"
print("4 →", s)
4 → кіт

Присвоєння 5. Два окремі списки з однаковим вмістом: == і is розходяться.

In [11]:
a = [1, 2]
b = [1, 2]
print("5 →", a == b, a is b)
5 → True False

Присвоєння 6. Маленьке ціле число потрапляє в кеш CPython — обʼєкт один на двох.

In [12]:
a = 100
b = 100
print("6 →", a is b)
6 → True

Присвоєння 7. Кортеж незмінний — але лише на один рівень. Замінити його елемент не можна, а от змінити список усередині нього — можна.

In [13]:
пара = ([1, 2], "текст")
пара[0].append(3)
print("7 →", пара)
7 → ([1, 2, 3], 'текст')

Присвоєння 8. Найпідступніше. слова + ["місяць"] створює новий список і перечіпляє на нього імʼя слова. Старий обʼєкт лишається — і його тримає копія.

In [14]:
слова = ["сонце"]
копія = слова
слова = слова + ["місяць"]
print("8 →", копія)
8 → ['сонце']

Звіряємо всі вісім разом¶

Тут зібрані правильні відповіді. Якщо десь не збіглося — повернись до відповідної клітинки й простеж, скільки обʼєктів створює кожен рядок.

In [15]:
відповіді = {
    1: [1, 2, 3],
    2: [1, 2],
    3: 5,
    4: "кіт",
    5: (True, False),
    6: True,
    7: ([1, 2, 3], "текст"),
    8: ["сонце"],
}
print("правильні відповіді:")
print(" 1 →", відповіді[1])
print(" 2 →", відповіді[2])
print(" 3 →", відповіді[3])
print(" 4 →", відповіді[4])
print(" 5 →", відповіді[5][0], відповіді[5][1])
print(" 6 →", відповіді[6])
print(" 7 →", відповіді[7])
print(" 8 →", відповіді[8])
правильні відповіді:
 1 → [1, 2, 3]
 2 → [1, 2]
 3 → 5
 4 → кіт
 5 → True False
 6 → True
 7 → ([1, 2, 3], 'текст')
 8 → ['сонце']

Найпідступніша пара: + проти +=¶

Одна з відповідей вище виглядає нелогічною, доки не порівняєш її з майже однаковим на вигляд рядком. слова = слова + [...] створює новий список, а слова += [...] для списку змінює обʼєкт у місці.

In [16]:
слова = ["сонце"]
копія = слова
слова = слова + ["місяць"]       # новий обʼєкт: копія лишається зі старим
print("через  + :  слова =", слова, "| копія =", копія)

слова = ["сонце"]
копія = слова
слова += ["місяць"]              # зміна в місці: копія бачить те саме
print("через += :  слова =", слова, "| копія =", копія)

assert копія == ["сонце", "місяць"], "+= для списку змінює обʼєкт, а не створює новий"
print("✅ два майже однакові рядки дають різний результат")
через  + :  слова = ['сонце', 'місяць'] | копія = ['сонце']
через += :  слова = ['сонце', 'місяць'] | копія = ['сонце', 'місяць']
✅ два майже однакові рядки дають різний результат

8 · is проти == і кеш малих цілих¶

CPython наперед створює всі цілі від −5 до 256 і роздає їх повторно. Тому для дрібних чисел is випадково дає True. Для більших — уже ні. Створимо велике число з рядка, щоб напевно отримати окремий обʼєкт.

In [17]:
мале_1 = 100
мале_2 = 100
велике_1 = 1000
велике_2 = int("1000")           # int() з рядка гарантовано будує новий обʼєкт

print("мале:   100 == 100 →", мале_1 == мале_2, "| 100 is 100 →", мале_1 is мале_2)
print("велике: 1000 == 1000 →", велике_1 == велике_2, "| 1000 is 1000 →", велике_1 is велике_2)
print()
print("порівняння за значенням працює завжди:", мале_1 == мале_2 and велике_1 == велике_2)

assert велике_1 == велике_2, "== порівнює значення — і воно однакове"
print("✅ висновок: числа порівнюй через ==, а is лиши для None")
мале:   100 == 100 → True | 100 is 100 → True
велике: 1000 == 1000 → True | 1000 is 1000 → False

порівняння за значенням працює завжди: True
✅ висновок: числа порівнюй через ==, а is лиши для None

9 · Обмін значень¶

Класична задача. Python дозволяє зробити це одним рядком: спершу обчислюється весь правий бік, і лише потім роздаються імена.

In [18]:
перший = "ліворуч"
другий = "праворуч"
print("до обміну :", перший, "|", другий)

перший, другий = другий, перший
print("після     :", перший, "|", другий)

assert перший == "праворуч" and другий == "ліворуч", "обмін не спрацював"
print("✅ обмін без третього імені")
до обміну : ліворуч | праворуч
після     : праворуч | ліворуч
✅ обмін без третього імені

10 · Пастка спільного списку¶

Тут вперше зʼявиться функція — забігання наперед: це спосіб дати імʼя шматку коду. Докладно про функції буде далі в курсі, зараз важлива лише одна деталь: список [] у заголовку створюється один раз, коли Python читає рядок def.

In [19]:
def додати_у_кошик(що, кошик=[]):       # ⚠️ так робити НЕ можна
    кошик.append(що)
    return кошик

перший_виклик = додати_у_кошик("яблуко")
другий_виклик = додати_у_кошик("груша")
третій_виклик = додати_у_кошик("слива")

print("перший виклик :", перший_виклик)
print("другий виклик :", другий_виклик)
print("третій виклик :", третій_виклик)
print("це один і той самий список?", перший_виклик is третій_виклик)

assert перший_виклик is третій_виклик, "усі виклики працюють з одним обʼєктом"
print("✅ пастка спрацювала: у «порожньому» кошику вже", len(третій_виклик), "елементи")
перший виклик : ['яблуко', 'груша', 'слива']
другий виклик : ['яблуко', 'груша', 'слива']
третій виклик : ['яблуко', 'груша', 'слива']
це один і той самий список? True
✅ пастка спрацювала: у «порожньому» кошику вже 3 елементи

Тепер правильний варіант. Значення за замовчуванням — None, незмінний одинак, який неможливо зіпсувати. Порожній список створюємо всередині, на кожен виклик свій.

In [20]:
def додати_у_кошик_чесно(що, кошик=None):
    if кошик is None:                    # is для None — той самий випадок, коли він доречний
        кошик = []
    кошик.append(що)
    return кошик

а = додати_у_кошик_чесно("яблуко")
б = додати_у_кошик_чесно("груша")

print("перший виклик :", а)
print("другий виклик :", б)
print("це один і той самий список?", а is б)

assert а == ["яблуко"] and б == ["груша"], "кожен виклик має отримати свій список"
assert а is not б, "обʼєкти мають бути різні"
print("✅ тепер кожен виклик має власний кошик")
перший виклик : ['яблуко']
другий виклик : ['груша']
це один і той самий список? False
✅ тепер кожен виклик має власний кошик

11 · Лічильник посилань і del¶

del прибирає імʼя, а не обʼєкт. Обʼєкт живе, поки на нього вказує хоч хтось. sys.getrefcount() показує лічильник — памʼятай, що вона сама на мить створює зайве посилання, тому число завжди на одиницю більше за очікуване.

In [21]:
import sys

числа = [1, 2, 3]
print("лічильник після створення :", sys.getrefcount(числа))

ще_одне_імʼя = числа
print("лічильник з другим імʼям  :", sys.getrefcount(числа))

del числа                                # прибрали ЯРЛИК, не обʼєкт
print("після del перше імʼя зникло, а обʼєкт живий:", ще_одне_імʼя)
print("лічильник тепер            :", sys.getrefcount(ще_одне_імʼя))

assert ще_одне_імʼя == [1, 2, 3], "del не мав знищити обʼєкт — на нього ще є посилання"
print("✅ обʼєкт помирає лише тоді, коли зникає ОСТАННЄ імʼя")
лічильник після створення :
 2
лічильник з другим імʼям  : 3
після del перше імʼя зникло, а обʼєкт живий: [1, 2, 3]
лічильник тепер            : 2
✅ обʼєкт помирає лише тоді, коли зникає ОСТАННЄ імʼя

Що станеться, якщо звернутися до видаленого імені? Помилка — і це теж навчальний матеріал. Ловимо її, щоб зошит виконався до кінця.

In [22]:
try:
    print(числа)
except NameError as помилка:
    print("NameError:", помилка)
    print("саме так: ярлика більше немає, і Python чесно про це каже")
NameError:
 name 'числа' is not defined
саме так: ярлика більше немає, і Python чесно про це каже

Завдання трьох рівнів¶

🟢 Рівень 1 — База¶

Створи список із трьох улюблених фільмів, дай йому друге імʼя, додай четвертий фільм через друге імʼя й доведи двома способами, що обʼєкт один: через is і через id().

🟡 Рівень 2 — Плюс¶

Візьми список списків: розклад = [["пн"], ["вт"]]. Зроби копія = розклад.copy(), а потім копія[0].append("математика"). Подивись на розклад і поясни словами, чому .copy() цього разу не врятував.

🔴 Рівень 3 — Виклик¶

Напиши три рядки коду, після яких a == b дає True, а a is b — False, і ще три, після яких обидва дають True. Для кожної пари поясни, скільки обʼєктів створено і чому.

Розгорнуті умови й критерії «зроблено» — у homework.md.