Область действия Python и правило LEGB: разрешение имён в коде

Оглавление

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

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

В этом уроке вы узнаете:

  • Что такое области видимости и как они работают в Python
  • Почему важно знать о области применения Python
  • Что такое правило LEGB и как Python использует его для разрешения имен
  • Как изменить стандартное поведение Python scope, используя global и nonlocal
  • Какие инструменты, связанные со сферой применения предлагает Python и как вы можете их использовать

Обладая этими знаниями, вы можете воспользоваться преимуществами Python scopes для написания более надежных и удобных в обслуживании программ. Использование Python scope поможет вам избежать или свести к минимуму ошибки, связанные с коллизией имен, а также неправильным использованием глобальных имен в ваших программах.

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

Понимание области применения

В программировании область действия имени определяет область программы, в которой вы можете однозначно получить доступ к этому имени, например, к переменным, функциям, объектам и так далее. Имя будет видно и доступно только коду в его области видимости. Некоторые языки программирования используют scope для предотвращения коллизий имен и непредсказуемого поведения. Чаще всего можно выделить две основные области видимости:

  1. Глобальная область действия: Имена, которые вы определяете в этой области, доступны для всего вашего кода.

  2. Локальная область: Имена, которые вы определяете в этой области, доступны или видны только коду в пределах этой области.

Область применения появилась потому, что ранние языки программирования (такие как BASIC) имели только глобальные имена. С таким названием любая часть программы могла в любой момент изменить любую переменную, поэтому обслуживание и отладка больших программ могли превратиться в настоящий кошмар. Чтобы работать с глобальными именами, вам нужно было бы помнить обо всем коде одновременно, чтобы в любой момент знать, каково значение данного имени. Это было важным побочным эффектом отсутствия областей видимости.

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

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

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

Имена и области видимости в Python

Поскольку Python является языком с динамической типизацией, переменные в Python появляются, когда вы впервые присваиваете им значение. С другой стороны, функции и классы доступны после того, как вы определите их с помощью def или class, соответственно. Наконец, модулей существуют после их импорта. В заключение, вы можете создать имена Python с помощью одной из следующих операций:

Операция Применение
Назначение x = value
Операции импорта import module or from module import name
Определения функций def my_func(): ...
Определения аргументов в контексте функций def my_func(arg1, arg2,... argN): ...
Определения классов class MyClass: ...

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

Примечание: Существует важное различие между операциями присваивания и операциями ссылки или доступа. Когда вы ссылаетесь на имя, вы просто извлекаете его содержимое или значение. Когда вы присваиваете имя, вы либо создаете это имя, либо изменяете его.

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

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

Область видимости Python против пространства имен

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

Имена на верхнем уровне модуля хранятся в пространстве имен модуля. Другими словами, они хранятся в атрибуте модуля .__dict__. Взгляните на следующий код:

>>> import sys
>>> sys.__dict__.keys()
dict_keys(['__name__', '__doc__', '__package__',..., 'argv', 'ps1', 'ps2'])

После импорта sys, вы можете использовать .keys() для проверки ключей sys.__dict__. Это возвращает список со всеми именами, определенными на верхнем уровне модуля. В этом случае вы можете сказать, что .__dict__ содержит пространство имен sys и является конкретным представлением области действия модуля.

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

В качестве еще одного примера предположим, что вам нужно использовать имя ps1, которое определено в sys. Если вы знаете, как .__dict__ и пространства имен работают в Python, то вы можете ссылаться на ps1 как минимум двумя различными способами:

  1. Используя точечное обозначение в названии модуля в форме module.name
  2. Используя операцию подписки на .__dict__ в форме module.__dict__['name']

Взгляните на следующий код:

>>> sys.ps1
'>>> '
>>> sys.__dict__['ps1']
'>>> '

После импорта sys вы можете получить доступ к ps1, используя точечное обозначение на sys. Вы также можете получить доступ к ps1, используя поиск по ключу в словаре с помощью ключа 'ps1'. Оба действия возвращают одинаковый результат, '>>> '.

Примечание: ps1 это строка указание основного запроса интерпретатора Python. ps1 определяется только в том случае, если интерпретатор находится в интерактивном режиме и его начальное значение равно '>>> '.

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

Использование правила LEGB для области видимости Python

Python разрешает имена, используя так называемое правило LEGB, которое названо в честь области видимости имен в Python. Буквы в алфавите LEGB означают "Локальный", "Включающий", "глобальный" и "встроенный". Вот краткий обзор того, что означают эти термины:

  • Локальная область видимости (или функция) - это блок кода или тело любой функции Python или lambda выражения. Эта область Python содержит имена, которые вы определяете внутри функции. Эти имена будут видны только из кода функции. Он создается при вызове функции, а не при определении функции, так что у вас будет столько же различных локальных областей, сколько и вызовов функций. Это верно, даже если вы вызываете одну и ту же функцию несколько раз или рекурсивно. Результатом каждого вызова будет создание новой локальной области видимости.

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

  • Глобальная область (или модуль) - это самая верхняя область в программе, скрипте или модуле на Python. Эта область Python содержит все имена, которые вы определяете на верхнем уровне программы или модуля. Имена в этой области Python видны из любого места вашего кода.

  • Встроенная область - это специальная область Python, которая создается или загружается всякий раз, когда вы запускаете скрипт или открываете интерактивный сеанс. Эта область содержит такие имена, как ключевые слова, функции, исключения и другие атрибуты, встроенные в Python. Имена в этой области Python также доступны из любого места вашего кода. Они автоматически загружаются Python при запуске программы или скрипта.

