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, але, думаю, ідею ви зрозуміли.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