Области видимости в Python

Перевод статьи Understanding Python scopeopen in new window.

Рассмотрим функцию Python и модульный тест для неё. Что в ней неверно?

import unittest

def method_under_test(callback, value):
    """Call callback with value."""
    callback(value)

class MyTestCase(unittest.TestCase):
    def test_function_calls_callback(self):
        callback_called = False
        def callback(actual):
            callback_called = True
            if actual != 42:
                raise AssertionError('wrong value!')

        method_under_test(callback, 42)
        self.assertTrue(callback_called)

if __name__ == '__main__':
    unittest.main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Запустим тест:

$ python test.py
F
======================================================================
FAIL: test_function_calls_callback (__main__.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "x.py", line 16, in test_function_calls_callback
    self.assertTrue(callback_called)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Выглядит так как будто функция callback не вызвана. Проверим это: если изменить аргумент для callback передаваемый в method_under_test(), вы увидите что проверка в функции callback срабатывает.

Разберёмся почему так происходит.

Виды областей видимости

Начнём с основ. Как и большинство языков, Python использует статические области видимостиopen in new window переменных. Упрощённо, это означает, что для определения объекта на который ссылается идентификатор достаточно только изучения текста программы.

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

Динамические области видимости редко применяются в современных языках программирования, так как затрудняют анализ программы: определить поведение функции значительно сложнее. Примеры языков которые использующих динамические области видимости: Perl и Emacs Lisp. Все ANGOL3-подобные языки (Pascal, C, C#, Java и др.) используют статические области видимости.

Статические области видимости также называют лексическими областями видимости. Иногда термин лексические области видимости используется для отделения подмножества языков со статическими областями видимости которые разрешают произвольные вложенные области видимости: для определения привязки переменной к объекту используются родительские области видимости.

Замыкания

Никакое из определений выше не имеет ничего общего с замыканиями. Замыкания это экземпляр функции ссылающийся на нелокальные переменные. Таким образом, замыкание это комбинация лямбда-выражения (функции со свободными переменными) вместе с привязкой свободных переменных к специальным объектам. Замыкание не означает анонимную функцию, или функцию без переменных.

В тоже время, различия между языками могут быть в том могут ли перепрвязываться нелокальные переменные внутри замыкания и видны ли последствия этого другим замыканиям (и даже при повторном вызове одного замыкания).

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

global и nonlocal

Вернёмся к примеру. Python использует статические области видимости и не содержит явного оператора определения переменной (как ключевое слово var). Это означает что когда оператор присваивания ссылается на переменную, то интерпретатор должен знать как определить что должно произойти: изменение существующей нелокальной привязки этой переменной к объекту или создание новой локальной привязки которая скроет существующую.

Правило используемое в Python: любое присваивание внутри блока устанавливает новую локальную привязку. Если используется ключевое слово global перед именем переменной внутри блока то привязка создаётся на уровне модуля.

Когда мы пишем:

def test_function_calls_callback(self):
    callback_called = False
    def callback(actual):
        callback_called = True
1
2
3
4

Второе присваивание создаёт новую локальную привязку для переменной callback_called которая скрывает нелокальную. Исходная переменная, объявленная в начале метода test_function_calls_callback не меняется.

Python мог бы делать и по-другому: например, Ruby 1.8 интерпретирует присваивание внутри блока как создание новой привязки только если она не скроет уже существующую локальную привязку, если привязка уже существует то происходит перепривязка существующей переменной.

В Python 3 добавлено ключевое слово nonlocal, которое как global, может использоваться для принудительной интерпретации присваивания как создания (или переписывания) привязки во внешней области видимости.

Проблему также можно решить использование изменяемых структур данныхopen in new window:

def test_function_calls_callback(self):
    callback_called = [False]
    def callback(actual):
        callback_called[0] = True
        if actual != 42:
            raise AssertionError('wrong value!')

    method_under_test(callback, 42)
    self.assertTrue(callback_called[0])
1
2
3
4
5
6
7
8
9
Последниее изменение: 24.08.2023, 06:42:55