Область видимости
Использование LEGB
Концепция области видимости в Python обычно представлена c использованием правила, известного как правило LEGB. Буквы в аббревиатуре LEGB обозначают локальную, вложенную, глобальную и встроенную (Local, Enclosing, Global и Built-in Scope) области.
- Локальная (или функция) область видимости (Local) — это блок кода или тело любой функции Python или лямбда-выражения. Эта область Python содержит имена, которые вы определяете внутри функции. Эти имена будут видны только из кода функции. Он создается при вызове функции, а не при ее определении, поэтому у вас будет столько же различных локальных областей, сколько и при вызовах функций. Это верно, даже если вы вызываете одну и ту же функцию несколько раз или рекурсивно.Каждый вызов приведет к созданию новой локальной области.
- Вложенная (или нелокальная) область видимости (Enclosing) — это особая область видимости, которая существует только для вложенных функций. Если локальная область видимости является внутренней или вложенной функцией, тогда вложенная область видимости является областью внешней или вложенной функции. Эта область содержит имена, которые вы определяете во вложенной функции. Имена в охватывающей области видны из кода внутренних и включающих функций.
- Глобальная (или модульная) область видимости (Global) — это самая верхняя область в программе, скрипте или модуле Python. Эта область Python содержит все имена, которые вы определяете на верхнем уровне программы или модуля.Имена в этой области Python видны повсюду в вашем коде.
- Встроенная область видимости (Built-in) — это специальная область видимости Python, которая создается или загружается всякий раз, когда вы запускаете скрипт или открываете интерактивный сеанс. Эта область содержит имена, такие как ключевые слова, функции, исключения и другие атрибуты, встроенные в Python. Имена в этой области Python также доступны повсюду в вашем коде. Он автоматически загружается Python при запуске программы или сценария.
Правило LEGB — это своего рода процедура поиска имени, которая определяет порядок, в котором Python ищет имена. Например, если вы ссылаетесь на данное имя, тогда Python будет искать это имя последовательно в локальной, вложенной, глобальной и встроенной области видимости. Если имя существует, вы получите первое его появление. В противном случае вы получите ошибку.
В любой момент времени во время выполнения у вас будет не более четырех активных областей видимости — локальной, вложенной, глобальной и встроенной — в зависимости от того, где вы находитесь в коде. С другой стороны, у вас всегда будет как минимум две активные области: глобальная и встроенная. Эти две области всегда под прицелом.
Функции: локальная область видимости
Локальная область видимости или область функции создаётся при вызове функции. Каждый раз, при вызове функцию, создаётся новая локальная область видимости. С другой стороны, можно рассматривать каждый оператор def и лямбда-выражение как образец для новых локальных областей видимости. Эти локальные области будут появляться всякий раз при вызове функций под рукой.
По умолчанию параметры и имена, которые вы назначаете внутри функции, существуют только в пределах функции или локальной области, связанной с вызовом функции. Когда функция завершается, локальная область видимости уничтожается, а имена забываются. Вот как это работает:
>>> def square(base):
... result = base ** 2
... print(f'The square of {base} is: {result}')
...
>>> square(10)
The square of 10 is: 100
>>> result # Недоступен снаружи square()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
result
NameError: name 'result' is not defined
>>> base # Недоступен снаружи square()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
base
NameError: name 'base' is not defined
>>> square(20)
The square of 20 is: 400
</module></stdin></module></stdin>
square() — это функция, которая вычисляет квадрат заданного числа по основанию. Когда вы вызываете функцию, Python создает локальную область видимости, содержащую базу имен (аргумент) и результат (локальную переменную). После первого вызова square() base содержит значение 10, а result — значение 100. Во второй разлокальные имена не будут запоминать значения, которые были сохранены в них при первом вызове функции. Обратите внимание, что base теперь содержит значение 20, а result — 400.
Если вы попытаетесь получить доступ к результату или базе после вызова функции, вы получите NameError, потому что они существуют только в локальной области, созданной вызовом square(). Всякий раз, когда вы пытаетесь получить доступ к имени, которое не определено ни в одной области Python, вы получаете ошибку NameError. В сообщении об ошибке будет указано имя, которое не удалось найти.
Поскольку вы не можете получить доступ к локальным именам из операторов, находящихся вне функции, разные функции могут определять объекты с одинаковыми именами. Посмотрите этот пример:
>>> def cube(base):
... result = base ** 3
... print(f'The cube of {base} is: {result}')
...
>>> cube(30)
The cube of 30 is: 27000
Обратите внимание, что вы определяете cube(), используя ту же переменную и параметр, которые вы использовали в square(). Однако, поскольку cube() не может видеть имена внутри локальной области видимости square() и наоборот, обе функции работают должным образом без какого-либо конфликта имен.
Вы можете избежать конфликтов имен в своих программах, правильно используя локальную область видимости Python. Это также делает функции более самодостаточными и создает поддерживаемые программные модули. Кроме того, поскольку вы не можете изменять локальные имена из удаленных мест в коде, ваши программы будет легче отлаживать, читать и изменять.
Вы можете проверить имена и параметры функции, используя .__code__, который является атрибутом, содержащим информацию о внутреннем коде функции. Взгляните на код ниже:
>>> square.__code__.co_varnames
('base', 'result')
>>> square.__code__.co_argcount
1
>>> square.__code__.co_consts
(None, 2, 'The square of ', ' is: ')
>>> square.__code__.co_name
'square'
В этом примере кода вы проверяете .__code__ на square(). Это специальный атрибут, содержащий информацию о коде функции Python. В этом случае вы видите, что .co_varnames содержит кортеж, содержащий имена, которые вы определяете внутри square().
Вложенные функции: вложенные данные
Вложенная или нелокальная область видимости наблюдается, когда вы вкладываете функции внутрь других функций. Вложенная область видимости была добавлена в Python 2.2. Он принимает форму локальной области видимости любой вложенной функции. Имена, которые вы определяете во вложенной области Python, обычно называют нелокальными именами. Рассмотрим следующий код:
>>> def outer_func():
... # Этот блок является локальной областью outer_func()
... var = 100 # A nonlocal var
... # Это также область видимости inner_func()
... def inner_func():
... # Этот блок является локальной областью inner_func()
... print(f"Printing var from inner_func(): {var}")
...
... inner_func()
... print(f"Printing var from outer_func(): {var}")
...
>>> outer_func()
Printing var from inner_func(): 100
Printing var from outer_func(): 100
>>> inner_func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'inner_func' is not defined
</module></stdin>
Когда вы вызываете external_func(), вы также создаете локальную область видимости. Локальная область видимости outer_func() в то же время является охватывающей областью inner_func(). Изнутри inner_func() эта область не является ни глобальной, ни локальной. Это особая область видимости, которая находится между этими двумя областями и известна как вложенная область.
В некотором смысле inner_func() — это временная функция, которая оживает только во время выполнения своей функции-оболочки external_func(). Обратите внимание, что inner_func() виден только коду в outer_func().
Все имена, которые вы создаете во вложенной области видимости, видны изнутри inner_func(), кроме созданных после вызова inner_func(). Вот новая версия outer_fun(), которая показывает это:
>>> def outer_func():
... var = 100
... def inner_func():
... print(f"Printing var from inner_func(): {var}")
... print(f"Printing another_var from inner_func(): {another_var}")
...
... inner_func()
... another_var = 200 # This is defined after calling inner_func()
... print(f"Printing var from outer_func(): {var}")
...
>>> outer_func()
Printing var from inner_func(): 100
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
outer_func()
File "<stdin>", line 7, in outer_func
inner_func()
File "<stdin>", line 5, in inner_func
print(f"Printing another_var from inner_func(): {another_var}")
NameError: free variable 'another_var' referenced before assignment in enclosing
scope
</stdin></stdin></module></stdin>
Когда вы вызываете external_func(), код работает до точки, в которой вы вызываете inner_func(). Последний оператор inner_func() пытается получить доступ к another_var. На данный момент another_var еще не определено, поэтому Python вызывает NameError, потому что не может найти имя, которое вы пытаетесь использовать.
Последний, но тем не менее важный момент, нельзя изменять имена во вложенной области из вложенной функции, если не объявите их nonlocal во вложенной функции.
Модули: глобальные данные
С момента запуска программы Python вы находитесь в глобальной области действия Python. Внутренне Python превращает основной скрипт вашей программы в модуль с именем __main__ для выполнения основной программы. Пространство имен этого модуля — основная глобальная область видимости вашей программы.
В Python понятия глобальной области видимости и глобальных имен тесно связаны с файлами модулей. Например, если вы определяете имя на верхнем уровне любого модуля Python, то это имя считается глобальным для модуля. Вот почему такая область видимости также называется областью модуля.
Всякий раз, когда вы запускаете программу Python или интерактивный сеанс, интерпретатор выполняет код в модуле или скрипте, который служит точкой входа в вашу программу. Этот модуль или скрипт загружается со специальным именем __main__. С этого момента вы можете сказать, что ваша основная глобальная область видимости — это область видимости main. Для проверки имен в вашей основной глобальной области видимости, можно использовать dir(). Если вы вызовете dir() без аргументов, то получите список имен, которые находятся в текущей глобальной области видимости. Взгляните на этот код:
>>> dir()
['__annotations__', '__builtins__',..., '__package__', '__spec__']
>>> var = 100 # Назначьте var на верхнем уровне __main__
>>> dir()
['__annotations__', '__builtins__',..., '__package__', '__spec__', 'var']
Когда вы вызываете dir() без аргументов, то получаете список имен, доступных в вашей основной глобальной области Python. Обратите внимание, что если вы назначите новое имя (например, здесь var) на верхнем уровне модуля (здесь main), то это имя будет добавлено в список, возвращаемый dir().
На выполнение программы существует только одна глобальная область видимости Python. Эта область сохраняется до тех пор, пока программа не завершится и все ее имена не будут забыты. В противном случае при следующем запуске программы имена будут помнить свои значения из предыдущего запуска. Вы можете получить доступ к значению любого глобального имени или сослаться на него из любого места в вашем коде. Сюда входят функции и классы. Вот пример, поясняющий эти моменты:
>>> var = 100
>>> def func():
... return var # Вы можете получить доступ к var из func()
...
>>> func()
100
>>> var # Остается неизменной
100
Внутри func() вы можете свободно обращаться к значению var или ссылаться на него. Это не влияет на ваше глобальное имя var, но показывает, что к var можно получить свободный доступ из func(). С другой стороны, вы не можете назначать глобальные имена внутри функций, если вы явно не объявите их как глобальные имена с помощью глобального оператора, который вы увидите позже.
Каждый раз, когда вы присваиваете значение имени в Python, может произойти одно из двух:
- Вы создаете новое имя
- Вы обновляете существующее имя
Конкретное поведение будет зависеть от области Python, в которой вы назначаете имя. Если вы попытаетесь присвоить значение глобальному имени внутри функции, тогда вы создадите это имя в локальной области видимости функции, затеняя или замещая глобальное имя. Это означает, что вы не сможете изменить большинство переменных, которые были определены вне функции, изнутри функции.
Если вы будете следовать этой логике, то поймете, что следующий код не будет работать так, как вы могли ожидать:
>>> var = 100 # Глобальная переменная
>>> def increment():
... var = var + 1 # Попробуйте обновить глобальную переменную
...
>>> increment()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
increment()
File "<stdin>", line 2, in increment
var = var + 1
UnboundLocalError: local variable 'var' referenced before assignment
</stdin></module></stdin>
Внутри increment() вы пытаетесь увеличить глобальную переменную var. Поскольку var не объявлен глобальным внутри функции increment(), Python создает новую локальную переменную с тем же именем var внутри функции. В процессе Python понимает, что вы пытаетесь использовать локальную переменную до ее первого присвоения (var + 1), поэтому вызывает UnboundLocalError. Вот еще один пример:
>>> var = 100 # Глобальная переменная
>>> def func():
... print(var) # Ссылка на глобальную переменную var
... var = 200 # Определить новую локальную переменную с тем же именем var
...
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
func()
File "<stdin>", line 2, in func
print(var)
UnboundLocalError: local variable 'var' referenced before assignment
</stdin></module></stdin>
Вы, вероятно, ожидаете, что сможете распечатать глобальную переменную и обновить ее позже, но снова вы получите UnboundLocalError. Здесь происходит следующее: когда вы запускаете тело функции func(), Python решает, что var является локальной переменной, поскольку она назначается в области видимости функции. Это не ошибка, а выбор дизайна. Python предполагает, что имена, присвоенные в теле функции, являются локальными для этой функции.
Глобальные имена можно обновлять или изменять из любого места в вашей глобальной области Python. Помимо этого, оператор global можно использовать для изменения глобальных имен практически из любого места в вашем коде.
Изменение глобальных имен обычно считается плохой практикой программирования, потому что это может привести к следующему:
- Трудно отладить: почти любой оператор в программе может изменить значение глобального имени.
- Трудно понять: вам нужно знать все операторы, которые обращаются к глобальным именам и изменяют их.
- Невозможно повторно использовать: код зависит от глобальных имен, специфичных для конкретной программы.
Хорошая практика программирования рекомендует использовать локальные имена, а не глобальные. Вот несколько советов:
- Напишите автономные функции, которые полагаются на локальные имена, а не на глобальные.
- Старайтесь использовать уникальные имена объектов, независимо от того, в какой области вы находитесь.
- Избегайте глобальных модификаций имен в ваших программах.
- Избегайте изменения имени кросс-модуля.
- Используйте глобальные имена как константы, которые не меняются во время выполнения вашей программы.
В следующем примере показано, где расположены области видимости в вашем коде и как Python ищет имена с их помощью:
>>> # Эта область является глобальной или модульной областью
>>> number = 100
>>> def outer_func():
... # Этот блок является локальной областью outer_func()
... # Это также область видимости inner_func()
... def inner_func():
... # Этот блок является локальной областью inner_func()
... print(number)
...
... inner_func()
...
>>> outer_func()
100
Когда вы вызываете outer_func(), вы получаете 100, напечатанное на вашем экране. Но как в этом случае Python ищет номер имени? Следуя правилу LEGB, вы будете искать числа в следующих местах:
Внутри inner_func(): это локальная область видимости, но числа там не существует.
Внутри outer_func(): это вложенная область,но и там число не определено.
В области видимости модуля: Это глобальная область видимости, и вы найдете там номер, чтобы вы могли вывести его на экран.
Если число не определено внутри глобальной области видимости, Python продолжает поиск, просматривая встроенную область видимости. Это последний компонент правила LEGB, как вы увидите далее.
Встроенные функции: встроенные данные
Встроенная область видимости — это специальная область видимости Python, которая реализована в виде стандартного библиотечного модуля с именем builtins в Python 3.x. Все встроенные объекты Python находятся в этом модуле. Они автоматически загружаются во встроенную область видимости, когда вы запускаете интерпретатор Python. Python выполняет поиск встроенных функций последними в поиске LEGB, так что вы получите все имена, которые он определяет, бесплатно. Это означает, что вы можете использовать их без импорта какого-либо модуля.
Обратите внимание, что имена во встроенных командах всегда загружаются в вашу глобальную область Python со специальным именем __builtins__, как вы можете видеть в следующем коде:
>>> dir()
['__annotations__', '__builtins__',..., '__package__', '__spec__']
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError',..., 'tuple', 'type', 'vars', 'zip']
В выводе первого вызова dir() вы можете видеть, что __builtins__ всегда присутствует в глобальной области Python. Если вы проверите сам __builtins__ с помощью dir(), вы получите полный список встроенных имен Python.
Встроенная область видимости добавляет более 150 имен в вашу текущую глобальную область видимости Python. Например, в Python 3.8 вы можете узнать точное количество имен следующим образом:
>>> len(dir(__builtins__))
152
С помощью вызова len() вы получаете количество элементов в списке, возвращаемое функцией dir(). Это возвращает 152 имени, которые включают исключения, функции, типы, специальные атрибуты и другие встроенные объекты Python.
Несмотря на то, что вы можете получить доступ ко всем этим встроенным объектам Python бесплатно (ничего не импортируя), вы также можете явно импортировать встроенные функции и получать доступ к именам, используя точечную нотацию. Вот как это работает:
>>> import builtins # Импортировать встроенные функции как обычный модуль
>>> dir(builtins)
['ArithmeticError', 'AssertionError',..., 'tuple', 'type', 'vars', 'zip']
>>> builtins.sum([1, 2, 3, 4, 5])
15
>>> builtins.max([1, 5, 8, 7, 3])
8
>>> builtins.sorted([1, 5, 8, 7, 3])
[1, 3, 5, 7, 8]
>>> builtins.pow(10, 2)
100
Вы можете импортировать встроенные модули, как и любой другой модуль Python. С этого момента вы можете получить доступ ко всем именам во встроенных функциях, используя поиск атрибутов с точками или полностью определенные имена. Это может быть весьма полезно, если вы хотите убедиться, что у вас не будет конфликта имен, если какое-либо из ваших глобальных имен переопределяет любое встроенное имя. Вы можете переопределить или переопределить любое встроенное имя в своей глобальной области видимости. Если вы это сделаете, имейте в виду, что это повлияет на весь ваш код. Взгляните на следующий пример:
>>> abs(-15) # Стандартное использование встроенной функции
15
>>> abs = 20 # Переопределить встроенное имя в глобальной области видимости
>>> abs(-15)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
</module></stdin>
Если вы переопределите или повторно назначите abs, то исходный встроенный abs() повлияет на весь ваш код. Теперь предположим, что вам нужно вызвать исходный abs(), но вы забыли, что переназначили имя. В этом случае, когда вы снова вызываете abs(), вы получите ошибку TypeError, потому что abs теперь содержит ссылку на целое число, которое нельзя вызвать.