Перейти до змісту

Простори імен

Спочатку трошки теорії:

Простір імен, також про́стір назв або іменний про́стір (англ. namespace) — концепція у програмуванні, призначена для розмежування різних множин ідентифікаторів і попередження конфліктів між їхніми іменами

Якщо трошки простіше, то простір імен — це сукупність визначених в певний момент імен і інформації про сутності, на які вони посилаються. Можна розглядати такий простір як словник, у якому ключі є іменами, а значення — самими сутностями.

Простори імен в Python

В Python розрізяють 4 простори імен:

  • Local — локальний (всередині функції)
  • Enclosing — охоплюючий (локальний простір функцій-обгорток, які у свою чергу містять інші функції)
  • Global — глобальний (основний модуль)
  • Built-in — вбудований (зарезервовані значення Python)

Простори імен мають різні життєві цикли. По мірі виконання програми інтерпретатор Python створює необхідні простори імен і видаляє їх коли потреба в них зникає.

Built-in

Вбудований простір імен містить імена усіх вбудованих об'єктів Python. Наприклад функції len() та print(), типи даних int та list. Коли інтерпретатор запускається він автоматично створює вбудований простір імен і знищує його по завершенні своєї роботи.

Global

Глобальний простір імен містить імена, які визначаються на рівні основної програми (основного модуля програми) або імпортуються в основний модуль з інших модулів. Створюється одразу ж після завантаження основного модуля програми і існує до завершення роботи інтерпретатора.

Вбудована функція globals() повертає словник, який містить усі імена з глобального простору і відповідні їм об'єкти. Давайте подивимось які імена вже є у цьому просторі одразу після запуска інтерпретатора:

>>> type(globals())
<class 'dict'>
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001F0441E0AF0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> globals().keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__'])
>>>

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

Додамо ще одне ім'я у простір:

>>> my_variable = 'text'
>>> globals().keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'my_variable'])
>>>

І одразу усі імена з модуля random:

>>> from random import *
>>> globals().keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'my_variable', 'Random', 'seed', 'random', 'uniform', 'randint', 'choice', 'sample', 'randrange', 'shuffle', 'normalvariate', 'lognormvariate', 'expovariate', 'vonmisesvariate', 'gammavariate', 'triangular', 'gauss', 'betavariate', 'paretovariate', 'weibullvariate', 'getstate', 'setstate', 'getrandbits', 'choices', 'SystemRandom'])
>>>

Функція globals() повертає посилання на активний словник з іменами і значеннями.
Отже через цей словник можна додавати в глобальний простір імен нові імена і відповідні їм значення:

>>> globals()['new_var'] = 'Hi!'
>>> globals().keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'my_variable', 'Random', 'seed', 'random', 'uniform', 'randint', 'choice', 'sample', 'randrange', 'shuffle', 'normalvariate', 'lognormvariate', 'expovariate', 'vonmisesvariate', 'gammavariate', 'triangular', 'gauss', 'betavariate', 'paretovariate', 'weibullvariate', 'getstate', 'setstate', 'getrandbits', 'choices', 'SystemRandom', 'new_var'])
>>>

Останнє є еквівалентним наступному:

new_var = 'Hi!'

Local

Кожного разу при виклику функції інтерпретатор створює новий простір імен. Цей простір є локальним для функції і існує до момента повернення з функції. При черговому виклику функції ця процедура повторюється.

Імена, які ініціалізуються всередині функції, попадають у локальний простір імен.

Аналогічно функції global() вбудована функція local() повертає словник з іменами з локального простору імен і відповідними значеннями:

>>> def add_two(arg):
...     var = 2
...     print(locals().keys())
...     return arg + var
...
>>> add_two(1)
dict_keys(['arg', 'var'])
3
>>>

Вище у локальному просторі імен функції add_two міститься два імені. Змінна arg отримує значення при передачі аргумента під час виклику функції. Змінній var значення присвоюється під час виконання функції.

Зауважте: на відміну від globals() функція locals() повертає не активний словник, а його копію. Отже змінити через цей словинк локальний простір імен не вийде. Ця відмінність між двома функціями може завдати клопоту у майбутньому, отже добре запам'ятайте її!

Якщо функцію locals() викликати поза межами функції, в основному модулі програми, то її поведінка буде така сама, як у функції globals()

Enclosing

Коли ми оголошуємо функцію, в її тілі ми можемо використовувати будь-які інструкції, у тому числі і оголошення іншої функції.

>>> def outer():
...     print('Start outer()')
...     def inner():
...         print('Start inner()')
...         print('End inner()')
...         return
...     inner()
...     print('End outer()')
...     return
...
>>> outer()
Start outer()
Start inner()
End inner()
End outer()
>>>

У наведеному прикладі всередині функції outer() оголошується функція inner(). Функція inner() є локальною по відношенню до outer(). У свою чергу outer() є охоплючою функцією для inner().

Коли основна програма викликає функцію outer(), Python створює для неї новий простір імен. Аналогічно коли outer() викликає функцію inner(), остання отримує свій власний окремий простір імен. Простір, створений для функції inner(), є локальним, а простір, створений для функції outer() — охоплюючим.

Обидва простори існують до повернення з відповідної їм функції.

Область видимості

Наявність декількох окремих просторів імен означає, що в процесі виконання програми одночасно можуть існувати однакові імена, кожне у своєму просторі. Але як же інтерпретатор розрізняє яке саме ім'я ви маєте на увазі в певний момент часу, тобто у якому просторі імен його шукати?

В Python є правило LEGB, яким він користується при пошуці змінних. Якщо всередині функції виконується звернення до змінної, Python шукає її ім'я по просторам імен у такому порядку (до першого співпадіння):

Local — Enclosing — Global — Built-in

