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 # This will be evaluated only when g() is called
print(a.f())
Теперь, когда мы его импортируем, все нормально:
import b
b.g()
# 1 Printed a first time since module 'a' calls 'print(f())' at the end
# 1 Printed a second time, this one is our call to '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, но, думаю, идею вы поняли.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