Концепция области видимости определяет, как переменные и имена ищутся в вашем коде. Она определяет видимость переменной в коде. Область видимости имени или переменной зависит от того места в коде, где вы создаете эту переменную. Концепция области видимости в Python обычно представлена ​​с использованием правила, известного как правило LEGB. Буквы в аббревиатуре LEGB обозначают локальную, вложенную, глобальную и встроенную (Local, Enclosing, Global и Built-in Scope) области. В итого определяются не только уровни областей действия, но и последовательность шагов поиска имен при исполнении программы Python.

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

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

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

Содержание    ↑

Общие сведения об области видимости    ↑

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

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

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

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

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

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

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

Имена и области в 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 использует для хранения имен. Они хранятся в специальном атрибуте .__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 Scope    ↑

Python разрешает имена с помощью так называемого правила LEGB, которое названо в честь области Python для имен. Буквы в LEGB (Local, Enclosing, Global и Built-in) обозначают локальный, вложенный, глобальный и встроенный. Вот краткий обзор того, что означают эти термины:

  • Локальная (или функция) область видимости (Local) — это блок кода или тело любой функции Python или лямбда-выражения. Эта область Python содержит имена, которые вы определяете внутри функции. Эти имена будут видны только из кода функции. Он создается при вызове функции, а не при ее определении, поэтому у вас будет столько же различных локальных областей, сколько и при вызовах функций. Это верно, даже если вы вызываете одну и ту же функцию несколько раз или рекурсивно.Каждый вызов приведет к созданию новой локальной области.
  • Вложенная (или нелокальная) область видимости (Enclosing) — это особая область видимости, которая существует только для вложенных функций. Если локальная область видимости является внутренней или вложенной функцией, тогда вложенная область видимости является областью внешней или вложенной функции. Эта область содержит имена, которые вы определяете во вложенной функции. Имена в охватывающей области видны из кода внутренних и включающих функций.
  • Глобальная (или модульная) область видимости (Global) — это самая верхняя область в программе, скрипте или модуле Python. Эта область Python содержит все имена, которые вы определяете на верхнем уровне программы или модуля.Имена в этой области Python видны повсюду в вашем коде.
  • Встроенная область видимости (Built-in) — это специальная область видимости Python, которая создается или загружается всякий раз, когда вы запускаете скрипт или открываете интерактивный сеанс. Эта область содержит имена, такие как ключевые слова, функции, исключения и другие атрибуты, встроенные в Python. Имена в этой области Python также доступны повсюду в вашем коде. Он автоматически загружается Python при запуске программы или сценария.

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

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

Таким образом, когда вы используете вложенные функции, имена разрешаются путем первой проверки локальной области видимости или локальной области видимости самой внутренней функции. Затем 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 "", line 1, in 
    result
NameError: name 'result' is not defined
>>> base  # Недоступен снаружи square()
Traceback (most recent call last):
  File "", line 1, in 
    base
NameError: name 'base' is not defined
>>> square(20)
The square of 20 is: 400

square() — это функция, которая вычисляет квадрат заданного числа по основанию. Когда вы вызываете функцию, Python создает локальную область видимости, содержащую базу имен (аргумент) и результат (локальную переменную). После первого вызова square() base содержит значение 10, а result — значение 100. Во второй разлокальные имена не будут запоминать значения, которые были сохранены в них при первом вызове функции. Обратите внимание, что база теперь содержит значение 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 "", line 1, in 
NameError: name 'inner_func' is not defined

Когда вы вызываете 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 "", line 1, in 
    outer_func()
  File "", line 7, in outer_func
    inner_func()
  File "", 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

Когда вы вызываете external_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  # Назначьте var на верхнем уровне __main__
>>> dir()
['__annotations__', '__builtins__',..., '__package__', '__spec__', 'var']

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

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

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

>>> var = 100
>>> def func():
...     return var  # Вы можете получить доступ к var из func()
...
>>> func()
100
>>> var  # Остается неизменной
100

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

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

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

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

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

>>> var = 100  # Глобальная переменная
>>> def increment():
...     var = var + 1  # Попробуйте обновить глобальную переменную
...
>>> increment()
Traceback (most recent call last):
  File "", line 1, in 
    increment()
  File "", line 2, in increment
    var = var + 1
UnboundLocalError: local variable 'var' referenced before assignment

Внутри 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 "", line 1, in 
    func()
  File "", line 2, in func
    print(var)
