Все, что вы хотели узнать про области видимости в Python, но стеснялись спросить

  • Tutorial
В преддверии старта нового потока по курсу «Разработчик Python», решили поговорить про области видимости в Python. Что из этого вышло? — Читайте в материале ниже.




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

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

  • Локальная
  • Глобальная
  • Нелокальная

Последняя, нелокальная область видимости, была добавлена в Python 3.

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

Локальная область видимости


Рассмотрим функцию, которая выведет список some_list поэлементно:

def print_list(some_list):
    for element in some_list:
        print(element)

Здесь element и some_list – локальные переменные, которые видны только внутри функции, и которые не могут использоваться за ее пределами с теми значениями, которые были им присвоены внутри функции при ее работе. То есть, если мы в основном теле программы вызовем print(element), то получим ошибку:

NameError: name 'element' is not defined


Теперь мы поступим следующим образом:

def print_list(some_list):
    for element in some_list:
        print(element) 

element = 'q'
print_list([1, 2, 3])
print(element) 


И получим:

1
2
3
q

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

def print_list(some_list):
    for element in sudden_list:
        print(element) 

sudden_list = [0, 0, 0]
print_list([1, 2, 3])

Результат:

0
0
0

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

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

SUDDEN_LIST = [0, 0, 0]

def print_list(some_list):
    for element in SUDDEN_LIST:
        print(element) 

print_list([1, 2, 3]) 

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

Глобальная область видимости


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

candy = 5

def get_candy():
    global candy 
    candy += 1
    print('У меня {} конфет.'.format(candy))
    
get_candy()
get_candy()
print(candy)


В результате получим:


	У меня 6 конфет.
У меня 7 конфет.
7


Однако менять значение глобальной переменной изнутри функции – не лучшая практика и лучше так не делать, поскольку читаемости кода это не способствует. Чем меньше то, что происходит внутри функции будет зависеть от глобальной области видимости, тем лучше.

Лайфхак: Чтобы не мучиться с именованием переменных, вы можете вынести основной код программы в функцию main(), тогда все переменные, которые будут объявлены внутри этой функции останутся локальными и не будут портить глобальную область видимости, увеличивая вероятность допустить ошибку.

Нелокальная область видимости


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

def get_candy():
    candy = 5
    def increment_candy(): 
        nonlocal candy
        candy += 1
        return candy
    return increment_candy
    
result = get_candy()()
print('Всего {} конфет.'.format(result))

Результат:

Всего 6 конфет.

Насколько это полезно вам предстоит решить самостоятельно. Больше примеров вы можете найти здесь.

В качестве вывода можно сформулировать несколько правил:

  1. Изнутри функции видны переменные, которые были определены и внутри нее и снаружи. Переменные, определенные внутри – локальные, снаружи – глобальные.
  2. Снаружи функций не видны никакие переменные, определенные внутри них.
  3. Изнутри функции можно изменять значение переменных, которые определены в глобальной области видимости с помощью спецификатора global.
  4. Изнутри вложенной функции с помощью спецификатора nonlocal можно изменять значения переменных, которые были определены во внешней функции, но не находятся в глобальной области видимости.

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

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

