Модуль 3 · Урок 11 із 58
Scope, binding і mutability без прихованого стану
Python передає функції об’єкти, а parameters стають локальними іменами, прив’язаними до цих об’єктів. Через це rebinding локального імені й mutation спільного list — різні події. Розуміння scope та aliasing прибирає клас помилок, які виглядають як «функція сама щось змінила».
Безпечна практика модуля 3
Пакет містить тільки синтетичні function contracts, import-safe modules і локальні tests. Жодних реальних звернень, персональних даних, secrets, мережевих викликів або зовнішніх залежностей.
Імена шукаються за scopes
Для звичайного function body корисна модель LEGB: Local, Enclosing, Global, Built-ins. Assignment усередині функції за замовчуванням створює local binding. Reference до імені шукає його від local назовні.
threshold = 60
def classify(minutes: int) -> str:
label = "urgent" if minutes >= threshold else "standard"
return labelminutes і label local; threshold знайдено в module global scope. Для core-логіки краще передати threshold parameter-ом: dependency стане видимою в сигнатурі й тесті.
Чому виникає UnboundLocalError
count = 0
def increment() -> int:
count = count + 1
return countЧерез assignment Python вважає count local у всьому function block, але справа воно читається до local binding. global технічно змінить правило, однак shared mutable state ускладнить тести. Чистіший контракт: increment(count) повертає нове число, а owner state зберігає його явно.
Rebinding не змінює caller object
def replace_label(label: str) -> str:
label = label.strip().casefold()
return label
raw = " Urgent "
clean = replace_label(raw)
print(raw) # " Urgent "
print(clean) # "urgent"Local name label спочатку вказує на той самий immutable str, потім rebind-иться до нового str. Caller name raw не змінюється.
Mutation видно через alias
def append_route(routes: list[str], route: str) -> None:
routes.append(route)
items = ["standard"]
append_route(items, "urgent")
print(items) # ["standard", "urgent"]routes та items посилаються на один mutable list, тому append видно caller-у. Це може бути валідним контрактом, але його треба назвати й тестувати. Якщо mutation не потрібна, створіть copy й поверніть новий object.
def with_route(routes: list[str], route: str) -> list[str]:
result = list(routes)
result.append(route)
return resultShallow copy має межу
list(routes) або slice копіює outer container, але nested mutable objects лишаються shared. Deep copy не є автоматично правильним рішенням: він може бути дорогим і приховати неясну ownership model. Спершу визначте, хто володіє object і хто має право його змінювати.
global і nonlocal — явні, але дорогі залежності
global rebinding-ить module-level name, nonlocal — binding найближчого enclosing function scope. Обидва механізми законні, але для навчальної бізнес-логіки parameter + return зазвичай прозоріші. Closure корисний, коли state справді належить створеній function instance; його behavior усе одно має мати тести.
- Dependencies видно в parameters, а не випадкових globals.
- Команда відрізняє rebinding від mutation.
- Mutation argument задокументована або замінена return нового object.
- Copy policy враховує nested mutable values.
- Тест перевіряє і result, і незмінність caller input там, де це обіцяно.
Методичні джерела
- Python tutorial: Scopes and Namespaces
- Python reference: Naming and binding
- Python FAQ: Shared values and objects
Урок, сценарії, пояснення й вправи створені SEOWORK. Посилання ведуть лише на офіційну документацію Python.
Закріпіть матеріал уроку
Три сценарні питання. Для зарахування уроку потрібно дати щонайменше дві правильні відповіді.