Шаг 196.
Python: сборник рецептов. Модули и пакеты. Загрузка модулей с удаленного компьютера с использованием хуков импортирования. Обсуждение

    На этом шаге мы начнем обсуждение этого вопроса.

Обсуждение

    Перед детальным обсуждением этого рецепта стоит отметить, что модули, пакеты и механизм импорта Python - это одна из сложнейших частей всего языка. Часто даже самые опытные программисты плохо ее понимают - до тех пор, пока не вложат некоторое количество усилий, чтобы погрузиться в тему. Есть несколько важнейших документов, которые достойны прочтения, включая документацию к importlib и PEP 302. Эту документацию мы не будем повторять тут, но некоторые важнейшие моменты обсудим.

    Во-первых, если вы хотите создать новый объект модуля, то используете функцию imp.new_module(). Например:

>>> import imp
>>> m = imp.new_modu1e('spam')
>>> m
<modu1e 'spam'>
>>> m.__name__
'spam'
>>>

    Объекты модулей обычно имеют несколько ожидаемых атрибутов, включая __file__ (имя файла, из которого был загружен модуль) и __package__ (имя пакета, в котором находится модуль, если он есть).

    Во-вторых, модули кешируются интерпретатором. Закешированный модуль может быть найден в словаре sys.modules. Из-за этого кеширования распространенной практикой является объединение кеширования и создания модуля в один этап. Например:

>>> import sys
>>> import imp
>>> m = sys.modules.setdefault('spam', imp.new_module('spam'))
>>> m
<module 'spam'>
>>>

    Главная причина делать это в том, что если модуль с заданным именем уже существует, то вы вместо пересоздания получите уже созданный модуль. Например:

>>> import math
>>> m = sys.modu1es.setdefau1t('math', imp.new_modu1e('math'))
>>> m
<modu1e 'math' from '/usr/1oca1/1ib/python3.3/1ib-dyn1oad/math.so'>
>>> m.sin(2)
0.9092974268256817 
>>> m.cos(2)
-0.4161468365471424
>>>

    Поскольку создавать модули легко, то можно без труда написать простые функции, такие как load_module() из первой части этого рецепта. Недостаток этого подхода в том, что с его помощью трудно обрабатывать более сложные случаи, такие как импортирование пакетов. Чтобы работать с пакетами, вам придется заново реализовать большую часть "подкапотной" логики, которая является частью обычной инструкции import (например, проверку каталогов, поиск файлов __init__.py, выполнение этих файлов, установка путей и т. д.). Эта высокая сложность - одна из причин, из-за которых часто лучше расширить инструкцию import напрямую, а не определять собственную функцию.

    Расширение инструкции import выполняется прямолинейно, но вовлекает достаточно много "движущихся частей". На высшем уровне операции import обрабатываются списком поисковиков "метапутей", который вы можете найти в списке sys.metapath. Если вы выведете его значение, то увидите следующее:

>>> from pprint import pprint
>>> pprint(sys.meta_path)
[<class '_frozen_importlib.BuiltinImporter'>,
 <class '_frozen_importlib.FrozenImporter'>,
 <class '_frozen_importlib_external.PathFinder'>]
>>> 

    При выполнении инструкции типа import fib интерпретатор проходит по объектам-поисковикам из sys.meta_path и вызывает их метод find_module(), чтобы найти подходящий загрузчик модуля. Определите нижеприведенный класс и попробуйте поэкспериментировать с ним, чтобы разобраться в том, как это работает:

>>> class Finder:
	def find_module(self, fullname, path):
		print('Looking for', fullname, path)
		return None


>>> import sys
>>> sys.meta_path.insert(0, Finder()) # Вставить в начало
>>> import math
Looking for math None
>>> import types
Looking for types None
>>> import threading
Looking for threading None
Looking for time None
Looking for traceback None
Looking for linecache None
Looking for tokenize None
Looking for token None
>>>

    Обратите внимание на то, что метод find_module() запускается при каждом импортировании. Роль аргумента path здесь заключается в обработке пакетов. Когда пакеты импортированы, это список каталогов, которые находятся в атрибуте пакета __path__. Это пути, которые нужно проверить, чтобы найти подкомпоненты пакета. Например, посмотрите установки пути для xml.etree и xml.etree.ElementTree:

>>> import xml.etree.ElementTree 
Looking for xml None
Looking for xml.etree ['/usr/local/lib/python3.3/xml']
Looking for xml.etree.ElementTree ['/usr/local/lib/python3.3/xml/etree']
Looking for warnings None Looking for contextlib None
Looking for xml.etree.ElementPath ['/usr/local/lib/python3.3/xml/etree']
Looking for _elementtree None 
Looking for copy None 
Looking for org None Looking for pyexpat None 
Looking for ElementC14N None

    Положение поисковика в sys.meta_path является критически важным. Переместите его из начала в конец списка и попробуйте выполнить несколько операций импортирования:

>>> del sys.meta_path[0]
>>> sys.meta_path.append(Finder())
>>> import urllib.request 
>>> import datetime

    Теперь вы не видите никакого вывода, потому что импортирование обрабатывается другими записями из sys.meta_path. В этом случае вы увидите их только в том случае, если они включатся по причине попытки импортирования несуществующих модулей:

>>> import fib
Looking for fib None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'
>>> import xml.superfast
Looking for xml.superfast ['/usr/local/lib/python3.3/xml']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'xml.superfast'
>>>

    На следующем шаге мы продолжим это обсуждение.




Предыдущий шаг Содержание Следующий шаг