note-PythonCookbook-第十章 模块与包

第十章 模块与包

10.1 构建一个模块的层级包

在每个代码文件夹中都都需要定义 __init__.py 文件。
导入模块下的文件时,会先导入该模块下的 __init__.py 文件。
__init__.py 一般是空的。能用来自动加载子模块。

10.2 控制模块被全部导入的内容

当使用 from module import * 语句时,从该模块导出的内容进行精确控制。
在模块中定义一个 __all__ 变量来制定会被导出的内容。
from module import * 语句一般在定义了大量变量名的模块中使用。这样会导入所有不易下划线开头的对象。如果该模块定义了 __all__,就只会导出列举出的内容。__all__ 为空时,没有东西被导出,__all__ 包含未定义的名字时,导入时会引起 AttributeError。

10.3 使用相对路径名导入包中子模块

文件结构:

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 选项,相对导入会正确运行。

10.4 将模块分割成多个文件

# 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 才行。

10.5 利用命名空间导入目录分散的代码

分散的代码各自是文件目录,想用共同的包前缀吧所有组件连接而不需要一个个地安装。
删掉顶级目录中创造共同命名空间的 __init__.py 文件,在导入包时,解释器会创建一个由所有包含匹配包名的目录组成的列表。该包名的 __path__ 变量中有这个目录列表的副本。
一个包如果没有 __file__ 属性,那这个包是一个包命名空间。

10.6 重新加载模块

使用 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 … 从该模块导入的内容并未被更新。

10.7 运行目录或压缩文件

应用程序的目录添加 __main__.py 文件。在顶级目录运行 python,加该目录名,解释器会把 __main__.py 作为主文件执行。
将该目录下的 py 文件打包为一个 zip文件后也能用 python解释器运行这个压缩文件。

10.8 读取位于包中的数据文件

使用内置的 I/O 功能如 open() 读取数据文件的问题:

  1. 包对工作路径的控制权小,其中的 I/O 操作一般使用绝对路径。整理各个模块的路径很繁琐。
  2. 包安装为 .zip 或 .egg 文件时,open() 不会起作用。

pkgutil.get_data() 函数是用来读取数据文件的工具,不论包安装在哪里,都能照常将文件内容以字节字符串返回。第一个参数是包含包名的字符串,能使用包名或 __package__ 特殊变量。第二个参数是数据文件的相对名称。

10.9 将文件夹加入到 sys.path

想添加新目录到 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'))

10.10 通过字符串名导入模块

importlib.import_module() 能手动导入字符串表示的模块,返回生成的模块对象,使用参数将其接收后作为正常模块使用。

import importlib
math = importlib.import_module('math')
print(math.sin(2))

可以传入 package 参数用于相对导入。

可以导包内的模块,但不能直接导模块内的函数。

10.11 通过钩子远程加载模块

从远程机器上导入模块。
创建如下代码结构:

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'<urlmodule {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 "<stdin>", 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 中。

10.12 导入模块的同时修改模块

想在模块加载时执行某个动作。
能使用上节的钩子函数实现。

# 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())

10.13 安装私有的包

使用 --user 指令能创建用户私有的 site-packages。sys.path 中用户的 site-packages 位于系统的该目录之前,因此在该用户使用时会比系统的优先级高。

pip install --user packagename
# 好像不能用

另外一种方法是创建虚拟环境。

10.14 创建新的 Python 环境

不创建新的 Python 克隆的前提下创建一个新的 Python 环境。
使用 pyvenv 命令:

pyvenv Venv001

在文件夹 Venv001/bin 下会有一个 Python 解释器,这个解释器的 site-packages 目录重定位到一个新的目录而和系统路径分开。
默认情况下,虚拟环境是空的。如果想把一个已安装包作为虚拟环境的一部分,可以使用 --system-site-packages 创建虚拟环境。

pyvenv --system-site-packages Venv001

10.15 分发包

编写的库分享之前,先整理目录结构:

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 = 'name@address.com',
    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 安装。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值