# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!