Главная / Курсы / Python / Области видимости
# Глава 22. Области видимости > Пространства имен — это вещь! Будем делать их больше! Дзен питона Разработчикам, пришедшим в мир питона из других языков, области видимости буквально ломают шаблоны. Разберем все необходимое, чтобы быть готовыми к контринтуитивной специфике областей видимости в питоне. ## Вложенный блок — это не всегда область видимости Понятия «пространство имен» и «область видимости» тесно связаны. **Область видимости** определяет, в каком месте программы видима переменная. **Пространство имен** — это совокупность переменных, доступных в области видимости. Хранится оно в dunder-атрибуте `__dict__`, который есть у любого объекта: модуля, класса, функции. Начиная разговор про области видимости, зайдем с козырей: вложенные блоки `if`, `for`, `while`, `try/except`, `with` **не создают** область видимости. ```python {.example_for_playground} if True: x = "variable inside 'if'" print(x) ``` ``` variable inside 'if' ``` Этот пример подтвердил, что переменные, определенные внутри блока `if`, доступны и снаружи. С небольшой оговоркой: внутри **выполнившегося** блока. Инвертируем условие, чтобы оно никогда не выполнилось: ```python {.example_for_playground} if False: x = "variable inside 'if'" print(x) ``` ``` Traceback (most recent call last): File "example.py", line 4, in <module> print(x) ^ NameError: name 'x' is not defined ``` Код внутри условия не был выполнен, и при попытке обращения к несуществующей переменной сгенерировалось исключение. Блоки `for` и `while` также не создают области видимости. Но если уж возникла потребность снаружи использовать переменную, объявленную внутри цикла, нужна гарантия, что цикл выполнится хотя бы 1 раз: ```python {.example_for_playground} vals = [] for val in vals: print(f"val in loop: {val}") print(f"val after loop: {val}") ``` ``` Traceback (most recent call last): File "example.py", line 5, in <module> print(f"val after loop: {val}") ^^^ NameError: name 'val' is not defined. Did you mean: 'vals'? ``` Рассмотрим пример с циклом `for`: ```python {.example_for_playground} for i in range(3): x = i * 2 print(i) print(x) ``` ``` 2 4 ``` После цикла продолжила существовать не только переменная `x`, но и `i`! Она равна последнему значению, полученному из диапазона `range()`. Напишите цикл `while`, тело которого выполнится строго 1 раз. В нем заведите переменную `a`, равную 10. После цикла выведите ее значение в консоль. {.task_text} ```python {.task_source #python_chapter_0220_task_0010} ``` Чтобы цикл выполнился 1 раз, воспользуйтесь `break`. {.task_hint} ```python {.task_answer} while True: a = 10 break print(a) ``` Что насчет блока `try/except`? ```python {.example_for_playground} try: a = "Inside 'try'" raise ValueError("Some unexpected val") except Exception as e: b = "Inside 'except'" print(a) print(b) ``` ``` Inside 'try' Inside 'except' ``` Переменная `a` успела создаться внутри `try` до того, как выбросилось исключение. Исключение было перехвачено в блоке `except`, поэтому переменная `b` тоже создалась. В итоге обе они доступны снаружи блока `try/except`. Важная деталь: хоть `try/except` и не создает областей видимости для вложенных блоков, но переменная, в которую перехватывается исключение с помощью `except ... as`, доступна только внутри блока `except`. Допустим, нам понадобилось получить доступ к объекту исключения вне блока `except`. {.task_text} Убедитесь, что обращение к `e` напрямую завершится исключением типа `NameError`. {.task_text} Затем исправьте это введением дополнительной переменной с именем `res`. {.task_text} ```python {.task_source #python_chapter_0220_task_0020} try: x = False assert x, "x must be True" except AssertionError as e: ... print(e) ``` Заведите внутри блока `except` переменную `res` и присвойте ей значение `e`. Эту же переменную используйте для консольного вывода вне блока `except` {.task_hint} ```python {.task_answer} try: x = False assert x, "x must be True" except AssertionError as e: res = e print(res) ``` Какие сущности в питоне все же **создают** области видимости? - [Функции.](/courses/python/chapters/python_chapter_0060/) - [Классы.](/courses/python/chapters/python_chapter_0160/) - [Модули.](/courses/python/chapters/python_chapter_0200/) - [List comprehensions.](/courses/python/chapters/python_chapter_0240/) О них мы поговорим в одной из следующих глав. Что выведет этот код? {.task_text} В случае исключения напишите `error`. {.task_text} ```python {.example_for_playground} def f(a, b): res = a + b res = f(1, 2) print(res) ``` ```consoleoutput {.task_source #python_chapter_0220_task_0030} ``` В функции `f()` нет `return`. {.task_hint} ```python {.task_answer} None ``` ## Правило LEGB для разрешения имен Переменные в питоне — это имена для ссылок на объекты в памяти. Имя не объявляется заранее. Перед использованием ему должно быть присвоено значение. Место присваивания в коде определяет, в какую область видимости попадет переменная. Какими способами в питоне можно создать имя? - Присваивание: `s = 9`. - Определение функции: `def f()`. - Создание аргумента, передаваемого в функцию: `f(a, b)`. - Импорт модуля: `import math`. Либо импорт определения из модуля: `from math import pi`. - Определение класса: `class Cache`. По месту создания имени интерпретатор связывает новую переменную с соответствующим пространством имен. При изменении и удалении переменных интерпретатор по неким правилам ищет их в пространствах имен. Правила разрешения областей видимости имеют акроним LEGB. Они устанавливают порядок, в котором интерпретатор перебирает пространства имен: - **L**ocal - **E**nclosing - **G**lobal - **B**uilt-in **Local:** локальная область видимости. Объявленные в ней переменные доступны только внутри этой области и не доступны снаружи. Создается с помощью инструкций `def` или `lambda`: тело функции, метода или лямбды является локальной областью видимости. **Enclosing:** нелокальная область видимости. Это объемлющая область, созданная с помощью `def`, внутри которой содержится вложенная область видимости. Если внутри функции объявить лямбду, то для лямбды тело функции будет нелокальной областью и лямбда будет иметь к нему доступ: ```python {.example_for_playground} def f(): needle = "c" search = lambda haystack: haystack.find(needle) print(search("abcd")) f() ``` ``` 2 ``` В этом примере изнутри лямбды доступна переменная `needle`, объявленная в нелокальной по отношению к телу лямбды области видимости. **Global:** глобальная область видимости. Это пространство имен, которое создает модуль. Так как модулем можно назвать любой скрипт на питоне, то в глобальную область попадают все переменные, заведенные вне инструкций `def` и `lambda`. **Built-in:** встроенная область видимости. Это пространство для ключевых слов, функций, классов исключений и других зарезервированных в языке имен. Например, имена `range`, `Exception`, `open` хранятся во встроенной области. Встретив в коде переменную, интерпретатор ищет ее сначала в локальном, затем в нелокальном, глобальном и затем во встроенном пространстве имен: ``` ######################## # # # # # # Local # # # # # # # # # ######### # # # # # # # # Enclosing # # # # # # # ############## # # # # # # Global # # # # # ################### # # # # Built-in # # # ######################## ``` Как можно догадаться, работаем мы с тремя областями видимости. Четвертую — только ломаем. Если локальная переменная перекроет переменную с таким же именем из встроенной области видимости, вас ждет небольшая катастрофа: ```python {.example_for_playground} str = "питон сломан" print(str) x = str(7) print(x) ``` ``` питон сломан Traceback (most recent call last): File "example.py", line 5, in <module> x = str(7) ^^^^^^ TypeError: 'str' object is not callable ``` В этом примере мы затенили встроенную функцию `str()` переменной строкового типа. **Вывод:** запомните ключевые слова питона и не используйте их при именовании. Пример, демонстрирующий разрешение имен по правилу LEGB: ```python {.example_for_playground} x = 1 def f(): x = 2 def inner(): x = 3 print(f"inner(): {x}") print(f"f(): {x}") inner() print(f"global: {x}") f() ``` ``` global: 1 f(): 2 inner(): 3 ``` Переменная `x` во вложенной функции `inner()` перекрывает переменные из внешней функции `f()` и из глобальной области видимости. `x` в функции `f()` также перекрывает `x` из глобальной области. Закомментируйте объявления `x` в локальной и нелокальной областях видимости. Посмотрите, как это отразится на консольном выводе. {.task_text} ```python {.task_source #python_chapter_0220_task_0040} x = 1 def f(): x = 2 def inner(): x = 3 print(f"inner(): {x}") print(f"f(): {x}") inner() print(f"global: {x}") f() ``` Закомментируйте `x` внутри `inner()` и `f()`. {.task_hint} ```python {.task_answer} x = 1 def f(): # x = 2 def inner(): # x = 3 print(f"inner(): {x}") print(f"f(): {x}") inner() print(f"global: {x}") f() ``` Мы **не можем переопределить** переменную из внешней области видимости с помощью оператора присваивания. Вместо этого она затеняется, перекрывается локальной переменной. Но мы можем модифицировать переменную. Заменим в нашем примере целочисленную переменную `x` на список `lst`: ```python {.example_for_playground} lst = [1] def f(): lst.append(2) def inner(): lst.append(3) print(f"inner(): {lst}") print(f"f(): {lst}") inner() print(f"global: {lst}") f() ``` ``` global: [1] f(): [1, 2] inner(): [1, 2, 3] ``` Список `lst`, объявленный в глобальной области видимости, доступен для модификации в нелокальной и локальных областях. ## Ключевые слова global и nonlocal Как же быть, если внутри локальной области видимости необходимо применить оператор присваивания для внешней переменной? Убедимся, что оператор присваивания не изменит переменную из глобальной области видимости: ```python {.example_for_playground} path = "/tmp/" def set_path(): path = "/home/centos/" set_path() print(path) ``` ``` /tmp/ ``` Так как имя локальной переменной `path` внутри функции перекрыло глобальное имя, ничего не получилось. В подобных случаях на помощь придут ключевые слова `global` и `nonlocal`. После них через запятую перечисляются переменные из внешней области видимости, с которыми предстоит работать: ```python global var1, var2, var3 ``` `global` указывает, что переменные следует искать в глобальной области видимости; `nonlocal` — в нелокальной. Поправим пример выше. Добавим перед присваиванием `path` указание, что это глобальная переменная: ```python {.example_for_playground} path = "/tmp/" def set_path(): global path path = "/home/centos/" set_path() print(path) ``` ``` /home/centos/ ``` Вывод скрипта изменился: теперь вместо `"/tmp/"` переменная `path` равна значению, присвоенному внутри функции. Один из сценариев использования `global` — ленивая инициализация глобальных переменных. Причем `nonlocal` с той же целью использовать не получится. Организуйте ленивую инициализацию глобальной переменной `stats`:{.task_text} - Заведите функцию `load_stats()`, внутри которой присвойте глобальной переменной `stats` какое-нибудь значение. При этом снаружи функции ее не объявляйте (в этом и смысл ленивой инициализации). - Вызовите функцию `load_stats()`. - Убедитесь, что после вызова функции переменная появилась в глобальной области видимости: выведите в консоль ее значение. ```python {.task_source #python_chapter_0220_task_0050} ``` Не забудьте про ключевое слово `global`. {.task_hint} ```python {.task_answer} def load_stats(): global stats stats = "a" load_stats() print(stats) ``` Вообще использование `global` и `nonlocal` не приветствуется и как правило свидетельствует об ошибках в архитектуре проекта. Глобальные переменные делают код путанным и подверженным разнообразным ошибкам. Например, вызывающий код полагается на то, что ленивая инициализации переменной состоялась и обращается к ней, но инициализирующая переменную функция еще не была вызвана. Поэтому старайтесь избегать применения этих ключевых слов в пользу более гибких решений. ## Встроенные функции для работы с пространствами имен Для чтения и модификации пространств имен предусмотрено несколько встроенных функций: - `dir()`. При вызове без аргументов возвращает список имен в текущей области видимости. Если же в функцию передать объект, она возвращает список его атрибутов. Этот сценарий использования `dir()` [рассмотрен](/courses/python/chapters/python_chapter_0180#block-dir) в главе про модель данных. - `vars()`. Принимает объект и возвращает его dunder-атрибут `__dict__`. - `locals()`. Возвращает словарь имен, доступных в точке вызова функции из локальной области видимости. - `globals()`. Возвращает словарь имен, доступных в глобальной области видимости. Если вызвать `locals()` и `globals()` в глобальном пространстве имен, то они будут ссылаться на один и тот же словарь: ```python {.example_for_playground} a = 1 b = 2 def f(): x = 8 print(locals() is globals()) print(locals()) ``` ``` True {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fd666ff3350>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/home/ail/test2.py', '__cached__': None, 'a': 1, 'b': 2, 'f': <function f at 0x7fd666b13240>} ``` Найдите место в коде, в котором `locals()` вернет словарь из двух элементов с ключами `a` и `b`. Выведите в консоль значение `locals()` в этом месте. {.task_text} ```python {.task_source #python_chapter_0220_task_0060} x = 1 def f(): a = "a" b = "b" c = "c" y = 2 f() ``` Вызовите `print(locals())` на строке перед объявлением переменной `c` в функции `f()`. {.task_hint} ```python {.task_answer} x = 1 def f(): a = "a" b = "b" print(locals()) c = "c" y = 2 f() ``` ## Резюмируем - Вложенные блоки `if`, `for`, `while`, `try/except`, `with` **не создают** область видимости. - Разрешение имен в областях видимости осуществляется по правилу LEGB. - Ключевые слова `global` и `nonlocal` указывают, что используемая переменная принадлежит внешней области видимости. - `global` можно использовать для ленивого создания переменных в глобальной области. `nonlocal` этого не может. - Для работы с переменными в пространствах имен предусмотрены встроенные функции `dir()`, `vars()`, `locals()`, `globals()`.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!