Comments 19

    0

    Бы дополнить одной строкой про порядок подхватывания переменных (локальных и глобальных) при совпадающих именах.
    Такое бывает даже по независящим причинам (сторонние библиотеки).

      0

      Приведённый пример с константой, кажется сильно натянутым:


      SUDDEN_LIST = [0, 0, 0]
      
      def print_list(some_list):
          for element in SUDDEN_LIST:
              print(element) 
      
      print_list([1, 2, 3]) 

      Разве вы напишете такое в реальном коде? Функция с названием print_list, принимающая аргумент some_list и за каким-то макаром печатающая глобальную переменную?

        0
        Эти примеры используются для объяснения работы областей, не более того.
        0

        И может стоило бы привести ссылку на соотвествующий раздел документации: Execution model / Resolution of names.

          0

          В основном пишу на PHP, сейчас возникла неоходимость набросать небольшой скриптик на Python и столкнулся с непониманием. В Python, внутри класса нет собственной области видимости?


          file_name = 'something'
          
          class Updater:
              file_name = ''
          
              def __init__(self, file_name: str):
                  self.file_name = file_name
          
          updater = Updater(file_name)

          В этом примере переменная file_name внутри __init__ может быть взята сразу из глобального скоупа (без прокидования через аргументы)?
          PyCharm подсказывает мне: "Shadows name file_name from outer scope" (подсказка для аргумента в __init__). Это так непривычно для меня, т.е. я должен выдумывать другие имена аргументам, чтобы они не пересекались с перменными из глобального скоупа?

            0
            Правильней не выдумывать другие имена аргументам, а вообще избегать такого дизайна.
              0

              Можно, пожалуйста, конкретней?
              Представим другой пример: есть два класса (ClassA, ClassB). Для работы классу А необходимо наличие инстанса класса B. В PHP хорошей практикой является использование агрегации:


              class ClassA {
                  public $b;
              
                  public function __construct(ClassB $b)
                  {
                      $this->b = $b;
                  }
              }
              
              class ClassB {
                  public $name;
              
                  public function __construct(string $name)
                  {
                      $this->name = $name;
                  }
              }
              
              $b = new ClassB('test_name');
              $a = new ClassA($b);
              

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

                0
                Т.е. инициализация класса B происходит в клиентском коде, после чего через аргументы передается в консруктор класса А, где сохраняем инстанс в локальную переменную класса, все дальнейшее взаимодейтсвии происходит внутри локальной области видимости.
                Такой подход не применим в Python?

                Конечно применим. Именно так и происходит. Более того, если класс B является вспомогательным классом A, и более нигде не нужен, его можно определить прямо внутри класса.
              0
              В вашем примере намешано все подряд.
              file_name = 'something' # это глобальная переменная

              class Updater:
              file_name = '' # это переменная класса

              def __init__(self, file_name: str):
              self.file_name = file_name # это переменная экземпляра класса

              Переменная класса это совершенно не тоже самое, что переменная экземпляра класса. Она одинакова для всех экземпляров класса. Ее можно сравнить со статической переменной.

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

                При всем уважении, позволю себе не согласиться. Это не переменная класса, это свойство класса, которое может изменяться в ходе работы с классом. В моем, случае file_name = '' — это инициализация свойства (которое, можно и опустить вообще), дальше в __init__ я присваиваю значение этому свойству, полученное через аргумент конструктора.


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

                В моем случае у меня всего-то две плоскости:
                1) Глобальная область видимости, она же уровень клиентского кода.
                2) Классы, у которых должны быть собственные локальные области видимости.


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

                  +1
                  может изменяться в ходе работы с классом. В моем, случае file_name = '' — это инициализация свойства (которое, можно и опустить вообще), дальше в __init__ я присваиваю значение этому свойству, полученное через аргумент конструктора.

                  нет, вы не правы тут. К ней вы можете получить доступ через cls, а не self, в конструкторе вы ей ничего не присваиваете.
                    0
                    Вы может задумывали 2 плоскости, но в своем примере привели 3 плоскости.
                    вот попробуйте следующий пример:
                    class TestClass:
                    name = 'cls_name'
                    def __init__(self, s):
                    self.name = s

                    @classmethod
                    def get_cls_name(cls):
                    return cls.name

                    obj = TestClass('self_name')
                    print(obj.name)
                    print(TestClass.name)
                    print(obj.get_cls_name())

                    А потом заново переосмыслите области видимости классов в python. Можно называть их свойствами, можно переменными, суть от этого не изменится, то что в области класса и то что в области экземпляра класса — это разные вещи и не пересекаются никак.
                    +1
                    Речь о том, что глобальные переменные это вообще зло, и вы в своем примере это наглядно показали.
                      0

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


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


                      if __name__ == '__main__':
                          main()

                      Ну а определение всех свойст класса вынес в __init__, а не так, как в моем примере выше.

                  0
                  file_name — это переменная класса (обьекта-класса), в __ init __ вы имеете дело с уже с экземпляром, поэтому для доступа к переменной именно «класса» нужно написать
                  self.file_name = self.__class__.file_name
                  или
                  self.file_name = type(self).file_name

                  Просто имя file_name в аспекте начального значения работает по обычному для питона алгоритму вверх local -> nonlocal (вышестоящий обьект) -> global (уровень модуля)
                  «Определение класса» нельзя считать «вышестоящим обьектом»
                  0

                  Промазал. Deleted.

                    0
                    Чем бесполезнее статья, тем больше комментаторов
                      0

                      Стесняюсь спросить. Не реклама ли опять?

                      Only users with full accounts can post comments. Log in, please.