Python & AI Tutorials Logo
Программирование Python

21. Область видимости переменных и разрешение имён

Когда вы создаёте переменную в Python, где она «живёт»? Может ли функция видеть переменные, созданные вне неё? Может ли код вне функции получить доступ к переменным, созданным внутри неё? Эти вопросы относятся к области видимости(scope) — части вашей программы, где имя видно и может быть использовано.

Понимание области видимости критически важно для написания функций, которые работают корректно и предсказуемо. Без этих знаний вы можете случайно создать баги, когда переменные имеют не те значения, которые вы ожидаете, или когда изменения переменных не сохраняются так, как задумано.

В этой главе мы рассмотрим, как Python определяет, на какую переменную указывает имя, как управлять тем, где переменные доступны, и что происходит, когда вы удаляете имя. К концу вы будете понимать правила, которые управляют видимостью переменных в программах Python.

21.1) Локальные и глобальные переменные

Каждая переменная в Python существует в определённой области видимости(scope) — участке кода, где это имя переменной определено и доступно. Две самые базовые области видимости — локальная(local) и глобальная(global).

Понимание глобальной области видимости

Переменные, созданные на верхнем уровне вашей программы — вне любой функции — существуют в глобальной области видимости(global scope). Они называются глобальными переменными(global variables), и они доступны из любого места в вашем модуле после того, как определены.

python
# Глобальная переменная — определена на уровне модуля
total_users = 0
 
def show_user_count():
    # Эта функция может ЧИТАТЬ глобальную переменную
    print(f"Total users: {total_users}")
 
show_user_count()  # Output: Total users: 0
print(total_users)  # Output: 0

В этом примере total_users — глобальная переменная. И функция show_user_count(), и код на уровне модуля могут обращаться к ней. Думайте о глобальных переменных как о видимых во всём файле вашей программы.

Понимание локальной области видимости

Переменные, созданные внутри функции, существуют в локальной области видимости(local scope) этой функции. Они называются локальными переменными(local variables) и доступны только внутри функции, где определены. Как только функция завершает выполнение, локальные переменные исчезают.

python
def calculate_discount(price):
    # discount_rate — ЛОКАЛЬНА для этой функции
    discount_rate = 0.15
    discount_amount = price * discount_rate
    return discount_amount
 
result = calculate_discount(100)
print(result)  # Output: 15.0
 
# Это вызвало бы ошибку — discount_rate здесь не существует
# print(discount_rate)  # NameError: name 'discount_rate' is not defined

Переменные discount_rate и discount_amount существуют только пока выполняется calculate_discount(). После возврата из функции эти имена больше не существуют. На самом деле это хорошо — это предотвращает засорение программы временными переменными, создаваемыми функциями.

Почему важна локальная область видимости

Локальная область видимости обеспечивает инкапсуляцию(encapsulation) — у каждой функции есть собственное приватное рабочее пространство. Это означает, что вы можете использовать одинаковые имена переменных в разных функциях без конфликтов:

python
def calculate_tax(amount):
    rate = 0.08  # Локальная переменная
    return amount * rate
 
def calculate_shipping(weight):
    rate = 5.00  # Другая локальная переменная с тем же именем
    return weight * rate
 
tax = calculate_tax(100)
shipping = calculate_shipping(3)
 
print(f"Tax: ${tax}")         # Output: Tax: $8.0
print(f"Shipping: ${shipping}")  # Output: Shipping: $15.0

Обе функции используют переменную с именем rate, но это полностью разные переменные в разных локальных областях видимости. Изменения rate в одной функции не влияют на rate в другой функции. Такая изоляция делает функции более надёжными и понятными.

Чтение глобальных переменных из функций

Функции могут читать глобальные переменные без какого-либо специального синтаксиса:

python
# Глобальная конфигурация
max_login_attempts = 3
 
def check_login(password):
    # Чтение глобальной переменной
    if password == "secret123":
        return "Login successful"
    else:
        return f"Invalid password. You have {max_login_attempts} attempts."
 
result = check_login("wrong")
print(result)  # Output: Invalid password. You have 3 attempts.

Функция check_login() может читать max_login_attempts, потому что это глобальная переменная. Однако есть важное ограничение, которое нам нужно понять.

