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, але, думаю, ідею ви зрозуміли.

undefined
1
Опрос
Ітератори,  11 уровень,  8 лекция
недоступен
Ітератори
Ітератори