Правило LEGB - это своего рода процедура поиска имен, которая определяет порядок, в котором Python ищет имена. Например, если вы ссылаетесь на заданное имя, то Python будет последовательно искать это имя в локальной, включающей, глобальной и встроенной области видимости. Если имя существует, то вы получите первое его упоминание. В противном случае вы получите сообщение об ошибке.

Примечание: Обратите внимание, что поиск в локальной и включающей области Python выполняется только в том случае, если вы используете имя внутри функции (локальная область) или вложенной или внутренней функции (локальная и включающая область).

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

В любой момент времени во время выполнения у вас будет не более четырех активных областей Python — локальная, включающая, глобальная и встроенная - в зависимости от того, где вы находитесь в коде. С другой стороны, у вас всегда будут как минимум две активные области - глобальная и встроенная. Эти две области всегда будут доступны для вас.

Функции: Локальная область действия

Локальная область или область функции - это область Python, создаваемая при вызове функции. Каждый раз, когда вы вызываете функцию, вы также создаете новую локальную область. С другой стороны, вы можете рассматривать каждую инструкцию def и выражение lambda как схему для новых локальных областей. Эти локальные области будут появляться всякий раз, когда вы вызываете соответствующую функцию.

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

>>> def square(base):
...     result = base ** 2
...     print(f'The square of {base} is: {result}')
...
>>> square(10)
The square of 10 is: 100
>>> result  # Isn't accessible from outside square()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    result
NameError: name 'result' is not defined
>>> base  # Isn't accessible from outside 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

square() это функция, которая вычисляет квадрат заданного числа base. Когда вы вызываете функцию, Python создает локальную область видимости, содержащую имена base (аргумент) и result (локальная переменная). После первого вызова square(), base содержит значение 10, а result содержит значение 100. Во второй раз локальные имена не будут запоминать значения, которые были сохранены в них при первом вызове функции. Обратите внимание, что base теперь содержит значение 20, а result содержит 400.

Примечание: Если вы попытаетесь получить доступ к result или base после вызова функции, то получите 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():
...     # This block is the Local scope of outer_func()
...     var = 100  # A nonlocal var
...     # It's also the enclosing scope of inner_func()
...     def inner_func():
...         # This block is the Local scope of 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

Когда вы вызываете outer_func(), вы также создаете локальную область видимости. Локальная область видимости outer_func() в то же время является охватывающей областью видимости inner_func(). Изнутри inner_func() эта область не является ни глобальной, ни локальной. Это особая область, которая находится между этими двумя областями и известна как объемлющая область.

Примечание: В некотором смысле, inner_func() - это временная функция, которая запускается только во время выполнения своей заключающей функции, outer_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

Когда вы вызываете outer_func(), код выполняется до точки, в которой вы вызываете inner_func(). Последний оператор inner_func() пытается получить доступ к another_var. На данный момент another_var еще не определено, поэтому Python выдает NameError, потому что не может найти имя, которое вы пытаетесь использовать.

И последнее, но не менее важное: вы не можете изменять имена во вложенной области из вложенной функции, если только вы не объявите их как nonlocal во вложенной функции. Вы узнаете, как использовать nonlocal позже в этом руководстве.

Модули: Глобальная область применения

С того момента, как вы запускаете программу на Python, вы попадаете в глобальную область применения Python. Внутри Python преобразует основной скрипт вашей программы в модуль с именем __main__, который поддерживает выполнение основной программы. Пространство имен этого модуля является основной глобальной областью действия вашей программы.

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

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

>>> __name__
'__main__'

Всякий раз, когда вы запускаете программу на Python или интерактивный сеанс, как в приведенном выше коде, интерпретатор выполняет код в модуле или скрипте, который служит точкой входа в вашу программу. Этот модуль или скрипт загружается со специальным именем __main__. С этого момента вы можете сказать, что ваша основная глобальная область действия - это область __main__.

Чтобы проверить имена в вашей основной глобальной области видимости, вы можете использовать dir(). Если вы вызовете dir() без аргументов, то получите список имен, которые находятся в вашей текущей глобальной области видимости. Взгляните на этот код:

>>> dir()
['__annotations__', '__builtins__',..., '__package__', '__spec__']
>>> var = 100  # Assign var at the top level of __main__
>>> dir()
['__annotations__', '__builtins__',..., '__package__', '__spec__', 'var']

Когда вы вызываете dir() без аргументов, вы получаете список имен, доступных в вашей основной глобальной области Python. Обратите внимание, что если вы назначите новое имя (например, var здесь) на верхнем уровне модуля (то есть __main__ здесь), то это имя будет добавлено в список, возвращаемый с помощью dir().

Примечание: Более подробно dir() вы рассмотрите позже в этом руководстве.

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

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

>>> var = 100
>>> def func():
...     return var  # You can access var from inside func()
...
>>> func()
100
>>> var  # Remains unchanged
100

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

Всякий раз, когда вы присваиваете значение имени в Python, может произойти одна из двух вещей:

  1. Вы создаете новое имя
  2. Вы обновляете существующее имя

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

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

>>> var = 100  # A global variable
>>> def increment():
...     var = var + 1  # Try to update a global variable
...
>>> 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

В пределах increment() вы пытаетесь увеличить глобальную переменную var. Поскольку var не объявлено global внутри increment(), Python создает новую локальную переменную с тем же именем, var, внутри функции. В процессе работы Python понимает, что вы пытаетесь использовать локальный var перед его первым назначением (var + 1), поэтому возникает ошибка UnboundLocalError.