UnboundLocalError: local variable 'var' referenced before assignment

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

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

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

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

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

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

К этому моменту вы рассмотрели три области действия Python. В следующем примере показано, где они расположены в вашем коде и как 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 "", line 1, in 
TypeError: 'int' object is not callable

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

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

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

>>> del abs  # Удалите переопределенный абс из вашей глобальной области видимости
>>> abs(-15) # Восстановить исходный abs()
15

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

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

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

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

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

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

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

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

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

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

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

  1. global
  2. nonlocal

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

Оператор global    ↑

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

>>> counter = 0  # Глобальное имя
>>> def update_counter():
...     counter = counter + 1  # Не удалось обновить счетчик
...
>>> update_counter()
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in update_counter
UnboundLocalError: local variable 'counter' referenced before assignment

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

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

>>> counter = 0  # Глобальное имя
>>> def update_counter():
...     global counter  # Объявить счетчик как глобальный
...     counter = counter + 1  # Успешно обновить счетчик
...
>>> update_counter()
>>> counter
1
>>> update_counter()
>>> counter
2
>>> update_counter()
>>> counter
3

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

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

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

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

>>> global_counter = 0  # Глобальное имя
>>> 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() определяет счетчик как параметр и возвращает его значение, увеличенное на 1 единицу при каждом вызове функции. Таким образом, результат update_counter() зависит от счетчика, который вы используете в качестве входных данных, а не от изменений, которые другие функции (или фрагменты кода) могут выполнять с глобальной переменной global_counter.

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

>>> def create_lazy_name():
...     global lazy  # Создайте глобальное имя, ленивое
...     lazy = 100
...     return lazy
...
>>> create_lazy_name()
100
>>> lazy  # Имя теперь доступно в глобальной области видимости
100
>>> dir()
['__annotations__', '__builtins__',..., 'create_lazy_name', 'lazy']

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

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

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

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

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

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

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

Оператор nonlocal    ↑

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

>>> def func():
...     var = 100  # Нелокальная переменная
...     def nested():
...         nonlocal var  # Объявить var нелокальным
...         var += 100
...
...     nested()
...     print(var)
...
>>> func()
200

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

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

>>> nonlocal my_var  # Попробуйте использовать нелокальный объект в глобальной области видимости
  File "", line 1
SyntaxError: nonlocal declaration not allowed at module level
>>> def func():
...     nonlocal var  # Попробуйте использовать нелокальный объект в локальной области видимости
...     print(var)
...
  File "", line 2
SyntaxError: no binding for nonlocal 'var' found

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

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

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

>>> def func():
...     def nested():
...         nonlocal lazy_var  # Попробуйте создать нелокальное ленивое имя
...
  File "", 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

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

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

>>> 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

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

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

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

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

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

Когда вы пишете программу Python, вы обычно разбиваете код на несколько модулей. Чтобы ваша программа работала, вам необходимо перенести имена этих отдельных модулей в ваш модуль __main__. Для этого вам необходимо явно импортировать модули или имена. Это единственный способ использовать эти имена в основной глобальной области 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 . Таким образом, вы можете использовать импортированное имя прямо в коде. Другими словами, вам не нужно явно использовать точечную нотацию.

Обнаружение необычных областей видимости    ↑

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

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

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

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

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

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

>>> [item for item in range(5)]
[0, 1, 2, 3, 4]
>>> item  # Попробуйте получить доступ к переменной включения
Traceback (most recent call last):
  File "", line 1, in 
    item
NameError: name 'item' is not defined

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

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

>>> for item in range(5):
...     print(item)
...
0
1
2
3
4
>>> item  # Доступ к переменной цикла
4

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

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

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

>>> lst = [1, 2, 3]
>>> try:
...     lst[4]
... except IndexError as err:
...     # Переменная err является локальной для этого блока
...     # Здесь вы можете делать что угодно с помощью err
...     print(err)
...
list index out of range
>>> err # Выходит за рамки
Traceback (most recent call last):
  File "", line 1, in 
    err
NameError: name 'err' is not defined

err содержит ссылку на исключение, вызванное предложением try. Вы можете использовать err только внутри блока кода предложения except. Таким образом, вы можете сказать, что область Python для переменной исключения является локальной по отношению к блоку кода 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  # Выходит за рамки
Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'err' is not defined
>>> ex  # Содержит ссылку на исключение
list index out of range

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

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

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

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

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

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

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

