9.1 Створення циклічних залежностей модуля
Припустимо, у вас є два файли, a.py
та b.py
, кожен з яких
імпортує інший наступним чином:
У a.py
:
import b
def f():
return b.x
print(f())
У b.py
:
import a
x = 1
def g():
print(a.f())
Спочатку спробуємо імпортувати a.py
:
import a
# 1
Спробувалося просто чудово. Можливо, це вас дивує. Зрештою, модулі циклічно імпортують один одного, і це, ймовірно, має бути проблемою, чи не так?
Відповідь така, що просте існування циклічного імпорту модулів саме по собі не є проблемою в Python. Якщо модуль вже був імпортований, Python досить розумний, щоб не намагатися повторно імпортувати його. Однак, залежно від того, в який момент кожен модуль намагається отримати доступ до функцій або змінних, визначених в іншому, ви дійсно можете зіткнутися з проблемами.
Отже, повертаючись до нашого прикладу, коли ми імпортували a.py
, у нього не було проблем з імпортом b.py
, оскільки b.py
не вимагає, щоб щось із a.py
було визначено під час його імпорту. Єдине посилання в b.py
на a
— це виклик a.f()
. Але цей виклик в g()
, і нічого в a.py
або b.py
не викликає g()
. Тож все працює прекрасно.
Але що станеться, якщо ми спробуємо імпортувати b.py
(без попереднього імпорту a.py
, тобто):
import b
Traceback (most recent call last):
File "<stdin>", line 1, in
File "b.py", line 1, in
import a
File "a.py", line 6, in
print(f())
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'
Проблема тут у тому, що в процесі імпорту b.py
він намагається імпортувати a.py
, який, у свою чергу, викликає f()
, який намагається отримати доступ до b.x
. Але b.x
ще не було визначено. Звідси виняток AttributeError
.
Принаймні, одне з рішень цієї проблеми досить тривіальне. Просто змініть b.py
, щоб імпортувати a.py
в g()
:
x = 1
def g():
import a # Це буде оцінено лише тоді, коли g() буде викликано
print(a.f())
Тепер, коли ми його імпортуємо, все добре:
import b
b.g()
# 1 Виведено вперше, оскільки модуль 'a' викликає 'print(f())' в кінці
# 1 Виведено вдруге, це наш виклик до 'g()'
9.2 Перетин імен з іменами модулів стандартної бібліотеки Python
Одна з переваг Python — це безліч модулів, які надаються «з коробки». Але в результаті, якщо ви свідомо не будете за цим стежити, можна зіткнутися з тим, що ім'я вашого модуля може збігтися з ім'ям модуля зі стандартної бібліотеки, що поставляється з Python (наприклад, у вашому коді може бути модуль з ім'ям email.py
, який буде конфліктувати з модулем стандартної бібліотеки з таким же ім'ям).
Це може призвести до серйозних проблем. Наприклад, якщо який-небудь з модулів буде намагатися імпортувати версію модуля зі стандартної бібліотеки Python, а у вас в проекті буде модуль з таким же ім'ям, він помилково імпортує ваш модуль замість модуля зі стандартної бібліотеки.
Тому слід проявляти обережність, щоб не використовувати ті ж самі імена, що й у модулях стандартної бібліотеки Python. Значно простіше змінити назву модуля в своєму проекті, ніж подавати запит на зміну імені модуля у стандартній бібліотеці і чекати його затвердження.
9.3 Видимість виключень
Розглянемо наступний файл main.py
:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int("1"))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
Наче все правильно, код повинен працювати, давайте подивимося, що він нам виведе:
$ python main.py 1
Traceback (most recent call last):
File "C:\Projects\Python\TinderBolt\main.py", line 19, in <module>
bad()
File "C:\Projects\Python\TinderBolt\main.py", line 17, in bad
print(e)
^
UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
Що тут щойно сталося? Проблема в тому, що в Python об'єкт у блоці виключення недоступний за його межами (причина цього полягає в тому, що в іншому випадку об'єкти в цьому блоці зберігалися б у пам'яті доти, поки збирач сміття не запуститься і не видалить посилання на них).
Один із способів уникнути цієї проблеми — зберегти посилання на об'єкт блоку виключення за межами цього блоку, щоб він залишався доступним. Ось версія попереднього прикладу, яка використовує цю техніку, тим самим роблячи код робочим:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int("1"))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
9.4 Неправильне використання методу __del__
Коли інтерпретатор видаляє об'єкт, він перевіряє — чи є в цього об'єкта функція __del__
, і якщо є, то викликає її перед видаленням об'єкта. Це дуже зручно, коли ви хочете, щоб ваш об'єкт підчистив за собою якісь зовнішні ресурси або кеш.
Припустимо, у вас є ось такий файл mod.py
:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
І ви намагаєтеся зробити ось таке з іншого файлу another_mod.py
:
import mod
mybar = mod.Bar()
І отримуєте жахливий AttributeError
.
Чому? Тому що, як повідомляється тут, коли інтерпретатор завершує роботу, всі глобальні змінні модуля мають значення None
. В результаті в наведеному вище прикладі, в момент виклику __del__
, ім'я foo
вже було встановлено в None
.
Рішенням цієї «задачі із зірочкою» буде використання спеціального методу atexit.register()
. Таким чином, коли ваша програма завершить виконання (тобто при нормальному виході з неї), ваші handle'и видаляються до того, як інтерпретатор завершить роботу.
З урахуванням цього виправлення для наведеного вище коду mod.py
може виглядати приблизно так:
import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
Подібна реалізація забезпечує простий і надійний спосіб виклику будь-якого необхідного очищення після звичайного завершення програми. Очевидно, що рішення про те, як вчинити з об'єктом, який пов'язаний з ім'ям self.myhandle
, залишається за foo.cleanup
, але, думаю, ідею ви зрозуміли.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