每日10行代码124: 编写高质量python代码的方法50:用包来安排模块,并提供稳固的API

程序的代码量变大之后,我们自然就需要重新调整其结构。我们会把大函数分割成小函数,会把某些数据结构重构为辅助类(参见该书第22条),也会把功能分散到多个相互依赖的模块之中。
到了一定的阶段,我们就会发现,模块的数量实在太多了,于是,我们就需要在程序之中引进一种抽象层,舍不得代码更加便于理解。Python 的包 package 就可以充当这样的抽象层。包,是一种含有其他模块的模块。
在大多数情况下,我们会给目录中放入名为 __init__.py 的空文件,并以些来定义包。只要目录里面有 __init__.py , 我们就可以采用相对于该目录的路径,来引入目录中的其他 Python 文件。例如,某个程序的目录结构如下。

main.py
mypackage/__init__.py
mypackage/models.py
mypackage/utils.py

为了以相对 的方式引入 utils 模块,我们需要把上级模块的绝对名称,写在引入语句的 from 部分之中,也就是说,我们要在 from 关键字右侧,写出 mypackage 包相对应的目录名。

# main.py
form mypackage import utils

如果某个包目录中还嵌套有其他的包 (如 mypackage.foo.bar) ,那么也需要采用上述形式来编写 import 语句。

