Практика · 03 · Змінні та обʼєкти¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
У лекції ми домовились: імʼя — це ярлик, а не коробка. Тут ти перевіриш це руками.
Що зробимо:
- подивимось на
id(),type()і на те, що присвоєння нічого не копіює; - побачимо різницю між незмінним числом і змінюваним списком — по номерах обʼєктів;
- передбачимо результат восьми присвоєнь до запуску й звіримо з тим, що зробив Python;
- розберемо
isпроти==і кеш малих цілих; - поміняємо значення місцями;
- зловимо класичну пастку зі спільним списком;
- подивимось на лічильник посилань і
del.
Запускай клітинки згори вниз: кожна друкує результат.
1 · Обʼєкт має тип, значення й номер¶
Створимо один обʼєкт і подивимось на всі три його властивості одразу.
Функція id() повертає номер обʼєкта — у CPython це його адреса в памʼяті.
Число буде своє при кожному запуску, і це нормально.
привітання = "привіт, Python"
print("значення:", привітання)
print("тип: ", type(привітання))
print("номер: ", id(привітання))
значення: привіт, Python тип: <class 'str'> номер: 140219463709536
2 · b = a не копіює, а чіпляє другий ярлик¶
Створимо список і дамо йому друге імʼя. Якщо модель «коробки» була б правильною, номери обʼєктів мали б відрізнятися. Перевіримо.
покупки = ["хліб", "молоко"]
другий_ярлик = покупки # НЕ копія: те саме, лише інше імʼя
print("покупки :", покупки, "| номер:", id(покупки))
print("другий_ярлик :", другий_ярлик, "| номер:", id(другий_ярлик))
print("це той самий обʼєкт?", другий_ярлик is покупки)
# перевіряємо наше передбачення з лекції: обʼєкт один на два імені
assert другий_ярлик is покупки, "присвоєння раптом почало копіювати!"
assert id(другий_ярлик) == id(покупки), "is і id() мають казати те саме"
print("✅ передбачення справдилось: обʼєкт один, ярликів два")
покупки : ['хліб', 'молоко'] | номер: 140219464022592 другий_ярлик : ['хліб', 'молоко'] | номер: 140219464022592 це той самий обʼєкт? True ✅ передбачення справдилось: обʼєкт один, ярликів два
3 · id() до і після зміни¶
Тепер найважливіше. Змінимо список через друге імʼя й подивимось на дві речі: що побачить перше імʼя і чи змінився номер обʼєкта.
номер_до = id(покупки)
другий_ярлик.append("сир") # чіпаємо лише другий ярлик
print("покупки після зміни:", покупки)
print("номер до :", номер_до)
print("номер після:", id(покупки))
print("номер той самий?", номер_до == id(покупки))
assert покупки == ["хліб", "молоко", "сир"], "зміна через друге імʼя має бути видна першому"
assert номер_до == id(покупки), "список змінюється в місці, новий обʼєкт не створюється"
print("✅ обʼєкт той самий — змінився його вміст, а не адреса")
покупки після зміни: ['хліб', 'молоко', 'сир'] номер до : 140219464022592 номер після: 140219464022592 номер той самий? True ✅ обʼєкт той самий — змінився його вміст, а не адреса
4 · Як зробити справжню копію¶
Якщо копія потрібна — про це треба сказати прямо. Метод .copy() створює новий
список із тим самим вмістом. Порівняємо обидва оператори.
копія = покупки.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 зробив нову шістку.
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() і забути зберегти результат.
місто = "Полтава"
голосно = місто.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. Зміна через друге імʼя видно першому — обʼєкт один.
a = [1, 2]
b = a
b.append(3)
print("1 →", a)
1 → [1, 2, 3]
Присвоєння 2. Те саме, але через .copy(). Обʼєктів тепер два.
a = [1, 2]
b = a.copy()
b.append(3)
print("2 →", a)
2 → [1, 2]
Присвоєння 3. Число незмінне: y перечепилось, x лишилось на місці.
x = 5
y = x
y = y + 1
print("3 →", x)
3 → 5
Присвоєння 4. Рядок теж незмінний — склеювання створює новий обʼєкт.
s = "кіт"
t = s
t = t + "и"
print("4 →", s)
4 → кіт
Присвоєння 5. Два окремі списки з однаковим вмістом: == і is розходяться.
a = [1, 2]
b = [1, 2]
print("5 →", a == b, a is b)
5 → True False
Присвоєння 6. Маленьке ціле число потрапляє в кеш CPython — обʼєкт один на двох.
a = 100
b = 100
print("6 →", a is b)
6 → True
Присвоєння 7. Кортеж незмінний — але лише на один рівень. Замінити його елемент не можна, а от змінити список усередині нього — можна.
пара = ([1, 2], "текст")
пара[0].append(3)
print("7 →", пара)
7 → ([1, 2, 3], 'текст')
Присвоєння 8. Найпідступніше. слова + ["місяць"] створює новий список
і перечіпляє на нього імʼя слова. Старий обʼєкт лишається — і його тримає копія.
слова = ["сонце"]
копія = слова
слова = слова + ["місяць"]
print("8 →", копія)
8 → ['сонце']
Звіряємо всі вісім разом¶
Тут зібрані правильні відповіді. Якщо десь не збіглося — повернись до відповідної клітинки й простеж, скільки обʼєктів створює кожен рядок.
відповіді = {
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 → ['сонце']
Найпідступніша пара: + проти +=¶
Одна з відповідей вище виглядає нелогічною, доки не порівняєш її з майже
однаковим на вигляд рядком. слова = слова + [...] створює новий список,
а слова += [...] для списку змінює обʼєкт у місці.
слова = ["сонце"]
копія = слова
слова = слова + ["місяць"] # новий обʼєкт: копія лишається зі старим
print("через + : слова =", слова, "| копія =", копія)
слова = ["сонце"]
копія = слова
слова += ["місяць"] # зміна в місці: копія бачить те саме
print("через += : слова =", слова, "| копія =", копія)
assert копія == ["сонце", "місяць"], "+= для списку змінює обʼєкт, а не створює новий"
print("✅ два майже однакові рядки дають різний результат")
через + : слова = ['сонце', 'місяць'] | копія = ['сонце'] через += : слова = ['сонце', 'місяць'] | копія = ['сонце', 'місяць'] ✅ два майже однакові рядки дають різний результат
8 · is проти == і кеш малих цілих¶
CPython наперед створює всі цілі від −5 до 256 і роздає їх повторно.
Тому для дрібних чисел is випадково дає True. Для більших — уже ні.
Створимо велике число з рядка, щоб напевно отримати окремий обʼєкт.
мале_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 дозволяє зробити це одним рядком: спершу обчислюється весь правий бік, і лише потім роздаються імена.
перший = "ліворуч"
другий = "праворуч"
print("до обміну :", перший, "|", другий)
перший, другий = другий, перший
print("після :", перший, "|", другий)
assert перший == "праворуч" and другий == "ліворуч", "обмін не спрацював"
print("✅ обмін без третього імені")
до обміну : ліворуч | праворуч після : праворуч | ліворуч ✅ обмін без третього імені
10 · Пастка спільного списку¶
Тут вперше зʼявиться функція — забігання наперед: це спосіб дати імʼя шматку коду.
Докладно про функції буде далі в курсі, зараз важлива лише одна деталь:
список [] у заголовку створюється один раз, коли Python читає рядок def.
def додати_у_кошик(що, кошик=[]): # ⚠️ так робити НЕ можна
кошик.append(що)
return кошик
перший_виклик = додати_у_кошик("яблуко")
другий_виклик = додати_у_кошик("груша")
третій_виклик = додати_у_кошик("слива")
print("перший виклик :", перший_виклик)
print("другий виклик :", другий_виклик)
print("третій виклик :", третій_виклик)
print("це один і той самий список?", перший_виклик is третій_виклик)
assert перший_виклик is третій_виклик, "усі виклики працюють з одним обʼєктом"
print("✅ пастка спрацювала: у «порожньому» кошику вже", len(третій_виклик), "елементи")
перший виклик : ['яблуко', 'груша', 'слива'] другий виклик : ['яблуко', 'груша', 'слива'] третій виклик : ['яблуко', 'груша', 'слива'] це один і той самий список? True ✅ пастка спрацювала: у «порожньому» кошику вже 3 елементи
Тепер правильний варіант. Значення за замовчуванням — None, незмінний одинак,
який неможливо зіпсувати. Порожній список створюємо всередині, на кожен виклик свій.
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() показує лічильник — памʼятай, що вона сама на мить створює
зайве посилання, тому число завжди на одиницю більше за очікуване.
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 ✅ обʼєкт помирає лише тоді, коли зникає ОСТАННЄ імʼя
Що станеться, якщо звернутися до видаленого імені? Помилка — і це теж навчальний матеріал. Ловимо її, щоб зошит виконався до кінця.
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.