Якщо ж ім'я не буде знайдено в жодному просторі імен, інтерпретатор видасть помилку.

Розглянемо декілька прикладів. У наступному ім'я len буде знайдено в локальному просторі імен функції inner():

>>> len = 'global'
>>> def outer():
...     len = 'enclosing'
...     def inner():
...         len = 'local'
...         print(len)
...     inner()
...
>>> outer()
local
>>>

Якщо ім'я len не буде визначено у функції inner(), тоді його буде знайдено у просторі імен enclosing:

>>> len = 'global'
>>> def outer():
...     len = 'enclosing'
...     def inner():
...         print(len)
...     inner()
...
>>> outer()
enclosing
>>>

Якщо ж ім'я len не буде визначено і в функції outer(), його буде знайдено у просторі імен global:

>>> len = 'global'
>>> def outer():
...     def inner():
...         print(len)
...     inner()
...
>>> outer()
global
>>>

Ну і якщо не визначено ніде в нашій програмі, тоді знайдеться в builtins:

>>> def outer():
...     def inner():
...         print(len)
...     inner()
...
>>> outer()
<built-in function len>
>>>

А ось ім'я unknown немає ніде:

>>> def outer():
...     def inner():
...         print(unknown)
...     inner()
...
>>> outer()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in outer
  File "<stdin>", line 3, in inner
NameError: name 'unknown' is not defined
>>>

Модифікація об'єктів поза областю видимості

Якщо всередині функції змінній присвоїти значення, то така змінна автоматично вважається локальною, тобто ім'я попадає в локальний простір імен. А що, якщо нам знадобиться присвоїти значення імені з іншого простору?

Інструкція global

Існує можливість змінити значення глобальної змінної за допомогою інструкції 'global':

>>> def function():
...     global x
...     x = 'global assigned inside function'
...     print (x)
...
>>> function()
global assigned inside function
>>> x
'global assigned inside function'
>>>

Інструкція global змушує інтерпретатор починати пошук імен з глобального простору і дозволяє присвоювати змінним нові значення. Область пошуку простирається аж до простору імен Built-in якщо потрібне ім'я не буде знайдено у модулі, при цьому операція присвоєння значень глобальним іменам завжди буде створювати або змінювати змінні в глобальному просторі імен.

Можна оголошувати одразу декілька глобальних імен:

global a, b, text

Ім'я оголошене як глобальне не може використовуватись раніше самого оголошення:

>>> def function():
...     x = 'global assigned inside function'
...     global x
...     print (x)
...
  File "<stdin>", line 3
SyntaxError: name 'x' is assigned to before global declaration
>>>

Параметри функцій не можуть бути глобальними:

>>> def f(arg):
...     global arg
...     arg = 7
...
  File "<stdin>", line 2
SyntaxError: name 'arg' is parameter and global
>>>

Інструкція nonlocal

>>> def outer():
...     x = "x assigned inside outer"
...     def inner():
...         x = "x assigned inside inner"
...         print("inner():", x)
...     inner()
...     print("outer():", x)
...
>>> outer()
inner(): x assigned inside inner
outer(): x assigned inside outer
>>>

Функції outer() та inner() кожна мають свої локальні змінні x.

Всередині вкладеної функції ми маємо можливість вказати, що певна змінна не є локальною для цієї вкладеної функції, а є локальною для функції-обгортки. Робиться це за допомогою інструкції nonlocal:

>>> def outer():
...     x = "x assigned inside outer"
...     def inner():
...         nonlocal x
...         x = "x assigned inside inner"
...         print("inner():", x)
...     inner()
...     print("outer():", x)
...
>>> outer()
inner(): x assigned inside inner
outer(): x assigned inside inner
>>>

Всередині функції inner() ми вказали, що змінна x є локальною для функції outer(). Тобто змінна x як би попадає з функції outer() в простір імен функції inner(). Якщо ми змінемо значення нелокальної змінної, це відобразиться і на локальній змінній.

nonlocal обмежує пошук простором імен функцій-обгорток. В область пошуку не входять глобальний та вбудований простори імен.

Інструкція nonlocal вимагає, щоб перераховані імена вже існували:

>>> def outer():
...     x = "x assigned inside outer"
...     def inner():
...         nonlocal x, unknown
...         x = "x assigned inside inner"
...         print("inner():", x)
...     inner()
...     print("outer():", x)
...
  File "<stdin>", line 4
SyntaxError: no binding for nonlocal 'unknown' found
>>>

Кращі практики

Не дивлячись на те, що Python надає можливість міняти об'єкти поза межами функції за допомогою global і nonlocal у більшості випадків це не є хорошою практикою. І це стосується не лише Python, а взагалі багатьох мов програмування.

Зміна глобальних об'єктів всередині функції — це свого роду "побічний ефект" функції. Основні недоліки використання глобальних змінних:

  • дуже важко відстежувати де саме відбувається доступ до глобальних об'єктів
  • виникають помилки, які дуже важко локалізувати
  • зменшується читабельність початкового кода
  • з'являється багато "непередбачуваних" зв'язків у системі, що призводить до непередбачливої її поведінки а також затруднює її розвиток і масштабування
  • затруднюється модульне тестування

Бувають ситуації, коли модифікація глобальних змінних сильно спрощує код, але таке зустрічається рідко. Замість того існують більш "безпечні" практики.

Серед розробників є таке "заклинання":

глобальні змінні — це зло!

Резюме

  • в Python є чотири простори імен: вбудований, глобальний, охоплюючий і локальний
  • правило LEGB: пошук імен відбувається від локальної до вбудованої.
  • при використанні і операції присвоєння ім'я вважається локальним. Цю поведінку можна змінити за допомогою операторів global та nonlocal.

Додаткові матеріали

Back to top