Правило: присваивание создаёт локальные переменные

Вот где область видимости становится хитрой. Если вы присваиваете значение имени переменной внутри функции, Python создаёт новую локальную переменную с этим именем, даже если существует глобальная переменная с таким же именем:

python
counter = 0  # Global variable
 
def increment_counter():
    # ВНИМАНИЕ: это создаёт НОВУЮ локальную переменную с именем counter — только для демонстрации
    # ПРОБЛЕМА: попытка прочитать counter до локального присваивания
    counter = counter + 1  # UnboundLocalError: local variable 'counter' referenced before assignment
    print(counter)
 
# increment_counter()  # Этот вызов приведёт к UnboundLocalError

Этот код падает, потому что Python видит присваивание counter = counter + 1 и решает, что counter должен быть локальной переменной. Но затем, когда он пытается вычислить counter + 1, локальная переменная counter ещё не имеет значения — мы пытаемся использовать её до того, как присвоили ей значение.

Это распространённый источник путаницы. Правило такое: если функция где-либо в своём теле присваивает значение имени переменной, это имя считается локальным во всей функции целиком, даже до присваивания.

Давайте увидим это более наглядно:

python
message = "Hello"  # Global variable
 
def show_message():
    print(message)  # Это работает — просто читаем global
    
def change_message():
    # ВНИМАНИЕ: это демонстрирует типичную ошибку — только для демонстрации
    # ПРОБЛЕМА: Python видит присваивание ниже, поэтому message считается локальной по всей функции
    print(message)  # UnboundLocalError!
    message = "Goodbye"  # Это делает message локальной для ВСЕЙ функции
 
show_message()  # Output: Hello
# change_message()  # Этот вызов приведёт к UnboundLocalError

Функция show_message() работает нормально, потому что она только читает message. Но change_message() падает, потому что присваивание во второй строке заставляет Python считать message локальной по всей функции, включая оператор print(), который идёт до присваивания.

Параметры — это локальные переменные

Параметры функции — это локальные переменные, которые получают начальные значения из аргументов, переданных при вызове функции:

python
def greet(name):  # 'name' — локальная переменная
    greeting = f"Hello, {name}!"  # 'greeting' тоже локальная
    return greeting
 
message = greet("Alice")
print(message)  # Output: Hello, Alice!
 
# Ни 'name', ни 'greeting' здесь не существуют
# print(name)  # NameError

Параметр name существует только внутри функции greet(). Он создаётся при вызове функции и исчезает при возврате из функции.

Практический пример: расчёт корзины покупок

Посмотрим, как локальная и глобальная область видимости работают вместе в реалистичном сценарии:

python
# Глобальная конфигурация
tax_rate = 0.08
free_shipping_threshold = 50
 
def calculate_total(subtotal):
    # Локальные переменные для этого расчёта
    tax = subtotal * tax_rate  # Чтение глобальной tax_rate
    
    # Определяем стоимость доставки
    if subtotal >= free_shipping_threshold:  # Чтение глобального порога
        shipping = 0
    else:
        shipping = 5.99
    
    total = subtotal + tax + shipping
    return total
 
# Рассчитываем для разных значений корзины
cart1 = calculate_total(30)
cart2 = calculate_total(60)
 
print(f"Cart 1 total: ${cart1:.2f}")  # Output: Cart 1 total: $38.39
print(f"Cart 2 total: ${cart2:.2f}")  # Output: Cart 2 total: $64.80

В этом примере:

  • tax_rate и free_shipping_threshold — глобальные значения конфигурации
  • subtotal, tax, shipping и total — локальные для каждого вызова calculate_total()
  • Каждый вызов функции получает свой собственный отдельный набор локальных переменных
  • Функция может читать глобальную конфигурацию, но не изменяет её

Такое разделение ответственности делает код ясным: глобальные переменные хранят конфигурацию, которая применяется везде, а локальные переменные хранят временные результаты вычислений, специфичные для каждого вызова функции.

21.2) Правило LEGB для разрешения имён

Когда Python встречает имя переменной, как он понимает, на какую переменную вы ссылаетесь? Python следует определённому порядку поиска, который называется правилом LEGB(LEGB rule). LEGB расшифровывается как Local, Enclosing, Global, Built-in — четыре области видимости, которые Python просматривает в этом порядке.