Вот еще один пример:

>>> var = 100  # A global variable
>>> def func():
...     print(var)  # Reference the global variable, var
...     var = 200   # Define a new local variable using the same name, 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

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

Примечание: Глобальные имена могут быть обновлены или изменены из любого места в вашей глобальной области Python. Кроме того, оператор global можно использовать для изменения глобальных имен практически в любом месте вашего кода, как вы увидите в Инструкции global.

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

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

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

  • Напишите автономные функции, которые используют локальные имена, а не глобальные.
  • Старайтесь использовать уникальные имена объектов, независимо от того, в какой области вы находитесь.
  • Избегайте глобальных изменений имен в ваших программах.
  • Избегайте изменений в названиях различных модулей.
  • Используйте глобальных имен в качестве констант, которые не изменяются во время выполнения вашей программы.

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

>>> # This area is the global or module scope
>>> number = 100
>>> def outer_func():
...     # This block is the local scope of outer_func()
...     # It's also the enclosing scope of inner_func()
...     def inner_func():
...         # This block is the local scope of inner_func()
...         print(number)
...
...     inner_func()
...
>>> outer_func()
100

Когда вы вызываете outer_func(), на вашем экране появляется 100. Но как Python ищет имя number в этом случае? Следуя правилу LEGB, вы будете искать number в следующих местах:

  1. Внутри inner_func(): Это локальная область видимости, но number там не существует.
  2. Внутри outer_func(): Это заключающая область, но number там тоже не определено.
  3. В области модуля: Это глобальная область, и там вы найдете number, чтобы вывести number на экран.

Если number не определен в глобальной области видимости, то Python продолжает поиск, просматривая встроенную область видимости. Это последний компонент правила LEGB, как вы увидите в следующем разделе.

builtins: Встроенная область видимости

Встроенная область - это специальная область Python, которая реализована в виде модуля стандартной библиотеки с именем builtins в Python 3.x. Все встроенные объекты Python находятся в этом модуле. Они автоматически загружаются во встроенную область видимости при запуске интерпретатора Python. Python выполняет поиск builtins последним в своем запросе LEGB, поэтому вы получаете все имена, которые он определяет, бесплатно. Это означает, что вы можете использовать их без импорта какого-либо модуля.

Обратите внимание, что имена в builtins всегда загружаются в вашу глобальную область видимости 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(), вы получаете количество элементов в list, возвращаемое функцией dir(). Это возвращает 152 имени, которые включают исключения, функции, типы, специальные атрибуты и другие встроенные объекты Python.

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

>>> import builtins  # Import builtins as a regular module
>>> 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

Вы можете импортировать builtins так же, как и любой другой модуль Python. С этого момента вы можете получить доступ ко всем именам в builtins, используя пунктирный поиск по атрибуту или полные имена. Это может быть весьма полезно, если вы хотите убедиться, что у вас не возникнет конфликта имен, если какое-либо из ваших глобальных имен переопределит какое-либо встроенное имя.

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

>>> abs(-15)  # Standard use of a built-in function
15
>>> abs = 20  # Redefine a built-in name in the global scope
>>> abs(-15)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

Если вы переопределите или повторно назначите abs, то исходная встроенная функция abs() будет затронута во всем вашем коде. Теперь предположим, что вам нужно вызвать исходное abs(), и вы забыли, что переназначили это имя. В этом случае, когда вы снова вызовете abs(), вы получите TypeError, потому что abs теперь содержит ссылку на целое число, которое не может быть вызвано.

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

Если вы экспериментируете с каким-либо кодом и случайно переназначаете встроенное имя в интерактивном приглашении, вы можете либо перезапустить сеанс, либо запустить del name, чтобы удалить переопределение из вашей глобальной области Python. Таким образом, вы восстанавливаете исходное имя во встроенной области видимости. Если вы вернетесь к примеру с abs(), то вы можете сделать что-то вроде этого:

>>> del abs  # Remove the redefined abs from your global scope
>>> abs(-15)  # Restore the original abs()
15

Когда вы удаляете пользовательское имя abs, вы удаляете его из своей глобальной области видимости. Это позволяет вам снова получить доступ к исходному abs() во встроенной области видимости.

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

>>> import builtins
>>> builtins.abs(-15)
15

Как только вы явно импортируете builtins, имя модуля становится доступным в вашей глобальной области Python. С этого момента вы можете использовать полные имена, чтобы однозначно получить нужные вам имена из builtins, точно так же, как вы сделали с builtins.abs() в приведенном выше примере.

В качестве краткого описания, некоторые из преимуществ Python scope показаны в следующей таблице:

Действие Глобальный код Локальный код Код вложенной функции
Доступ или ссылка на имена, находящиеся в глобальной области действия Да Да Да
Изменение или обновление имен, находящихся в глобальной области действия Да Нет (если не объявлено как global) Нет (если не объявлено глобальным)
Имена доступа или ссылки, находящиеся в локальной области действия Нет Да (своя локальная область действия), Нет (другая локальная область действия) Да (своя локальная область действия), Нет (другая локальная область действия)
Переопределение имен во встроенной области действия Да Да (во время выполнения функции) Да (во время выполнения функции)
Имена доступа или ссылки, находящиеся в окружающей их области действия область действия Н/Д Н/Д Да
Изменение или обновление имён, находящихся в их охватывающей области действия Н/Д Н/Д Нет (если не объявлено nonlocal)  

