Области видимости и замыкания
Понимание областей видимости (scope) и замыканий (closures) -- ключ к написанию правильного Python-кода. Разберём правило LEGB, ключевые слова global/nonlocal и типичные ловушки.
Правило LEGB
Python ищет имена переменных по правилу LEGB (от внутреннего к внешнему):
L - Local: тело текущей функции
E - Enclosing: тело внешней (вложенной) функции
G - Global: уровень модуля (файла)
B - Built-in: встроенные имена Python (print, len, range...)
# Built-in scope
# print, len, range, etc.
# Global scope
x = "глобальная"
def outer():
# Enclosing scope
x = "внешняя"
def inner():
# Local scope
x = "локальная"
print(f"inner: {x}") # 'локальная'
inner()
print(f"outer: {x}") # 'внешняя'
outer()
print(f"global: {x}") # 'глобальная'
Поиск имён на практике
# Python looks up names at RUNTIME, not at definition time
name = "Глобальное"
def show_name():
print(name) # will look up 'name' when called
show_name() # 'Глобальное'
name = "Изменённое"
show_name() # 'Изменённое'
# UnboundLocalError - a common trap
x = 10
def broken():
# print(x) # UnboundLocalError! Python sees assignment below
x = 20 # This makes 'x' a LOCAL variable in the entire function
print(x)
# Python determines scope at COMPILE time (not runtime)
# If there's ANY assignment to x in the function, x is local
global и nonlocal
global
counter = 0
def increment():
global counter # declare that we want to modify the global variable
counter += 1
increment()
increment()
increment()
print(counter) # 3
# Without global:
count = 0
def broken_increment():
# count += 1 # UnboundLocalError! count is treated as local
pass
# global allows reading AND writing to global variables
settings = {}
def update_settings(**kwargs):
global settings
settings.update(kwargs)
update_settings(theme="dark", lang="ru")
print(settings) # {'theme': 'dark', 'lang': 'ru'}
# NOTE: global is generally discouraged
# Better approach: use function parameters and return values
def increment_value(current: int) -> int:
return current + 1
counter = 0
counter = increment_value(counter)
nonlocal
def make_counter(start: int = 0):
"""Create a counter with enclosing scope."""
count = start
def increment() -> int:
nonlocal count # modify variable in enclosing scope
count += 1
return count
def decrement() -> int:
nonlocal count
count -= 1
return count
def get() -> int:
return count # reading doesn't need nonlocal
return increment, decrement, get
inc, dec, get = make_counter(10)
print(inc()) # 11
print(inc()) # 12
print(dec()) # 11
print(get()) # 11
# nonlocal works only with enclosing function scope, NOT global
x = 10
def outer():
x = 20
def inner():
nonlocal x # refers to outer's x, not global x
x = 30
inner()
print(x) # 30
outer()
print(x) # 10 (global x unchanged)
Замыкания (Closures)
Замыкание -- это функция, которая запоминает переменные из окружающей области видимости:
def make_greeting(greeting: str):
"""Create a greeting function with a captured greeting word."""
def greet(name: str) -> str:
return f"{greeting}, {name}!"
return greet
hello = make_greeting("Привет")
goodbye = make_greeting("До свидания")
print(hello("Иван")) # Привет, Иван!
print(goodbye("Мария")) # До свидания, Мария!
# The closure captures the variable, not just the value
print(hello.__closure__)
print(hello.__closure__[0].cell_contents) # 'Привет'
Практические примеры замыканий
# Multiplier factory
def make_multiplier(factor: int):
"""Return a function that multiplies by factor."""
def multiply(x: int) -> int:
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Logger with prefix
def make_logger(prefix: str):
"""Return a logging function with a fixed prefix."""
def log(message: str) -> None:
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] [{prefix}] {message}")
return log
db_log = make_logger("DB")
api_log = make_logger("API")
db_log("Connected") # [14:30:00] [DB] Connected
api_log("Request GET") # [14:30:00] [API] Request GET
# Memoization closure
def memoize(func):
"""Cache function results."""
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 354224848179261915075 (instant with memoization)
# Validator factory
def make_range_validator(min_val: float, max_val: float):
"""Return a validator function for a numeric range."""
def validate(value: float) -> bool:
return min_val <= value <= max_val
validate.__doc__ = f"Check if value is between {min_val} and {max_val}"
return validate
is_valid_age = make_range_validator(0, 150)
is_valid_score = make_range_validator(0, 100)
print(is_valid_age(25)) # True
print(is_valid_age(200)) # False
print(is_valid_score(85)) # True
Ловушка Late Binding
Одна из самых частых ошибок с замыканиями:
# PROBLEM: late binding in closures
functions = []
for i in range(5):
functions.append(lambda: i)
# All functions return the SAME value!
print([f() for f in functions]) # [4, 4, 4, 4, 4]
# Why? lambda captures the VARIABLE i, not its VALUE
# When called, i is already 4 (after the loop)
# FIX 1: default argument (captures value at definition time)
functions = []
for i in range(5):
functions.append(lambda i=i: i) # default argument captures current value
print([f() for f in functions]) # [0, 1, 2, 3, 4]
# FIX 2: closure factory
def make_func(x):
return lambda: x
functions = [make_func(i) for i in range(5)]
print([f() for f in functions]) # [0, 1, 2, 3, 4]
# FIX 3: functools.partial
from functools import partial
def return_value(x):
return x
functions = [partial(return_value, i) for i in range(5)]
print([f() for f in functions]) # [0, 1, 2, 3, 4]
# The same problem with callbacks
callbacks = {}
for name in ["save", "load", "delete"]:
callbacks[name] = lambda: print(f"Action: {name}")
callbacks["save"]() # Action: delete (WRONG!)
callbacks["delete"]() # Action: delete
# Fix
callbacks = {}
for name in ["save", "load", "delete"]:
callbacks[name] = lambda n=name: print(f"Action: {n}")
callbacks["save"]() # Action: save (CORRECT)
Область видимости comprehensions
# List comprehensions have their own scope (Python 3+)
x = 10
squares = [x**2 for x in range(5)]
print(x) # 10 (x from comprehension doesn't leak)
# But in Python 2, x would be 4! (loop variable leaked)
# Walrus operator inside comprehension
results = [y := x**2 for x in range(5)]
print(y) # 16 (walrus DOES leak to enclosing scope!)
# Nested comprehension scopes
matrix = [[i * j for j in range(3)] for i in range(3)]
# i and j are local to their comprehensions
Глобальное состояние: паттерны и антипаттерны
# ANTI-PATTERN: mutable global state
_cache = {}
def get_cached(key):
global _cache
return _cache.get(key)
# BETTER: module-level with controlled access
class _Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._data = {}
return cls._instance
def get(self, key, default=None):
return self._data.get(key, default)
def set(self, key, value):
self._data[key] = value
config = _Config()
# BEST: dependency injection
def process_data(data: list[int], *, config: dict[str, str]) -> list[int]:
"""Process data using injected configuration."""
threshold = int(config.get("threshold", "10"))
return [x for x in data if x > threshold]
result = process_data([5, 15, 25], config={"threshold": "10"})
Итоги
- LEGB -- порядок поиска имён: Local, Enclosing, Global, Built-in
global-- объявляет переменную как глобальную для записиnonlocal-- изменяет переменную из enclosing scope- Замыкания захватывают переменные (не значения!) из окружения
- Late binding -- частая ловушка в циклах с лямбдами, исправляется через
i=i - Python определяет scope на этапе компиляции -- присваивание делает переменную локальной
- Избегайте
global-- используйте параметры функций и возвращаемые значения - Comprehensions имеют свою область видимости (Python 3+), но walrus operator протекает наружу