Простори імен
Спочатку трошки теорії:
Простір імен, також про́стір назв або іменний про́стір (англ. 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
.