Четыре области видимости в LEGB

Давайте разберём каждую область видимости в иерархии LEGB:

  1. Local (L): область видимости текущей функции
  2. Enclosing (E): область видимости любых охватывающих функций (функций, которые содержат текущую функцию)
  3. Global (G): область видимости уровня модуля
  4. Built-in (B): встроенные имена Python, такие как print, len, int и т. д.

Когда вы используете имя переменной, Python ищет его в этих областях в порядке: L → E → G → B. Он использует первое найденное совпадение и прекращает поиск.

Локальная область видимости: первое место, куда смотрит Python

Python всегда сначала проверяет локальную область видимости:

python
def calculate_price():
    price = 100  # Local variable
    tax = 0.08   # Local variable
    total = price * (1 + tax)
    return total
 
result = calculate_price()
print(result)  # Output: 108.0

Когда Python видит price, tax и total внутри calculate_price(), он находит их в локальной области видимости и использует эти значения. Поиск останавливается на локальной области видимости — Python не нужно искать дальше.

Глобальная область видимости: когда локально этого нет

Если имя не найдено локально, Python проверяет глобальную область видимости:

python
# Global variables
default_tax_rate = 0.08
default_currency = "USD"
 
def calculate_price(amount):
    # 'amount' is local, found immediately
    # 'default_tax_rate' is not local, found in global scope
    total = amount * (1 + default_tax_rate)
    return total
 
result = calculate_price(100)
print(result)  # Output: 108.0

Когда Python встречает default_tax_rate внутри функции, он не находит его локально, поэтому ищет в глобальной области видимости и находит там.

Встроенная область видимости: предопределённые имена Python

Если имя не найдено ни в локальной, ни в глобальной области видимости, Python проверяет встроенную область видимости — имена, которые Python предоставляет автоматически:

python
def process_data(numbers):
    # 'numbers' is local
    # 'len' is not local or global - it's built-in
    count = len(numbers)
    
    # 'max' is also built-in
    maximum = max(numbers)
    
    return count, maximum
 
data = [10, 25, 15, 30, 20]
result = process_data(data)
print(result)  # Output: (5, 30)

Имена len и max не определены в вашем коде — это встроенные функции, которые предоставляет Python. Когда Python не находит эти имена локально или глобально, он проверяет встроенную область видимости и находит их там.

Охватывающая область видимости: вложенные функции

Охватывающая область видимости(enclosing scope) играет роль, когда у вас есть вложенные функции — функции, определённые внутри других функций. Здесь становится важной буква «E» в LEGB:

python
def outer_function():
    outer_var = "I'm from outer"  # In enclosing scope for inner_function
    
    def inner_function():
        inner_var = "I'm from inner"  # Local to inner_function
        # inner_function can see both inner_var (local) and outer_var (enclosing)
        print(inner_var)   # Output: I'm from inner
        print(outer_var)   # Output: I'm from outer
    
    inner_function()
 
outer_function()

Для inner_function() область видимости outer_function() является охватывающей областью(enclosing scope). Когда inner_function() обращается к outer_var, Python ищет:

  1. Локальную область видимости inner_function() — не найдено
  2. Охватывающую область видимости outer_function() — найдено! Использует это значение

LEGB в действии: простой пример

Давайте посмотрим, как все четыре области видимости работают вместе в ясном, прямолинейном примере:

python
# Встроенная: len (Python предоставляет это)
# Глобальная: multiplier
multiplier = 10
 
def outer(x):
    # Охватывающая область видимости для inner
    y = 5
    
    def inner(z):
        # Локальная область видимости
        # z — локальная переменная (L)
        # y — из охватывающей области видимости (E)
        # multiplier — из глобальной области видимости (G)
        # len — из встроенной области видимости (B)
        result = len([z, y, multiplier])  # Uses all four scopes!
        return z + y + multiplier
 
    return inner(3)
 
answer = outer(100)
print(answer)  # Output: 18

Когда Python вычисляет z + y + multiplier внутри inner():

  1. L (Local): находит z = 3
  2. E (Enclosing): находит y = 5 в outer()
  3. G (Global): находит multiplier = 10
  4. B (Built-in): находит функцию len