Кроме того, код в разных областях может использовать одно и то же имя для разных объектов. Таким образом, вы можете использовать локальную переменную с именем spam, а также глобальную переменную с таким же именем spam. Однако это считается плохой практикой программирования.

Изменение поведения области видимости Python

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

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

Несмотря на то, что области Python по умолчанию следуют этим общим правилам, существуют способы изменить это стандартное поведение. Python предоставляет два ключевых слова, которые позволяют изменять содержимое глобальных и нелокальных имен. Эти два ключевых слова:

  1. global
  2. nonlocal

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

Оператор global

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

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

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

>>> counter = 0  # A global name
>>> def update_counter():
...     counter = counter + 1  # Fail trying to update counter
...
>>> update_counter()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in update_counter
UnboundLocalError: local variable 'counter' referenced before assignment

Когда вы пытаетесь назначить counter внутри update_counter(), Python предполагает, что counter является локальным для update_counter() и выдает UnboundLocalError, потому что выпытаюсь получить доступ к имени, которое еще не определено.

Если вы хотите, чтобы этот код работал так, как вы ожидаете, то вы можете использовать инструкцию global следующим образом:

>>> counter = 0  # A global name
>>> def update_counter():
...     global counter  # Declare counter as global
...     counter = counter + 1  # Successfully update the counter
...
>>> update_counter()
>>> counter
1
>>> update_counter()
>>> counter
2
>>> update_counter()
>>> counter
3

В этой новой версии update_counter() вы добавляете оператор global counter в тело функции непосредственно перед попыткой изменить counter. С помощью этого небольшого изменения вы сопоставляете имя counter в области функции с тем же именем в глобальной области или области модуля. С этого момента вы можете свободно изменять counter внутри update_counter(). Все изменения будут отражены в глобальной переменной.

С помощью инструкции global counter вы указываете Python искать в глобальной области имя counter. Таким образом, выражение counter = counter + 1 не создает новое имя в области действия функции, а обновляет его в глобальной области.

Примечание: Использование global в целом считается плохой практикой. Если вы обнаружите, что используете global для устранения проблем, подобных описанной выше, то остановитесь и подумайте, есть ли лучший способ написать свой код.

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

>>> global_counter = 0  # A global name
>>> def update_counter(counter):
...     return counter + 1  # Rely on a local name
...
>>> global_counter = update_counter(global_counter)
>>> global_counter
1
>>> global_counter = update_counter(global_counter)
>>> global_counter
2
>>> global_counter = update_counter(global_counter)
>>> global_counter
3

Эта реализация update_counter() определяет counter в качестве параметра и возвращает его значение, увеличенное на единицу 1 при каждом вызове функции. Таким образом, результат update_counter() зависит от counter, который вы используете в качестве входных данных, а не от изменений, которые другие функции (или фрагменты кода) могут выполнять с глобальной переменной, global_counter.

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

>>> def create_lazy_name():
...     global lazy  # Create a global name, lazy
...     lazy = 100
...     return lazy
...
>>> create_lazy_name()
100
>>> lazy  # The name is now available in the global scope
100
>>> dir()
['__annotations__', '__builtins__',..., 'create_lazy_name', 'lazy']

Когда вы вызываете create_lazy_name(), вы также создаете глобальную переменную с именем lazy. Обратите внимание, что после вызова функции имя lazy доступно в глобальной области Python. Если вы проверите глобальное пространство имен с помощью dir(), то увидите, что lazy отображается последним в списке.

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

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

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

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

>>> name = 100
>>> dir()
['__annotations__', '__builtins__',..., '__spec__', 'name']
>>> global name
>>> dir()
['__annotations__', '__builtins__',..., '__spec__', 'name']

Использование оператора global, подобного global name, ничего не меняет в вашей текущей глобальной области видимости, как вы можете видеть в выходных данных dir(). Переменная name является глобальной переменной независимо от того, используете вы global или нет.

Оператор nonlocal

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

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

>>> def func():
...     var = 100  # A nonlocal variable
...     def nested():
...         nonlocal var  # Declare var as nonlocal
...         var += 100
...
...     nested()
...     print(var)
...
>>> func()
200

С помощью инструкции nonlocal var вы сообщаете Python, что будете изменять var внутри nested(). Затем вы увеличиваете var, используя операцию расширенного присваивания . Это изменение отражено в нелокальном имени var, которое теперь имеет значение 200.

В отличие от global, вы не можете использовать nonlocal вне вложенной или замкнутой функции. Чтобы быть более точным, вы не можете использовать оператор nonlocal ни в глобальной, ни в локальной области видимости. Вот пример:

>>> nonlocal my_var  # Try to use nonlocal in the global scope
  File "<stdin>", line 1
SyntaxError: nonlocal declaration not allowed at module level
>>> def func():
...     nonlocal var  # Try to use nonlocal in a local scope
...     print(var)
...
  File "<stdin>", line 2
SyntaxError: no binding for nonlocal 'var' found

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

Примечание: Для получения более подробной информации о nonlocal инструкции ознакомьтесь с PEP 3104 — Доступ к именам во внешних областях.

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

>>> def func():
...     def nested():
...         nonlocal lazy_var  # Try to create a nonlocal lazy name
...
  File "<stdin>", line 3
SyntaxError: no binding for nonlocal 'lazy_var' found

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

Использование замкнутых областей в качестве замыканий

