2.16. Область видимості

Коли Python шукає назву всередині функції, він проходить конкретну послідовність областей видимості. Розуміння цієї послідовності пояснює, чому одні присвоєння затіняють зовнішні назви, чому інші їх модифікують, і чому вкладені функції можуть пам’ятати значення з місця їхнього визначення.

Nested boxes showing a local scope inside a module scope inside the built-in scope, with an arrow indicating that name lookup walks outward from the innermost frame.

Пошук назви починається в локальній області видимості функції і рухається назовні до областей видимості модуля та вбудованих, поки не знайдено збіг.

2.16.1. Локальна та модульна область видимості

Назви, визначені всередині функції, є локальними для цієї функції і зникають після завершення виклику:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

Назви, визначені на верхньому рівні файлу .py, є рівня модуля (іноді їх називають глобальними) і видимі скрізь у цьому файлі, включно з функціями:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

Читання назви рівня модуля зсередини функції відбувається автоматично. Присвоєння цій назві зсередини функції – ні: присвоєння створює нову локальну змінну, яка затіняє назву рівня модуля на час виклику:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

counter у лівій частині робить counter локальною назвою у bump, тому читання праворуч не знаходить жодного значення.

2.16.1.1. Ключове слово global

Щоб фактично перевизначити назву рівня модуля зсередини функції, спочатку оголосіть її global:

counter = 0

def bump():
    global counter
    counter += 1

Використовуйте global ощадливо. Функції, що змінюють приховані стани, важче осмислювати, ніж функції, що приймають значення як аргументи і повертають нові значення. Звичайне вирішення «потрібно поділитися станом» – передати об’єкт (список, словник, екземпляр класу) як аргумент і змінювати його.

2.16.2. Лямбда-вирази

lambda будує невелику анонімну функцію в одному виразі:

square = lambda x: x * x
square(7)             # 49

Це точно еквівалентно

def square(x):
    return x * x

Тіло lambda має бути одним виразом – без операторів, без кількох рядків. Основне використання – передача невеликої функції як аргументу до чогось, що приймає функцію:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

Коли тіло розростається більш ніж на один вираз, переходьте до справжнього def. Іменування функції через def також дає їй назву у трасуваннях стека, чого lambda не має.

2.16.3. Замикання

Функція, визначена всередині іншої функції, може читати назви з області видимості зовнішньої функції. Внутрішня функція захоплює ці назви і продовжує працювати навіть після завершення зовнішнього виклику:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

Виведення:

105 110

add5 і add10 – це дві окремі функції, кожна пам’ятає своє n. Функція, яка таким чином будує і повертає налаштовану внутрішню функцію, називається замиканням. Це головна причина, чому мові потрібні вкладені функції – спосіб запекти певний стан у значення функції, а потім передати це значення як єдиний об’єкт, що можна викликати.

Читання захоплених назв відбувається автоматично. Перепризначення однієї потребує додаткового ключового слова. Наведений нижче приклад виглядає правильним, але не спрацьовує:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

Присвоєння count всередині tick робить count локальним для tick, так само, як воно зробило б його локальним у функції верхнього рівня. Ключове слово nonlocal повідомляє Python: «ця назва знаходиться у зовнішній функції, перепризначте її там»:

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

Виведення:

1 2 3

nonlocal відноситься до області видимості зовнішньої функції так само, як global – до області видимості модуля. Зауважте, що мутування захопленого об’єкта (виклик some_list.append(...), some_dict[k] = v) не потребує nonlocal – назва не перепризначається, змінюється лише об’єкт, на який вона вказує.