Модуль 3 · Урок 11 із 58

Scope, binding і mutability без прихованого стану

Python передає функції об’єкти, а parameters стають локальними іменами, прив’язаними до цих об’єктів. Через це rebinding локального імені й mutation спільного list — різні події. Розуміння scope та aliasing прибирає клас помилок, які виглядають як «функція сама щось змінила».

LEGBBindingMutabilityAliasing

Безпечна практика модуля 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 label

minutes і 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 result

Shallow copy має межу

list(routes) або slice копіює outer container, але nested mutable objects лишаються shared. Deep copy не є автоматично правильним рішенням: він може бути дорогим і приховати неясну ownership model. Спершу визначте, хто володіє object і хто має право його змінювати.

RebindLocal name починає вказувати на інший object.
MutateЗмінюється стан того самого object.
AliasКілька names посилаються на один object.
CopyНовий container; nested ownership треба уточнити.

global і nonlocal — явні, але дорогі залежності

global rebinding-ить module-level name, nonlocal — binding найближчого enclosing function scope. Обидва механізми законні, але для навчальної бізнес-логіки parameter + return зазвичай прозоріші. Closure корисний, коли state справді належить створеній function instance; його behavior усе одно має мати тести.

Definition of Done
  • Dependencies видно в parameters, а не випадкових globals.
  • Команда відрізняє rebinding від mutation.
  • Mutation argument задокументована або замінена return нового object.
  • Copy policy враховує nested mutable values.
  • Тест перевіряє і result, і незмінність caller input там, де це обіцяно.

Методичні джерела

Урок, сценарії, пояснення й вправи створені SEOWORK. Посилання ведуть лише на офіційну документацію Python.

Практична перевірка · урок 11 з 47

Закріпіть матеріал уроку

Три сценарні питання. Для зарахування уроку потрібно дати щонайменше дві правильні відповіді.

1. Функція читає threshold із module scope, хоча threshold є важливою business option. Як зробити dependency видимою?
2. Чому count = count + 1 усередині функції може дати UnboundLocalError, якщо count існує на module level?
3. Функція присвоїла parameter label новий normalized str. Чому original caller variable не змінився?