Замыкания - это особый случай использования заключающей области Python. Когда вы обрабатываете вложенную функцию как данные, операторы, составляющие эту функцию, упаковываются вместе со средой, в которой они выполняются. Результирующий объект называется замыканием. Другими словами, замыкание - это внутренняя или вложенная функция, которая несет информацию о заключающей ее области, даже если эта область завершила свое выполнение.

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

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

>>> def power_factory(exp):
...     def power(base):
...         return base ** exp
...     return power
...
>>> square = power_factory(2)
>>> square(10)
100
>>> cube = power_factory(3)
>>> cube(10)
1000
>>> cube(5)
125
>>> square(15)
225

Ваша функция фабрики замыканий power_factory() принимает аргумент с именем exp. Вы можете использовать эту функцию для создания замыканий, которые выполняют различные операции с питанием. Это работает, потому что каждый вызов power_factory() получает свой собственный набор информации о состоянии. Другими словами, он получает значение для exp.

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

В приведенном выше примере внутренней функции power() сначала присваивается значение square. В этом случае функция запоминает, что exp равно 2. Во втором примере вы вызываете power_factory(), используя 3 в качестве аргумента. Таким образом, cube содержит функциональный объект, который запоминает, что exp равно 3. Обратите внимание, что вы можете свободно повторно использовать square и cube, поскольку они не забывают информацию о своем соответствующем состоянии.

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

>>> def mean():
...     sample = []
...     def _mean(number):
...         sample.append(number)
...         return sum(sample) / len(sample)
...     return _mean
...
>>> current_mean = mean()
>>> current_mean(10)
10.0
>>> current_mean(15)
12.5
>>> current_mean(12)
12.333333333333334
>>> current_mean(11)
12.0
>>> current_mean(13)
12.2

Замыкание, которое вы создаете в приведенном выше коде, запоминает информацию о состоянии sample между вызовами current_mean. Таким образом, вы можете решить проблему элегантным и питоническим способом.

Примечание: Если вы хотите узнать больше о областях и замыканиях, ознакомьтесь с видеокурсом Изучение областей и замыканий в Python.

Обратите внимание, что если ваш поток данных становится слишком большим, то эта функция может стать проблемой с точки зрения использования памяти. Это связано с тем, что с каждым вызовом current_mean, sample будет содержаться все больший и больший список значений. Взгляните на следующий код для альтернативной реализации с использованием nonlocal:

>>> def mean():
...     total = 0
...     length = 0
...     def _mean(number):
...         nonlocal total, length
...         total += number
...         length += 1
...         return total / length
...     return _mean
...
>>> current_mean = mean()
>>> current_mean(10)
10.0
>>> current_mean(15)
12.5
>>> current_mean(12)
12.333333333333334
>>> current_mean(11)
12.0
>>> current_mean(13)
12.2

Несмотря на то, что это решение более подробное, у вас больше нет бесконечно растущего списка. Теперь у вас есть одно значение для total и length. Эта реализация намного эффективнее с точки зрения потребления памяти, чем предыдущее решение.

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

>>> from functools import partial
>>> def power(exp, base):
...     return base ** exp
...
>>> square = partial(power, 2)
>>> square(10)
100

Вы используете partial для создания функционального объекта, который запоминает информацию о состоянии, где exp=2. Затем вы вызываете этот объект для выполнения операции включения и получения конечного результата.

Перенос имен в область видимости с помощью import

Когда вы пишете программу на Python, вы обычно разделяете код на несколько модулей. Чтобы ваша программа работала, вам нужно перенести названия этих отдельных модулей в ваш модуль __main__. Для этого вам нужно import явно указать модули или имена. Это единственный способ, которым вы можете использовать эти имена в своей основной глобальной области Python.

Взгляните на следующий код для примера того, что происходит при импорте некоторых стандартных модулей и имен:

>>> dir()
['__annotations__', '__builtins__',..., '__spec__']
>>> import sys
>>> dir()
['__annotations__', '__builtins__',..., '__spec__', 'sys']
>>> import os
>>> dir()
['__annotations__', '__builtins__',..., '__spec__', 'os', 'sys']
>>> from functools import partial
>>> dir()
['__annotations__', '__builtins__',..., '__spec__', 'os', 'partial', 'sys']

Сначала вы импортируете sys и os из стандартной библиотеки Python. Вызвав dir() без аргументов, вы можете увидеть, что эти модули теперь доступны вам как имена в вашей текущей глобальной области видимости. Таким образом, вы можете использовать точечную нотацию, чтобы получить доступ к именам, которые определены в sys и os.

Примечание: Если вы хотите глубже разобраться в том, как работает импорт в Python, ознакомьтесь с Абсолютный и относительный импорт в Python.

В последней операции import используется форма from <module> import <name>. Таким образом, вы можете использовать импортированное имя непосредственно в своем коде. Другими словами, вам не нужно явно использовать точечную запись.

Открытие необычных областей применения Python

Вы найдете некоторые структуры Python, в которых разрешение имен, по-видимому, не соответствует правилу LEGB для областей Python. К таким структурам относятся:

В следующих нескольких разделах вы узнаете, как Python scope работает с этими тремя структурами. Обладая этими знаниями, вы сможете избежать незначительных ошибок, связанных с использованием имен в такого рода структурах Python.

Область применения переменных понимания

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

Пояснения состоят из пары квадратных скобок ([]) или фигурных скобок ({}), содержащих выражение, за которыми следует одно или более предложений for, а затем ноль или единица if пункта в соответствии с for пунктом.