Этот пример наглядно демонстрирует, как Python ищет по всем четырём областям видимости, чтобы разрешать имена.

Затенение: когда внутренние области скрывают внешние имена

Если одно и то же имя существует в нескольких областях видимости, «побеждает» самая внутренняя область — это называется затенением(shadowing):

python
value = "global"
 
def outer():
    value = "enclosing"
    
    def inner():
        value = "local"
        print(value)  # Which value?
    
    inner()
    print(value)  # Which value?
 
outer()
print(value)  # Which value?

Вывод:

local
enclosing
global

Каждый print() видит разное value, потому что Python останавливается на первом совпадении:

  • Внутри inner(): находит value локально → печатает "local"
  • Внутри outer(), но вне inner(): находит value в области outer() → печатает "enclosing"
  • На уровне модуля: находит value глобально → печатает "global"

Визуализация порядка поиска LEGB

Да

Нет

Да

Нет

Да

Нет

Да

Нет

Ссылка на имя

Найдено в Local?

Использовать значение Local

Найдено в Enclosing?

Использовать значение Enclosing

Найдено в Global?

Использовать значение Global

Найдено в Built-in?

Использовать значение Built-in

NameError

Эта диаграмма показывает процесс поиска в Python. Он начинается с самой внутренней области видимости и движется наружу. Если имя не найдено ни в одной области видимости, Python выбрасывает NameError.

Почему LEGB важно для написания функций

Понимание LEGB помогает вам:

  1. Предсказывать значения переменных: вы точно знаете, какую переменную использует Python
  2. Избегать конфликтов имён: вы понимаете, когда имена затеняют друг друга
  3. Проектировать лучшие функции: вы можете решать, какая область видимости подходит для каждой переменной
  4. Отлаживать проблемы области видимости: когда у переменных неожиданные значения, вы можете пройтись по LEGB

Правило LEGB — фундамент того, как Python разрешает имена. Каждый раз, когда вы используете переменную, Python следует этому правилу «за кулисами».

21.3) Осторожное использование ключевого слова global

Мы видели, что функции могут читать глобальные переменные, но что если вам нужно изменить глобальную переменную изнутри функции? Для этого существует ключевое слово global — но использовать его следует экономно и осторожно.

Проблема: присваивание создаёт локальные переменные

Как мы узнали ранее, присваивание переменной внутри функции создаёт локальную переменную:

python
counter = 0  # Global variable
 
def increment():
    # WARNING: This creates a NEW local variable named counter - for demonstration only
    # PROBLEM: Trying to read counter before assigning to it locally
    counter = counter + 1  # UnboundLocalError!
    
# increment()  # This call results in UnboundLocalError

Это не работает, потому что Python видит присваивание и считает counter локальной по всей функции. Но мы пытаемся прочитать counter до того, как присвоили ей значение локально.

Это одна из самых распространённых ошибок при работе с глобальными переменными. Сообщение UnboundLocalError: local variable 'counter' referenced before assignment точно говорит, что произошло: Python решил, что counter локальная (из-за присваивания), но вы попытались использовать её до того, как дали ей значение.

Решение: объявление переменных глобальными

Ключевое слово global говорит Python: «Не создавай новую локальную переменную с этим именем. Используй вместо этого глобальную переменную».

python
counter = 0  # Global variable
 
def increment():
    global counter  # Сказать Python использовать глобальную counter
    counter = counter + 1  # Теперь это изменяет глобальную переменную
 
print(f"Before: {counter}")  # Output: Before: 0
increment()
print(f"After: {counter}")   # Output: After: 1
increment()
print(f"After again: {counter}")  # Output: After again: 2

Объявление global counter должно стоять до использования переменной. Оно сообщает Python, что любые присваивания counter в этой функции должны изменять глобальную переменную, а не создавать локальную.

Несколько глобальных переменных

Вы можете объявить несколько переменных глобальными в одном операторе:

python
total_sales = 0
total_customers = 0
 
def record_sale(amount):
    global total_sales, total_customers
    total_sales += amount
    total_customers += 1
 
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $0, Customers: 0
 