python 3.4 提供了名称空间包(namespace package) 这一机制,使我们能够以更加灵活的方式来定义包。名称空间包中的模块,可以来自完全不同的目录、zip压缩文档,甚至远端系统。 PEP 420 (http://www.python.org/dev/peps/pep-0420/) 详细介绍了开发者应该如何使用名称空间包的高特性。

对于python程序来说,包所提供的能力,主要有两大用途。

1. 名称空间

包的第一种用途,是把模块划分到不同的名称空间之中。这使得开发者可以编写多个文件名相同的模块,并把它们放在不同的绝对路径之下。例如,下面这个程序,能够从两个同名的 utils.py 模块中,分别引入属性。由于这两个模块可以通过绝对路径来区分,所以这种引入方式是可行的。

# main.py
from analysis.utils import log_base2_bucket
from frontend.utils import stringify

bucket = stringify(log_base2_bucket(33))

但是,如果这引起包里面定义的函数、类或子模块相互重名,那么上面的做法就失效了。例如,我们要从 analysis.utils 和 frontend.utils 包中,分别引入名为 inspect 的函数。此时不能像上面那样,直接引入这两个包中的属性,因为第二条import 语句,会把当前作用域中的 inspect 值覆盖掉。

#main2.py
from analysis.utils import inspect
from fronted.utils import inspect   # Overwrites!

解决办法是:在 import 语句中,通过 as 子句,给引入当前作用域中的属性重新起个名字。

# main3.py
from analysis.utils import inspect as analysis_inspect
from frontend.utils import inspect as frontend_inspect

value = 33
if analysis_inspect(value) == frontend_inspect(value):
    print('Inspection equal!')

凡是通过 import 语句引入的内容,都可以用 as 子句来改名,即使引入整个模块,我们也依然能用 as 为其改名。这使得开发者能够轻松地访问位于不同名称空间之中的代码,并在引入该段代码的时候时候,阐明其身份。

提示: 还有一种办法,也可以避免引入互相冲突的名称,那就是:每次使用模块时,都从最高级的路径开始,完整地写出各模块的名称。
对于上例来说,我们可以编写 import analysis.utils 和 import frontend.utils 语句,然后,分别通过 analysis.utils.inspect 和 frontend.utils.inspect 这样的完整路径,来访问那两个模块里的 inspect 函数。
这种办法完全不需要便用as子句,而且对于头一次接触本代码的读者而言,它可以非常清晰地指出每个 inspect 函数究竟定义在哪个模块里面。

2. 稳固的 API

python 包的第二种用途,是为外部使用者提供严谨而稳固的 API.
如果要编写便用范围较广的 API,如编写开源包(参见该书第48条),那么,就需要提代一些稳固的功能,并保证它们不会因为版本的变动而受到影响。为此,我们必须把代码的内部结构对外隐藏起来,以便在不影响现有用户的前提下,通过重构来改善包内的模块。
在Python 程序中,我们可以为包或模块编写名为 __all__ 的特殊属性,以减少其暴露给外围 API 使用者的信息量。 __all__ 属性的值,是一份列表,其中的每个名称,都将作为本模块的一条公共 API, 导出给外部代码。如果外部用户以 from foo import * 的形式来使用foo 模块,那么只有 foo.__all__ 中列出的那些属性,才会从foo 中引入。若是foo 模块没有提供 __all__ ,则只会引入Public属性,也就是说,只会引入 不以下划线开头的那些属性(参见该书第27条)。
例如,要提供一个包,以计算移动的抛射体 (projectile) 之间的碰撞。下面定义的这个 models 模块,位于mypackage 包中,该模块用来表示抛射体:

# models.py
__all__ = ['projectile']

class Projectile(object):
    def __init__(self, mass, velocity):
        self.mass = mass
        self.velocity = velocity

然后,我们在这个 mypackage 包中,再定义 一个 utils 模块,用来对 Projectile 实例执行某些操作,例如,模拟 Projectile 之间的碰撞。

# utils.py
from .models import Projectile     

__all__ = ['simulate_collision']   

def _dot_product(a,b):
    # ...

def simulate_collision(a,b):
    # ...

现在,我们想把这套API 中所有的public 部分,都作为 mypackage 模块的属性,提供给外界用户,使他们总是能够直接引入 mypackage, 而不用再执行 mypackage.models 和 mypackage.utils 等形式的引入操作。这样一来,即使 mypackage 包的内部结构发生变化(例如,把utils.py下的 simulate_collision 放入了 models.py中),使用这些 API的外部代码,也依然能照常运行。
为了在Python包中实现此功能,我们需要修改mypackage目录下的 __init__.py 文件。当外界代码引入 mypackage 模块的时候,该文件实际上也会成为mypackage的一部分内容。因此,我们可以限定 __init__.py 文件引入的名称,以此来指明mypackage 模块所要暴露给外界用户的 API。由于mypackage内部那些模块,都已经提供了 __all__ 属性,所以,我们只需把内部模块的所有内容都引入进来,并据此更新 __init__.py__all__ 属性,即可把 mypackage 的public 接口适当地公布外界。

# __init__.py
__all__ = []
from . models import *
__all__ += models.__all__
from .utils import *
__all__ += utils.__all__

经过上述处理之后, API的使用者就可以直接引入 mypackage,而不用再访问具体的内部模块了:

# api_consumer.py
from mypackage import *

a = Projectile(1.5,3 )
b = Projectile(4, 1.7)
after_a, after_b = simulate_collision(a, b)

请注意,像mypackage.utils._dot_product 这样, 专门在内部使用的函数,是不会作为 mypackage 包的API 提供给使用者的,因为这些函数,没有出现在 __all__列表之中。 在 __all__ 里省略某个名称,就意味着外部代码不会能过 from mypackage import * 语句导入该名称,这实际上相当于把内部专用的名称给隐藏起来了。
在必须给外界提供严谨而稳固的 API 时,上面这套方案相当合宜。但是,如果我们只想在自己所拥有的各模块之间构建内部的API,那恐怕就用不到 __all__ 所提供的功能了,此时应该避免使用它。对于在大型代码库上相互协作的编程团队来说,包本身所提供的命名空间机制 ,通常来说,就已经足够用了,它可以令开发者在控制这份代码库的同时,保持合理的接口边界。

谨慎使用 import * 形式的引入语句

像 from x import y 这样的引入语句,是比较清晰的,因为我们可以明确看出 y 来源于x 包或x模块。包含通配符的 from foo import * 等语句,也是很有用的,它特别适在进行交互式 Python 编程时候使用。但是,通配符会令代码变得有些难懂。
对于刚读到这份代码的人来说,他们无法通过 from foo import * 语句,批出某个名称的来源。 如果模块里面有多条 import * 语句,那就必须把引用的全部模块都检查一遍,才能确定某个名称到底定义在哪个模块之中。
import * 语句会把外围模块中的同名内容覆盖掉。我们在代码中使用别的模块时,可能会无意间通过多条 import * 语句,引入了一些重复的名称,而这种现象,会引发奇怪的bug.
最安全的做法是:尽量不要在代码中使用 import * 语句,而是应该以from x import y 形式的语句,明确指出自己想要引入的名称。

要点:

  • Python 包是一种含有其他模块的模块。我们可以用包把代码划分成各自独立且互不冲突的名称空间,使得每块代码都 能具备独有的绝对模块名称。
  • 只要把 __init__.py 文件放入含有其他源文件的目录里,就可以将该目录定义为包。目录中的文件,都将成为包的子模块。该包的目录下面,也可以含有其他包。
  • 把外界可见的名称,列在名为__all__ 的特列属性里,即可为包提供一套明的API.
  • 如果想隐藏某个包的内部实现,那么我们可以在包的 __init__.py 文件中,只把外办可见的那些名称引入进来,或是给化限内部使用的那些名称添加下划级前缀。
  • 如果软件包只在开发团队或代码库内部使用,那可能没有必要通过 __all__ 来明确地导出API.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值