>>> class A:
...     attr = 100
...     print(attr)  # Доступ к атрибутам класса напрямую
...
100
>>> A.attr  # Доступ к атрибуту класса извне класса
100
>>> attr  # Не определен вне A
Traceback (most recent call last):
  File "", line 1, in 
    attr
NameError: name 'attr' is not defined

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

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

>>> A.undefined  # Пытаться получить доступ к неопределенному атрибуту класса
Traceback (most recent call last):
  File "", line 1, in 
    A.undefined
AttributeError: type object 'A' has no attribute 'undefined'

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

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

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

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

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

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

>>> class A:
...     def __init__(self, var):
...         self.var = var  # Создать новый атрибут экземпляра
...         self.var *= 2   # Обновить атрибут экземпляра
...
>>> obj = A(100)
>>> obj.__dict__
{'var': 200}
>>> obj.var
200

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

Примечание. Чтобы узнать больше о том, как классы работают в Python, ознакомьтесь с Объектно‑ориентированное программирование в Python 3.

Несмотря на то, что вы можете создавать атрибуты экземпляра в любом методе класса, рекомендуется создавать и инициализировать их внутри .__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 "", line 1, in 
    A.var
AttributeError: type object 'A' has no attribute 'var'

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

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

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

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

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

>>> class A:
...     var = 100
...     def print_var(self):
...         print(var)  # Попытка получить доступ к атрибуту класса напрямую
...
>>> A().print_var()
Traceback (most recent call last):
  File "", line 1, in 
    A().print_var()
  File "", line 4, in print_var
    print(var)
NameError: name 'var' is not defined

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

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

>>> class A:
...     var = 100
...     def __init__(self):
...         self.var = 200
...
...     def access_attr(self):
...         # Используйте точечную нотацию для доступа к атрибутам класса и экземпляра
...         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   # Доступ к атрибутам класса
100
>>> A().var # Доступ к атрибутам экземпляра
200
>>> A.__dict__.keys()
dict_keys(['__module__', 'var', '__init__',..., '__getattribute__'])
>>> A().__dict__.keys()
dict_keys(['var'])

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

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

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

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

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

Использование встроенных функций, связанных с областью видимости    ↑

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

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

global()    ↑

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

>>> globals()
{'__name__': '__main__',..., '__builtins__': }
>>> my_var = 100
>>> globals()
{'__name__': '__main__',..., 'my_var': 100}

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

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

# Filename: dispatch.py

from sys import platform

def linux_print():
    print('Printing from Linux...')

def win32_print():
    print('Printing from Windows...')

def darwin_print():
    print('Printing from macOS...')

printer = globals()[platform + '_print']

printer()

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

Другой пример использования 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() только захватывает имена, назначенные перед его вызовом, другого нет в словаре.

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

>>> locals()
{'__name__': '__main__',..., '__builtins__': }
>>> 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) # С модульным объектом
{'__name__': 'sys',..., 'ps1': '>>> ', 'ps2': '... '}
>>> vars(sys) is sys.__dict__
True
>>> class MyClass:
...     def __init__(self, var):
...         self.var = var
...
>>> obj = MyClass(100)
>>> vars(obj)  # С пользовательским объектом
{'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__': }
>>> vars() is locals()
True

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

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

>>> vars(10)  # Вызвать vars() с объектами, у которых нет .__dict__
Traceback (most recent call last):
  File "", line 1, in 
TypeError: vars() argument must have __dict__ attribute

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

dir()    ↑

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

>>> dir()  # Без аргументов
['__annotations__', '__builtins__',..., '__package__', '__spec__']
>>> dir(zip)  # С функциональным объектом
['__class__', '__delattr__',..., '__str__', '__subclasshook__']
>>> import sys
>>> dir(sys)  # С модульным объектом
['__displayhook__', '__doc__',..., 'version_info', 'warnoptions']
>>> var = 100
>>> dir(var)  # С целочисленной переменной
['__abs__', '__add__',..., 'imag', 'numerator', 'real', 'to_bytes']

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

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

>>> def func():
...     var = 100
...     print(dir())
...     another = 200  # определено после вызова dir()
...
>>> func()
['var']

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

Заключение    ↑

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

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

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

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

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

По мотивам Python Scope & the LEGB Rule: Resolving Names in Your Code

Опубликовано Вадим В. Костерин

ст. преп. кафедры ЦЭиИТ. Автор более 130 научных и учебно-методических работ. Лауреат ВДНХ (серебряная медаль).

Оставьте комментарий

Ваш адрес email не будет опубликован.