record_sale(25.50)
record_sale(30.00)
 
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $55.5, Customers: 2

Обе переменные total_sales и total_customers объявлены как глобальные, поэтому функция может изменять обе.

Когда использовать global: общее состояние

Ключевое слово global уместно, когда вам нужно поддерживать общее состояние — данные, к которым нескольким функциям нужно обращаться и которые им нужно изменять:

python
# Состояние игры
player_score = 0
player_lives = 3
game_over = False
 
def award_points(points):
    global player_score
    player_score += points
    print(f"Score: {player_score}")
 
def lose_life():
    global player_lives, game_over
    player_lives -= 1
    print(f"Lives remaining: {player_lives}")
    
    if player_lives <= 0:
        game_over = True
        print("Game Over!")
 
def check_game_status():
    # Просто читаем globals — ключевое слово global не нужно
    if game_over:
        return "Game Over"
    else:
        return f"Playing - Score: {player_score}, Lives: {player_lives}"
 
# Играем
award_points(100)    # Output: Score: 100
award_points(50)     # Output: Score: 150
lose_life()          # Output: Lives remaining: 2
print(check_game_status())  # Output: Playing - Score: 150, Lives: 2

Этот пример показывает уместное использование global: нескольким функциям нужно изменять общее состояние игры. Однако обратите внимание, что check_game_status() не нужен global, потому что она только читает переменные.

Почему global нужно использовать осторожно

Хотя global иногда необходим, чрезмерное использование может сделать код менее понятным и более сложным в сопровождении. Вот почему:

Проблема 1: скрытые зависимости

Когда функции изменяют глобальные переменные, из вызова функции неочевидно, что именно меняется:

python
total = 0
 
def add_to_total(value):
    global total
    total += value
 
# Что делает эта функция? Это нельзя понять, не прочитав её код
add_to_total(10)

Сравните это с функцией, которая возвращает значение:

python
def add_to_total(current_total, value):
    return current_total + value
 
total = 0
total = add_to_total(total, 10)  # Ясно: total обновляется

Вторая версия явно показывает, что total модифицируется.

Проблема 2: тестирование становится сложнее

Функции, которые модифицируют глобальное состояние, сложнее тестировать, потому что нужно настраивать и сбрасывать глобальные переменные:

python
# Сложно тестировать — зависит от глобального состояния
score = 0
 
def add_score(points):
    global score
    score += points
 
# Каждый тест должен сбрасывать score
# Test 1
score = 0
add_score(10)
assert score == 10
 
# Test 2 - must reset score again
score = 0
add_score(20)
assert score == 20

Проблема 3: функции не являются переиспользуемыми

Функции, которые зависят от конкретных глобальных переменных, нельзя легко переиспользовать в других программах:

python
# Эта функция работает только если существует глобальная переменная с именем 'inventory'
inventory = []
 
def add_item(item):
    global inventory
    inventory.append(item)

Лучшие альтернативы global

Во многих случаях можно избежать global, используя возвращаемые значения и параметры:

Вместо изменения глобального состояния:

python
# Using global (less ideal)
balance = 1000
 
def withdraw(amount):
    global balance
    if amount <= balance:
        balance -= amount
        return True
    return False
 
withdraw(100)
print(balance)  # Output: 900

Используйте возвращаемые значения:

python
# Using return values (better)
def withdraw(balance, amount):
    if amount <= balance:
        return balance - amount, True
    return balance, False
 
balance = 1000
balance, success = withdraw(balance, 100)
print(balance)  # Output: 900

Вторая версия более гибкая, тестируемая и переиспользуемая.

Когда global действительно уместен

Есть легитимные случаи использования global:

  1. Конфигурация, которая действительно должна быть глобальной:
python
# Настройки приложения
debug_mode = False
log_level = "INFO"
 
def enable_debug():
    global debug_mode, log_level
    debug_mode = True
    log_level = "DEBUG"
  1. Счётчики для отладки или статистики:
python
# Отслеживание вызовов функций для отладки
_function_call_count = 0
 
def tracked_function():
    global _function_call_count
    _function_call_count += 1
    # ... rest of function