Предложение for в понятии работает аналогично традиционному for loop. Переменная цикла в понятии является локальной для структуры. Проверьте следующий код:

>>> [item for item in range(5)]
[0, 1, 2, 3, 4]
>>> item  # Try to access the comprehension variable
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    item
NameError: name 'item' is not defined

Как только вы запустите проверку списка, переменная item будет забыта, и вы больше не сможете получить доступ к ее значению. Маловероятно, что вам нужно использовать эту переменную за пределами понимания, но, несмотря на это, Python гарантирует, что ее значение больше не будет доступно после завершения понимания.

Обратите внимание, что это относится только к понятиям. Когда дело доходит до обычных циклов for, переменная loop содержит последнее значение, обработанное циклом:

>>> for item in range(5):
...     print(item)
...
0
1
2
3
4
>>> item  # Access the loop variable
4

Вы можете свободно обращаться к переменной цикла item после завершения цикла. Здесь переменная цикла содержит последнее значение, обработанное циклом, которое в данном примере равно 4.

Область действия исключительных переменных

Еще один нетипичный случай Python scope, с которым вы столкнетесь, - это случай переменной exception. Переменная exception - это переменная, которая содержит ссылку на исключение, вызванное оператором try. В Python 3.x такие переменные являются локальными для блока except и забываются при завершении блока. Ознакомьтесь со следующим кодом:

>>> lst = [1, 2, 3]
>>> try:
...     lst[4]
... except IndexError as err:
...     # The variable err is local to this block
...     # Here you can do anything with err
...     print(err)
...
list index out of range
>>> err # Is out of scope
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    err
NameError: name 'err' is not defined

err содержит ссылку на исключение, вызванное предложением try. Вы можете использовать err только внутри блока кода предложения except. Таким образом, вы можете сказать, что область видимости Python для переменной exception является локальной для блока кода except. Также обратите внимание, что если вы попытаетесь получить доступ к err из-за пределов блока except, то получите NameError. Это потому, что как только блок except заканчивается, имя больше не существует.

Чтобы обойти это поведение, вы можете определить вспомогательную переменную из инструкции try, а затем назначить исключение этой переменной внутри блока except. Посмотрите на следующий пример:

>>> lst = [1, 2, 3]
>>> ex = None
>>> try:
...     lst[4]
... except IndexError as err:
...     ex = err
...     print(err)
...
list index out of range
>>> err  # Is out of scope
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'err' is not defined
>>> ex  # Holds a reference to the exception
list index out of range

Вы используете ex в качестве вспомогательной переменной для хранения ссылки на исключение, вызванное предложением try. Это может быть полезно, когда вам нужно что-то сделать с объектом exception после завершения блока кода. Обратите внимание, что если исключение не возникает, то ex остается None.

Область действия атрибутов класса и экземпляра

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

В отличие от функций, локальная область видимости класса создается не во время вызова, а во время выполнения. Каждый объект класса имеет свой собственный атрибут .__dict__, который содержит область действия класса или пространство имен, в котором находятся все атрибуты класса . Ознакомьтесь с этим кодом:

>>> class A:
...     attr = 100
...
>>> A.__dict__.keys()
dict_keys(['__module__', 'attr', '__dict__', '__weakref__', '__doc__'])

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

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

>>> class A:
...     attr = 100
...     print(attr)  # Access class attributes directly
...
100
>>> A.attr  # Access a class attribute from outside the class
100
>>> attr  # Isn't defined outside A
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    attr
NameError: name 'attr' is not defined

Внутри локальной области видимости A вы можете напрямую обращаться к атрибутам класса, точно так же, как вы это делали в инструкции print(attr). Чтобы получить доступ к любому атрибуту класса после выполнения блока кода класса, вам нужно будет использовать обозначение точкой или ссылку на атрибут, как вы это делали с A.attr. В противном случае вы получите NameError, потому что атрибут attr является локальным для класса block.

С другой стороны, если вы попытаетесь получить доступ к атрибуту, который не определен внутри класса, то получите сообщение AttributeError. Посмотрите на следующий пример:

>>> A.undefined  # Try to access an undefined class attribute
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    A.undefined
AttributeError: type object 'A' has no attribute 'undefined'

В этом примере вы пытаетесь получить доступ к атрибуту undefined. Поскольку этот атрибут не существует в A, вы получаете сообщение AttributeError, сообщающее вам, что A не имеет атрибута с именем undefined.

Вы также можете получить доступ к любому атрибуту класса, используя экземпляр класса следующим образом:

>>> obj = A()
>>> obj.attr
100

Когда у вас есть экземпляр, вы можете получить доступ к атрибутам класса, используя точечную запись, как вы это делали здесь с obj.attr. Атрибуты класса специфичны для объекта class, но вы можете получить к ним доступ из любых экземпляров класса. Стоит отметить, что атрибуты класса являются общими для всех экземпляров класса. Если вы измените атрибут класса, то изменения будут видны во всех экземплярах класса.

Примечание: Думайте о точечной записи так, как если бы вы говорили Python: “Найдите атрибут с именем attr в obj. Если ты найдешь его, то верни мне”.

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

Чтобы создать, обновить или получить доступ к любому атрибуту экземпляра внутри класса, вам необходимо использовать self вместе с точечной нотацией. Здесь self - это специальный атрибут, представляющий текущий экземпляр. С другой стороны, чтобы обновить любой атрибут экземпляра или получить к нему доступ из-за пределов класса, вам необходимо создать экземпляр и затем использовать точечную запись. Вот как это работает:

>>> class A:
...     def __init__(self, var):
...         self.var = var  # Create a new instance attribute
...         self.var *= 2  # Update the instance attribute
...
>>> obj = A(100)
>>> obj.__dict__
{'var': 200}
>>> obj.var
200

Класс A принимает аргумент с именем var, который автоматически удваивается внутри .__init__() с помощью операции присваивания self.var *= 2. Обратите внимание, что при проверке .__dict__ на obj вы получаете словарь, содержащий все атрибуты экземпляра. В этом случае словарь содержит только имя var, значение которого теперь равно 200.

Примечание: Подробнее о том, как работают классы в Python, читайте в статье Введение в объектно-ориентированное программирование на Python.

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

>>> class A:
...     def __init__(self, var):
...         self.var = var
...
...     def duplicate_var(self):
...         return self.var * 2
...
>>> obj = A(100)
>>> obj.var
100
>>> obj.duplicate_var()
200
>>> A.var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    A.var
AttributeError: type object 'A' has no attribute 'var'

Здесь вы изменяете A, чтобы добавить новый метод с именем duplicate_var(). Затем вы создаете экземпляр A, передавая 100 в инициализатор класса. После этого теперь вы можете вызвать duplicate_var() в obj, чтобы продублировать значение, сохраненное в self.var. Наконец, если вы попытаетесь получить доступ к var, используя объект class вместо экземпляра, вы получите AttributeError, поскольку доступ к атрибутам экземпляра невозможен с помощью объектов class.

В общем, когда вы пишете объектно-ориентированный код на Python и пытаетесь получить доступ к атрибуту, ваша программа выполняет следующие шаги:

  1. Сначала проверьте экземпляр локальную область или пространство имен.
  2. Если атрибут там не найден, то проверьте класс локальную область видимости или пространство имен.
  3. Если имя также не существует в пространстве имен класса, то вы получите сообщение AttributeError.

Это базовый механизм, с помощью которого Python разрешает имена в классах и экземплярах.

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

>>> class A:
...     var = 100
...     def print_var(self):
...         print(var)  # Try to access a class attribute directly
...
>>> A().print_var()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    A().print_var()
  File "<stdin>", line 4, in print_var
    print(var)
NameError: name 'var' is not defined

Поскольку классы не создают замкнутую область для методов, вы не можете получить доступ к var непосредственно из print_var(), как вы пытаетесь сделать здесь. Чтобы получить доступ к атрибутам класса из любого метода, вам нужно использовать точечную запись. Чтобы устранить проблему в этом примере, измените оператор print(var) внутри print_var() на print(A.var) и посмотрите, что произойдет.

Вы можете переопределить атрибут class атрибутом instance, который изменит общее поведение вашего класса. Однако вы можете получить однозначный доступ к обоим атрибутам, используя точечную запись, как в следующем примере:

>>> class A:
...     var = 100
...     def __init__(self):
...         self.var = 200
...
...     def access_attr(self):
...         # Use dot notation to access class and instance attributes
...         print(f'The instance attribute is: {self.var}')
...         print(f'The class attribute is: {A.var}')
...
>>> obj = A()
>>> obj.access_attr()
The instance attribute is: 200
The class attribute is: 100
>>> A.var  # Access class attributes
100
>>> A().var # Access instance attributes
200
>>> A.__dict__.keys()
dict_keys(['__module__', 'var', '__init__',..., '__getattribute__'])
>>> A().__dict__.keys()
dict_keys(['var'])

У указанного выше класса есть атрибут экземпляра и атрибут класса с одинаковыми именами var. Вы можете использовать следующий код для доступа к каждому из них:

  1. Пример: Используйте self.var для доступа к этому атрибуту.
  2. Класс: Используйте A.var для доступа к этому атрибуту.

Поскольку в обоих случаях используется точечная запись, проблем с коллизией имен не возникает.

Примечание: В целом, хорошие практики ООП рекомендуют не заменять атрибуты класса атрибутами экземпляра, которые имеют разные обязанности или выполняют разные действия. Это может привести к появлению малозаметных и труднодоступных ошибок.

Наконец, обратите внимание, что класс .__dict__ и экземпляр .__dict__ являются совершенно разными и независимыми словарями. Вот почему атрибуты класса доступны сразу после запуска или импорта модуля, в котором был определен класс. Напротив, атрибуты экземпляра вступают в силу только после создания объекта или экземпляра.

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

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

globals()

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

>>> globals()
{'__name__': '__main__',..., '__builtins__': <module 'builtins' (built-in)>}
>>> my_var = 100
>>> globals()
{'__name__': '__main__',..., 'my_var': 100}

Первый вызов globals() возвращает словарь, содержащий имена из вашего __main__ модуля или программы. Обратите внимание, что когда вы присваиваете новое имя на верхнем уровне модуля, как в my_var = 100, это имя добавляется в словарь, возвращаемый globals().

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

 1# Filename: dispatch.py
 2
 3from sys import platform
 4
 5def linux_print():
 6    print('Printing from Linux...')
 7
 8def win32_print():
 9    print('Printing from Windows...')
10
11def darwin_print():
12    print('Printing from macOS...')
13
14printer = globals()[platform + '_print']
15
16printer()

Если вы запустите этот скрипт в командной строке, то получите результат, который будет зависеть от вашей текущей платформы.

