在每个代码文件夹中都都需要定义 __init__.py 文件。
导入模块下的文件时,会先导入该模块下的 __init__.py 文件。
__init__.py 一般是空的。能用来自动加载子模块。
当使用 from module import * 语句时,从该模块导出的内容进行精确控制。
在模块中定义一个 __all__ 变量来制定会被导出的内容。
from module import * 语句一般在定义了大量变量名的模块中使用。这样会导入所有不易下划线开头的对象。如果该模块定义了 __all__,就只会导出列举出的内容。__all__ 为空时,没有东西被导出,__all__ 包含未定义的名字时,导入时会引起 AttributeError。
文件结构:
mypackage/
__init__.py
A/
__init__.py
spam.py
grok.py
B/
__init__.py
bar.py
模块 spam 想导入 grok 模块,相对路径和绝对路径导入都可以:
# mypackage/A/spam.py
from mypackage.A import grok
from . import grok
from ..B import bar
使用绝对路径导入的坏处是,会将顶层包名硬编码到代码中,重新组织的时候代码很难工作。
相对导入不能定义到包的目录之外。相对导入在顶层脚本的模块将不起作用,包的部分不能作为脚本直接执行。但是使用 python 的 -m 选项,相对导入会正确运行。
# mymodule.py
class A:
def spam(self):
print('A.spam')
class B(A):
def bar(self):
print('B.bar')
类似上述的模块想分成两个文件,并且使用到这个模块的代码不被破坏。
把 mymodule 变成一个目录:
mymodule/
__init__.py
a.py
b.py
# a.py
class A:
def spam(self):
print('A.spam')
# b.py
from .a import A
class B(A):
def bar(self):
print('B.bar')
# __init__.py
from .a import A
from .b import B
这样操作以后,mymodule 包将作为一个单一的逻辑模块。
如果一个包的文件众多,在使用时就需要用到大量的 import 语句:
from mymodule.a import A
from mymodule.b import B
这么写需要知道不同部分的位置,不如使用一条 import 容易。
from mymodule import A, B
一种方法是让 mymodule 成为一个大的源文件,第二种就是这一节的粘合方法。
对于很大的模块,如果只想在被用到时才被加载,__init__.py 文件需要改动:
def A():
from .a import A
return A()
def B():
from .b import B
return B()
这样实现延迟加载。主要缺点是继承和类型检查不能使用 mymodule.A,需要使用 mymodule.a.A 才行。
分散的代码各自是文件目录,想用共同的包前缀吧所有组件连接而不需要一个个地安装。
删掉顶级目录中创造共同命名空间的 __init__.py 文件,在导入包时,解释器会创建一个由所有包含匹配包名的目录组成的列表。该包名的 __path__ 变量中有这个目录列表的副本。
一个包如果没有 __file__ 属性,那这个包是一个包命名空间。
使用 importlib.reload()。替代之前的 imp.reload()。
>>> import pandas as pd
>>> import imp
__main__:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
>>> import importlib
>>> importlib.reload(pd)
<module 'pandas' from'C:\\Users\\PLAYER\\AppData\\Local\\Programs\\Python\\Python37\\lib\\site-packages\\pandas\\__init__.py'>
reload() 只更新所指的模块,而使用 from … import … 从该模块导入的内容并未被更新。
应用程序的目录添加 __main__.py 文件。在顶级目录运行 python,加该目录名,解释器会把 __main__.py 作为主文件执行。
将该目录下的 py 文件打包为一个 zip文件后也能用 python解释器运行这个压缩文件。
使用内置的 I/O 功能如 open() 读取数据文件的问题:
pkgutil.get_data() 函数是用来读取数据文件的工具,不论包安装在哪里,都能照常将文件内容以字节字符串返回。第一个参数是包含包名的字符串,能使用包名或 __package__ 特殊变量。第二个参数是数据文件的相对名称。
想添加新目录到 python 的路径,但是不想用硬链接。
使用 PYTHONPATH 环境变量添加:
>>> env PYTHONPATH=/some/dir:/other/dir python3
创建 .pth 文件,把目录列在里面:
# myapplication.pth
/some/dir
/other/dir
代码能在任何目录,只要目录在 .pth 文件中就行。但是这个文件必须要放在指定 python 的 site-packages 目录。
在代码中手动改 sys.path 的值也能实现功能,但是这么做会将目录名硬编码到代码源,当代码移动时,就需要维护。最好是不修改代码的情况下在其他地方进行 path 配置。
import sys
sys.path.insert(0, '/some/dir')
# 将脚本目录的src加到path里
from os.path import abspath, join, dirname
sys.path.insert(0, abspath(dirname('__file__'), 'src'))
importlib.import_module() 能手动导入字符串表示的模块,返回生成的模块对象,使用参数将其接收后作为正常模块使用。
import importlib
math = importlib.import_module('math')
print(math.sin(2))
可以传入 package 参数用于相对导入。
可以导包内的模块,但不能直接导模块内的函数。
从远程机器上导入模块。
创建如下代码结构:
testcode/
spam.py
fib.py
grok/
__init__.py
blah.py
# spam.py
print("I'm spam")
def hello(name):
print(f'Hello {name}')
# fib.py
print("I'm fib")
def fib(n):
if n < 2:
return 1
else:
return fib(n-1) + fib(n-2)
# grok/__init__.py
print("I'm grok.__init__.py")
# grok/blah.py
print("I'm grok.blah")
在 testcode 目录中启动 python server:
python -m http.server 5000
然后启动单独的 python 解释器,访问远程文件:
>>> from urllib.request import rlopen
>>> u = urlopen('http://localhost:15000/fob.py')
>>> data = u.read().decode('utf-8')
创建显式的加载函数自动加载远程模块
import imp
import urllib.request
import sys
def load_module(url):
# 下载源代码
u = urllib.request.urlopen(url)
source = u.read().decode('utf-8')
# 编译为可执行代码对象
code = compile(source, url, 'exec')
# 在新建的模块字典中执行
mod = sys.modules.setdefault(url, imp.new_module(url))
mod.__file__ = url
mod.__package__ = ''
exec(code, mod.__dict__)
return mod
这样直接访问只支持简单的模块,没有嵌入到 import 语句中。
想支持更高级的结构,需要创建自定义导入器。
方法1:创建元路径导入器
# urlimport.py
import sys
import importlib.abc
import imp
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from html.parser import HTMLParser
# Debugging
import logging
log = logging.getLogger(__name__)
# Get links from a given URL
def _get_links(url):
class LinkParser(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag == 'a':
attrs = dict(attrs)
links.add(attrs.get('href').rstrip('/'))
links = set()
try:
print(f'Getting links from {url}')
u = urlopen(url)
parser = LinkParser()
parser.feed(u.read().decode('utf-8'))
except Exception as e:
print('Could not get links.', e)
return links
class UrlMetaFinder(importlib.abc.MetaPathFinder):
def __init__(self, baseurl):
self._baseurl = baseurl
self._links = {}
self._loaders = {baseurl: UrlModuleLoader(baseurl)}
def find_module(self, fullname, path=None):
print(f'find_module: fullname={fullname}, path={path}')
if path is None:
baseurl = self._baseurl
else:
if not path[0].startswith(self._baseurl):
return None
baseurl = path[0]
parts = fullname.split('.')
basename = parts[-1]
print(f'find_module: baseurl={baseurl}, basename={basename}')
# Check link cache
if basename not in self._links:
print(baseurl)
self._links[baseurl] = _get_links(baseurl)
# Check if it's a package
if basename in self._links[baseurl]:
print(f'find_module: trying package {fullname}')
fullurl = self._baseurl + '/' + basename
# Attempt to load the package (which access __init__.py)
loader = UrlPackageLoader(fullurl)
try:
loader.load_module(fullname)
self._links[fullurl] = _get_links(fullurl)
self._loaders[fullurl] = UrlModuleLoader(fullurl)
print(f'find_module: package {fullname} loaded')
except ImportError as e:
print('find_module: package failed.', e)
loader = None
return loader
# A normal module
filename = basename + '.py'
if filename in self._links[baseurl]:
print(f'find_module: module {fullname} found')
return self._loaders[baseurl]
else:
print(f'find_module: module {fullname} not found')
return None
def invalidate_caches(self):
print('invalidating link cache')
self._links.clear()
# Module Loader for a URL
class UrlModuleLoader(importlib.abc.SourceLoader):
def __init__(self, baseurl):
self._baseurl = baseurl
self._source_cache = {}
def module_repr(self, module):
return f'{module.__name__} from {module.__file__}>'
# Required method
def load_module(self, fullname):
code = self.get_code(fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = self.get_filename(fullname)
mod.__loader__ = self
mod.__package__ = fullname.rpartition('.')[0]
exec(code, mod.__dict__)
return mod
# Optional extensions
def get_code(self, fullname):
src = self.get_source(fullname)
return compile(src, self.get_filename(fullname), 'exec')
def get_data(self, path):
pass
def get_filename(self, fullname):
return self._baseurl + '/' + fullname.split('.')[-1] + '.py'
def get_source(self, fullname):
filename = self.get_filename(fullname)
print(f'loader: reading {filename}')
if filename in self._source_cache:
print(f'loader: cached {filename}')
return self._source_cache[filename]
try:
u = urlopen(filename)
source = u.read().decode('utf-8')
print(f'loader: {filename} loaded')
self._source_cache[filename] = source
return source
except (HTTPError, URLError) as e:
print(f'loader: {filename} failed. {e}')
raise ImportError(f"Can't load {filename}")
def is_package(self, fullname):
return False
# Package loader for a URL
class UrlPackageLoader(UrlModuleLoader):
def load_module(self, fullname):
mod = super().load_module(fullname)
mod.__path__ = [self._baseurl]
mod.__package__ = fullname
def get_filename(self, fullname):
return self._baseurl + '/' + '__init__.py'
def is_package(self, fullname):
return True
# Utility functions for installing/unistalling the loader
_installed_meta_cache = {}
def install_meta(address):
if address not in _installed_meta_cache:
finder = UrlMetaFinder(address)
_installed_meta_cache[address] = finder
sys.meta_path.append(finder)
print(f'{finder} installed on sys.meta_path')
def remove_meta(address):
if address in _installed_meta_cache:
finder = _installed_meta_cache.pop(address)
sys.meta_path.remove(finder)
print(f'{finder} removed from sys.meta_path')
>>> import sys
>>> sys.meta_path
[<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]
>>> import urllib
>>> import urlimport
>>> urlimport .install_meta('http://localhost:15000')
<urlimport.UrlMetaFinder object at 0x0000000AB84D7B70> installed on sys.meta_path
>>> sys.meta_path
[<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>, <urlimport.UrlMetaFinder object at 0x0000000AB84D7B70>]
>>> import fib
find_module: fullname=fib, path=None
find_module: baseurl=http://localhost:15000, basename=fib
http://localhost:15000
Getting links from http://localhost:15000
find_module: module fib found
loader: reading http://localhost:15000/fib.py
loader: http://localhost:15000/fib.py loaded
I'm fib
>>> from grok import blah
find_module: fullname=grok, path=None
find_module: baseurl=http://localhost:15000, basename=grok
http://localhost:15000
Getting links from http://localhost:15000
find_module: trying package grok
loader: reading http://localhost:15000/grok/__init__.py
loader: http://localhost:15000/grok/__init__.py loaded
I'm grok.__init__.py
Getting links from http://localhost:15000/grok
find_module: package grok loaded
loader: reading http://localhost:15000/grok/__init__.py
loader: cached http://localhost:15000/grok/__init__.py
I'm grok.__init__.py
find_module: fullname=grok.blah, path=['http://localhost:15000/grok']
find_module: baseurl=http://localhost:15000/grok, basename=blah
http://localhost:15000/grok
Getting links from http://localhost:15000/grok
find_module: module grok.blah found
loader: reading http://localhost:15000/grok/blah.py
loader: http://localhost:15000/grok/blah.py loaded
I'm grok.blah
导入模块时,解释器遍历 sys.meta_path 中的查找器,调用它们的 find_module() 方法定位模块加载器。sys.meta_path 列表上查找器的位置会影响模块加载结果。
install_meta 方法会安装一个特殊的查找器 UrlMetaFinder 实例到 sys.meta_path 的末尾。前面的路径找不到的模块会被最后一个查找器捕获。查找器会通过抓取指定 URL 的内容构建链接,导入的模块名跟链接做对比,匹配上的会从远程机器上加载。
方法2:写一个钩子直接嵌入到 sys.path 中
# urlimport.py
# ......
# Path finder class for a URL
class UrlPathFinder(importlib.abc.PathEntryFinder):
def __init__(self, baseurl):
self._links = None
self._loader = UrlModuleLoader(baseurl)
self._baseurl = baseurl
def find_loader(self, fullname):
print(f'find_loader: {fullname}')
parts = fullname.split('.')
basename = parts[-1]
# Check link cache
if self._links is None:
self._links = []
self._links = _get_links(self._baseurl)
# Check if it's a package
if basename in self._links:
print(f'find_loader: trying package {fullname}')
fullurl = self._baseurl + '/' + basename
loader = UrlPackageLoader(fullurl)
try:
loader.load_module(fullname)
print(f'find_loader: package {fullname} loaded')
except ImportError as e:
print(f'find_loader: {fullname} is a namespace package')
loader = None
return (loader, [fullurl])
# A normal module
filename = basename + '.py'
if filename in self._links:
print(f'find_loader: module {fullname} found')
return (self._loader, [])
else:
print(f'find_loader: module {fullname} not found')
def invalidate_caches(self):
print('invalidating link cache')
self._links = None
# Check path to see if it looks like a URL
_url_path_cache = {}
def handle_url(path):
if path.startswith(('http://', 'https://')):
print(f'Handle path? {path}. [Yes]')
if path in _url_path_cache:
finder = _url_path_cache[path]
else:
finder = UrlPathFinder(path)
_url_path_cache[path] = finder
return finder
else:
print(f'Handle path? {path}. [No]')
def install_path_hook():
sys.path_hooks.append(handle_url)
# 清空查找器缓存,检验此次是否绑定成功
sys.path_importer_cache.clear()
print('Installing handle_url')
def remove_path_hook():
sys.path_hooks.remove(handle_url)
sys.path_importer_cache.clear()
print('Removing handle_url')
>>> import urlimport
>>> urlimport.install_path_hook()
Installing handle_url
>>> import fib
Handle path? C:\Users\PLAYER\AppData\Local\Programs\Python\Python37\python37.zip. [No]
Traceback (most recent call last):
File "" , line 1, in <module>
ModuleNotFoundError: No module named 'fib'
>>> import sys
>>> sys.path_hooks
[<class 'zipimport.zipimporter'>, <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x000000E6E9CBD598>, <function handle_url at 0x000000E6EA53A2F0>]
>>> sys.path.append('http://localhost:15000')
>>> import fib
Handle path? http://localhost:15000. [Yes]
find_loader: fib
Getting links from http://localhost:15000
find_loader: module fib found
loader: reading http://localhost:15000/fib.py
loader: http://localhost:15000/fib.py loaded
I'm fib
sys.path 实体被处理时,会调用sys.path_hooks 中的函数,如果这个函数返回了一个查找器对象,这个对象就会被用来为 sys.path 实体加载模块。
查找器对象和sys.apth 中的每一个实体都有绑定关系,存在 sys.path_importer_cache 中。
想在模块加载时执行某个动作。
能使用上节的钩子函数实现。
# postimport.py
import importlib
import sys
from collections import defaultdict
_post_import_hooks = defaultdict(list)
class PostImportFinder:
def __init__(self):
self._skip = set()
def find_module(self, fullname, path=None):
if fullname in self._skip:
return None
self._skip.add(fullname)
return PostImportLoader(self)
class PostImportLoader:
def __init__(self, finder):
self._finder = finder
def load_module(self, fullname):
importlib.import_module(fullname)
module = sys.modules[fullname]
for func in _post_import_hooks[fullname]:
func(module)
self._finder._skip.remove(fullname)
# 没有这一步,删掉sys.modules中的已有模块再重导入无法触发动作
return module
def when_imported(fullname):
def decorate(func):
if fullname in sys.modules:
func(sys.modules[fullname])
else:
_post_import_hooks[fullname].append(func)
return func
return decorate
# 加到第一位,可捕获全部导入操作
sys.meta_path.insert(0, PostImportFinder())
使用 --user 指令能创建用户私有的 site-packages。sys.path 中用户的 site-packages 位于系统的该目录之前,因此在该用户使用时会比系统的优先级高。
pip install --user packagename
# 好像不能用
另外一种方法是创建虚拟环境。
不创建新的 Python 克隆的前提下创建一个新的 Python 环境。
使用 pyvenv 命令:
pyvenv Venv001
在文件夹 Venv001/bin 下会有一个 Python 解释器,这个解释器的 site-packages 目录重定位到一个新的目录而和系统路径分开。
默认情况下,虚拟环境是空的。如果想把一个已安装包作为虚拟环境的一部分,可以使用 --system-site-packages 创建虚拟环境。
pyvenv --system-site-packages Venv001
编写的库分享之前,先整理目录结构:
projectname/
README.txt
Doc/
documentation.txt
projectname/
__init__.py
foo.py
bar.py
utils/
__init__.py
spam.py
grok.py
examples/
helloword.py
编写一个 setup.py。
# setup.py
from distutils.core import setup
setup(name = 'projectname',
version = '1.0',
author = 'name',
author_email = '[email protected]',
url = 'http://www.name.com/projectname',
packages = ['projectname', 'projectname.utils'],
)
下一步,创建一个 MAINFEST.in 文件,列出所有需要包含进来的非源码文件:
# MAINFEST.in
include *.txt
recursive-include examples *
recursive-include Doc *
setup.py 和 MAINFEST.in 文件要在包的最顶级目录。然后执行命令:
python setup.py sdist
这个命令创建一个文件 ‘projectname-1.0.zip’,这个文件可以发布。
python 中有一些第三方包是做为标准库中包的替代,用户可能不会安装这些三方包。自己要发布的包最好使用标准的 Python3 安装。