Ключевые выводы про global

  • Используйте global только когда вам действительно нужно изменять состояние уровня модуля
  • Вместо этого предпочитайте возвращаемые значения и параметры
  • Когда вы используете global, документируйте, почему это необходимо
  • Подумайте, нельзя ли улучшить дизайн, чтобы избежать global
  • Помните: чтение глобальных переменных не требует ключевого слова global — оно нужно только для их изменения

21.4) Использование nonlocal для изменения переменных во внешних функциях

Когда у вас есть вложенные функции, вам может понадобиться изменить переменную из области видимости охватывающей функции. Ключевое слово nonlocal служит для этого — оно похоже на global, но для областей видимости охватывающих функций, а не для глобальной области видимости.

Проблема: изменение переменных охватывающей области

Так же как присваивание по умолчанию создаёт локальные переменные, та же проблема возникает и с охватывающими областями:

python
def outer():
    count = 0  # Переменная в области outer
    
    def inner():
        # ВНИМАНИЕ: это создаёт НОВУЮ локальную переменную с именем count — только для демонстрации
        # ПРОБЛЕМА: попытка прочитать count до локального присваивания
        count = count + 1  # UnboundLocalError!
        print(count)
    
    inner()
 
# outer()  # This call results in UnboundLocalError

Python видит присваивание count в inner() и считает её локальной переменной. Но мы пытаемся прочитать её до того, как присвоим ей значение локально, что вызывает ошибку.

Решение: ключевое слово nonlocal

Ключевое слово nonlocal говорит Python: «Эта переменная не локальная — найди её в области видимости охватывающей функции и используй её».

python
def outer():
    count = 0  # Переменная в области outer
    
    def inner():
        nonlocal count  # Использовать count из области outer
        count = count + 1
        print(f"Count in inner: {count}")
    
    print(f"Count before: {count}")  # Output: Count before: 0
    inner()                          # Output: Count in inner: 1
    print(f"Count after: {count}")   # Output: Count after: 1
 
outer()

Теперь inner() может изменять переменную count из области видимости outer(). Изменение сохраняется после возврата из inner(), потому что мы изменяем реальную переменную в охватывающей области видимости.

Почему nonlocal полезен: функции, которые запоминают состояние

Ключевое слово nonlocal позволяет использовать мощный паттерн, при котором внутренние функции могут хранить и изменять состояние из своей охватывающей области видимости. Мы подробно изучим замыкания(closures) и фабричные функции(factory functions) в главе 23, но пока важно понимать, что nonlocal позволяет внутренним функциям изменять переменные из охватывающих областей.

Вот простой пример, показывающий, как работает nonlocal:

python
def create_counter():
    count = 0  # Эта переменная находится в охватывающей области для increment
    
    def increment():
        nonlocal count  # Изменять count из охватывающей области
        count += 1
        return count
    
    return increment  # Вернуть внутреннюю функцию
 
# Создаём счётчик
counter1 = create_counter()
 
print(counter1())  # Output: 1
print(counter1())  # Output: 2
print(counter1())  # Output: 3
 
# Создаём ещё один независимый счётчик
counter2 = create_counter()
 
print(counter2())  # Output: 1
print(counter2())  # Output: 2

Каждый вызов create_counter() создаёт новую переменную count и новую функцию increment(), которая может изменять конкретно эту count через nonlocal.

nonlocal против global

Важно понимать разницу:

python
x = "global"
 
def outer():
    x = "enclosing"
    
    def use_global():
        global x  # Относится к глобальной x
        print(f"use_global sees: {x}")  # Output: use_global sees: global
    
    def use_nonlocal():
        nonlocal x  # Относится к x из outer
        print(f"use_nonlocal sees: {x}")  # Output: use_nonlocal sees: enclosing
    
    use_global()
    use_nonlocal()
 
outer()
  • global всегда относится к области видимости уровня модуля
  • nonlocal относится к ближайшей охватывающей области видимости функции

Когда нельзя использовать nonlocal

Ключевое слово nonlocal работает только с областями видимости охватывающих функций. Его нельзя использовать для:

  1. Глобальной области видимости (вместо этого используйте global):
python
x = "global"
 
def func():
    nonlocal x  # SyntaxError: no binding for nonlocal 'x' found
    x = "modified"
  1. Переменных, которых не существует ни в одной охватывающей области видимости:
python
def outer():
    def inner():
        nonlocal count  # SyntaxError: no binding for nonlocal 'count' found

Ключевые выводы про nonlocal

  • Используйте nonlocal, чтобы изменять переменные из областей видимости охватывающих функций
  • nonlocal ищет в областях видимости охватывающих функций, а не в глобальной области
  • Чтение охватывающих переменных не требует nonlocal — оно нужно только для их изменения
  • nonlocal позволяет реализовывать мощные паттерны создания функций с приватным состоянием
  • Мы узнаем больше про замыкания и фабричные функции в главе 23

Ключевое слово nonlocal особенно полезно для создания функций, которые поддерживают приватное состояние, как мы видели на примере со счётчиком.

21.5) Удаление имён (а не объектов) с del и что это означает

Иногда вам нужно удалить переменную из пространства имён(namespace) вашей программы — возможно, чтобы освободить память в долго работающих программах, очистить временные переменные или удалить элементы из коллекций. Оператор del в Python выполняет эти задачи, но важно понимать, что именно он делает и чего не делает.

Оператор del в Python часто неправильно понимают. Он не удаляет объекты — он удаляет имена (привязки переменных). Понимание этого различия критически важно для понимания того, как Python управляет памятью и ссылками.

Что на самом деле делает del

Оператор del удаляет имя из текущей области видимости:

python
x = 42
print(x)  # Output: 42
 
del x
 
# print(x)  # NameError: name 'x' is not defined

После del x имя x больше не существует в текущей области видимости. Если вы попытаетесь использовать его, Python выбросит NameError, потому что имя больше не определено.

Удаление имён против удаления объектов

Вот ключевой инсайт: del удаляет имя, но не обязательно объект, на который это имя ссылается:

python
# Создаём список и два имени, которые на него ссылаются
original = [1, 2, 3]
reference = original  # Оба имени ссылаются на один и тот же список
 
print(original)   # Output: [1, 2, 3]
print(reference)  # Output: [1, 2, 3]
 
# Удаляем одно имя
del original
 
# Список всё ещё существует, потому что 'reference' всё ещё на него ссылается
print(reference)  # Output: [1, 2, 3]
 
# print(original)  # NameError: name 'original' is not defined

Список [1, 2, 3] продолжает существовать, потому что reference всё ещё ссылается на него. Удаление original удалило только это имя — оно не удалило сам объект списка.

Когда объекты действительно удаляются

Python автоматически удаляет объекты, когда на них больше не ссылается ни одно имя. Это называется сборкой мусора(garbage collection):

python
data = [1, 2, 3]  # Создаётся список, 'data' на него ссылается
 
del data  # Имя 'data' удаляется
 
# Теперь на список нет ссылок, поэтому Python со временем удалит его
# (Это происходит автоматически — вам не нужно ничего делать)

Когда мы удаляем data, у списка [1, 2, 3] не остаётся ссылок, поэтому сборщик мусора Python со временем освободит память. Но это происходит автоматически — вы не управляете моментом, когда это случится.

Удаление элементов из коллекций

Оператор del также может удалять элементы из коллекций, но это принципиально отличается от удаления имён. Когда вы используете del с индексированием или срезами коллекции, вы модифицируете саму коллекцию, а не удаляете имя.

Это важное различие: когда вы пишете del numbers[2], вы вызываете специальный метод объекта списка, чтобы удалить элемент. Имя numbers по-прежнему существует и по-прежнему ссылается на тот же объект списка — просто элементов в списке стало меньше.

python
# Удаление элементов списка по индексу
numbers = [10, 20, 30, 40, 50]
del numbers[2]  # Remove the element at index 2
print(numbers)  # Output: [10, 20, 40, 50]
 
# Удаление срезов списка
numbers = [10, 20, 30, 40, 50]
del numbers[1:3]  # Remove elements from index 1 to 3 (exclusive)
print(numbers)  # Output: [10, 40, 50]
 
# Удаление записей из словаря
person = {'name': 'Alice', 'age': 30, 'city': 'Boston'}
del person['age']
print(person)  # Output: {'name': 'Alice', 'city': 'Boston'}
© 2025. Primesoft Co., Ltd.
support@primesoft.ai