Другим примером использования globals() может быть проверка списка специальных имен в глобальной области видимости. Взгляните на следующий список для понимания:

>>> [name for name in globals() if name.startswith('__')]
['__name__', '__doc__', '__package__',..., '__annotations__', '__builtins__']

При таком понимании списка будет возвращен список со всеми специальными именами, которые определены в вашей текущей глобальной области видимости Python. Обратите внимание, что вы можете использовать словарь globals() так же, как вы бы использовали любой обычный словарь. Например, вы можете выполнить итерацию по нему, используя следующие традиционные методы:

  • .keys()
  • .values()
  • .items()

Вы также можете выполнять обычные операции с подпиской на globals(), используя квадратные скобки, как в globals()['name']. Например, вы можете изменить содержимое globals(), даже если это не рекомендуется. Взгляните на этот пример:

>>> globals()['__doc__'] = """Docstring for __main__."""
>>> __doc__
'Docstring for __main__.'

Здесь вы меняете ключ __doc__ на строку документации на __main__, так что с этого момента строка документации основного модуля будет иметь следующий вид: ценность 'Docstring for __main__.'.

locals()

Другой функцией, связанной с областью видимости и пространствами имен Python, является locals(). Эта функция обновляет и возвращает словарь, содержащий копию текущего состояния локальной области или пространства имен Python. Когда вы вызываете locals() в функциональном блоке, вы получаете все имена, присвоенные в локальной области или области функций, вплоть до точки, в которой вы вызываете locals(). Вот пример:

>>> def func(arg):
...     var = 100
...     print(locals())
...     another = 200
...
>>> func(300)
{'var': 100, 'arg': 300}

Всякий раз, когда вы вызываете locals() внутри func(), результирующий словарь содержит имя var, сопоставленное со значением 100, и arg, сопоставленное с 300. Поскольку locals() захватывает только имена, назначенные перед вызовом, another отсутствует в словаре.

Если вы вызовете locals() в глобальной области видимости Python, то вы получите тот же словарь, который вы получили бы, если бы вызвали globals():

>>> locals()
{'__name__': '__main__',..., '__builtins__': <module 'builtins' (built-in)>}
>>> locals() is globals()
True

Когда вы вызываете locals() в глобальной области видимости Python, вы получаете словарь, идентичный словарю, возвращаемому вызовом globals().

Обратите внимание, что вам не следует изменять содержимое locals(), поскольку изменения могут не повлиять на значения локальных и свободных имен. Ознакомьтесь со следующим примером:

>>> def func():
...     var = 100
...     locals()['var'] = 200
...     print(var)
...
>>> func()
100

Когда вы пытаетесь изменить содержимое var с помощью locals(), это изменение не отражается на значении var. Итак, вы можете сказать, что locals() полезен только для операций чтения, поскольку обновления словаря locals игнорируются Python.

vars()

vars() это встроенная функция Python, которая возвращает атрибут .__dict__ модуля, класса, экземпляра или любого другого объекта, который имеет атрибут словаря. Помните, что .__dict__ - это специальный словарь, который Python использует для реализации пространств имен. Взгляните на следующие примеры:

>>> import sys
>>> vars(sys) # With a module object
{'__name__': 'sys',..., 'ps1': '>>> ', 'ps2': '... '}
>>> vars(sys) is sys.__dict__
True
>>> class MyClass:
...     def __init__(self, var):
...         self.var = var
...
>>> obj = MyClass(100)
>>> vars(obj)  # With a user-defined object
{'var': 100}
>>> vars(MyClass)  # With a class
mappingproxy({'__module__': '__main__',..., '__doc__': None})

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

Без каких-либо аргументов vars() действует как locals() и возвращает словарь со всеми именами в локальной области видимости Python:

>>> vars()
{'__name__': '__main__',..., '__builtins__': <module 'builtins' (built-in)>}
>>> vars() is locals()
True

Здесь вы вызываете vars() на верхнем уровне интерактивного сеанса. Без аргументов этот вызов возвращает словарь, содержащий все имена в глобальной области Python. Обратите внимание, что на этом уровне vars() и locals() возвращают один и тот же словарь.

Если вы вызываете vars() с объектом, у которого нет .__dict__, то вы получите TypeError, как в следующем примере:

>>> vars(10)  # Call vars() with objects that don't have a .__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute

Если вы вызовете vars() с помощью объекта integer, то получите TypeError потому что у этого типа объектов Python нет .__dict__.

dir()

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

>>> dir()  # With no arguments
['__annotations__', '__builtins__',..., '__package__', '__spec__']
>>> dir(zip)  # With a function object
['__class__', '__delattr__',..., '__str__', '__subclasshook__']
>>> import sys
>>> dir(sys)  # With a module object
['__displayhook__', '__doc__',..., 'version_info', 'warnoptions']
>>> var = 100
>>> dir(var)  # With an integer variable
['__abs__', '__add__',..., 'imag', 'numerator', 'real', 'to_bytes']

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

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

>>> def func():
...     var = 100
...     print(dir())
...     another = 200  # Is defined after calling dir()
...
>>> func()
['var']

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

Заключение

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

Теперь вы можете:

  • Воспользуйтесь преимуществами Python scope, чтобы избежать или свести к минимуму ошибки, связанные с коллизией имен
  • Эффективно используйте глобальные и локальные имена в своих программах, чтобы улучшить удобство сопровождения кода
  • Используйте согласованную стратегию для доступа, изменения или обновления имен во всем вашем коде на Python

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

Back to Top