构建健壮 Python 包的 5 个简单规则

创建一个软件包(package)似乎已经足够简单了,也就是在文件目录下搜集一些模块,再加上一个__init__.py文件,对吧?我们很容易看出来,随着时间的推移,通过对软件包的越来越多的修改,一个设计很差的软件包可能会出现循环依赖问题,或是可能变得不可移植和不可靠。

1. __init__.py 仅为导入服务

对于一个简单的软件包,你可能会忍不住把工具方法,工厂方法和异常处理都丢进__init__.py,千万别这样!

一个结构良好的__init__.py文件,仅为一个非常重要的目的来服务:从子模块导入。你的__init__.py应该看起来像这个样子:

Python


# ORDER MATTERS HERE -- SOME MODULES ARE DEPENDANT ON OTHERS

# 导入顺序要考虑——一些模块会依赖另外的一些

from exceptions import FSQError, FSQEnvError, FSQEncodeError,

FSQTimeFmtError, FSQMalformedEntryError,

FSQCoerceError, FSQEnqueueError, FSQConfigError,

FSQPathError, FSQInstallError, FSQCannotLockError,

FSQWorkItemError, FSQTTLExpiredError,

FSQMaxTriesError, FSQScanError, FSQDownError,

FSQDoneError, FSQFailError, FSQTriggerPullError,

FSQHostsError, FSQReenqueueError, FSQPushError

# constants relies on: exceptions, internal

import constants

# const relies on: constants, exceptions, internal

from const import const, set_const # has tests

# path relies on: exceptions, constants, internal

import path # has tests

# lists relies on: path

from lists import hosts, queues

#...

2.使用__init__.py来限制导入顺序

把方法和类置于软件包的作用域中,这样用户就不需要深入软件包的内部结构,使你的软包变得易用。

作为调和导入顺序的唯一地方。

使用得当的话,__init__.py 可以为你提供重新组织内部软件包结构的灵活性,而不需要担心由内部导入子模块或是每个模块导入顺序所带来的副作用。因为你是以一个特定的顺序导入子模块,你的__init__.py 对于他程序员来讲应该简单易懂,并且能够明显的表示该软件包所能提供的全部功能。

文档字符串,以及在软件包层面对__all__属性的赋值应当是__init__.py中唯一与导入无关的代码:

Python


__all__ = [ 'FSQError', 'FSQEnvError', 'FSQEncodeError', 'FSQTimeFmtError',

'FSQMalformedEntryError', 'FSQCoerceError', 'FSQEnqueueError',

'FSQConfigError', 'FSQCannotLock', 'FSQWorkItemError',

'FSQTTLExpiredError', 'FSQMaxTriesError', 'FSQScanError',

'FSQDownError', 'FSQDoneError', 'FSQFailError', 'FSQInstallError',

'FSQTriggerPullError', 'FSQCannotLockError', 'FSQPathError',

'path', 'constants', 'const', 'set_const', 'down', 'up',

# ...

]

3.使用一个模块来定义所有的异常

你也许已经注意到了,__init__.py中的第一个导入语句从exceptions.py子模块中导入了全部的异常。从这里出发,你将看到,在大多数的软件包中,异常被定义在引起它们的代码附近。尽管这样可以为一个模块提供高度的完整性,一个足够复杂的软件包会通过如下两种方式,使得这一模式出现问题。

通常一个模块/程序需要从一个子模块导入一个函数, 利用它导入代码并抛出异常。为了捕获异常并保持一定的粒度,你需要导入你需要的模块,以及定义了异常的模块(或者更糟,你要导入一系列的异常)。这一系列衍生出来的导入需求,是在你的软件包中编织一张错综复杂的导入之网的始作俑者。你使用这种方式的次数越多,你的软件包内部就变的越相互依赖,也更加容易出错。

随着异常数量的不断增长,找到一个软件包可能引发的全部异常变的越来越难。把所有的异常定义在一个单独的模块中,提供了一个方便的地方,在这里,程序员可以审查并确定你的软件包所能引发全部潜在错误状态。

你应该为你的软件包的异常定义一个基类:

Python


class APackageException(Exception):

'''root for APackage Exceptions, only used to except any APackage error, never raised'''

pass

然后确保你的软件包在任何错误状态下,只会引发这个基类异常的子类异常,这样如果你需要的话,你就可以阻止全部的异常:

Python


try:

'''bunch of code from your package'''

except APackageException:

'''blanked condition to handle all errors from your package'''

对于一般的错误状态,这里有一些重要的异常处理已经被包括在标准库中了(例如,TypeError, ValueError等)

灵活地定义异常处理并保持足够的粒度:

Python


# from fsq

class FSQEnvError(FSQError):

'''An error if something cannot be loaded from env, or env has an invalid

value'''

pass

class FSQEncodeError(FSQError):

'''An error occured while encoding or decoding an argument'''

pass

# ... and 20 or so more

在你的异常处理中保持更大的粒度,有利于让程序员们在一个try/except中包含越来越大的,互相不干涉的代码段。

Python



# this

try:

item = fsq.senqueue('queue', 'str', 'arg', 'arg')

scanner = fsq.scan('queue')

except FSQScanError:

'''do something'''

except FSQEnqueueError:

'''do something else'''

# not this

try:

item = fsq.senqueue('queue', 'str', 'arg', 'arg')

except FSQEnqueueError:

'''do something else'''

try:

scanner = fsq.scan('queue')

except FSQScanError:

'''do something'''

# and definitely not

try:

item = fsq.senqueue('queue', 'str', 'arg', 'arg')

try:

scanner = fsq.scan('queue')

except FSQScanError:

'''do something'''

except FSQEnqueueError:

'''do something else'''

在异常定义时保持高度的粒度,会减少错综复杂的错误处理,并且允许你把正常执行指令和错误处理指令分别开来,使你的代码更加易懂和更易维护。

4. 在软件包内部只进行相对导入

在子模块中你时常见到的一个简单错误,就是使用软件包的名字来导入软件包。

Python


# within a sub-module

from a_package import APackageError

这样做会导致两个不好的结果:

子模块只有当软件包被安装在 PYTHONPATH 内才能正确运行。

子模块只有当这个软件包的名字是 a_package 时才能正确运行。

尽管第一条看上去并不是什么大问题,但是考虑一下,如果你在 PYTHONPATH 下的两个目录中,有两个同名的软件包。你的子模块可能最终导入了另一个软件包,你将无意间使得某个或某些对此毫无戒备的程序员(或是你自己)debug 到深夜。

Python


# within a sub-module

from . import FSQEnqueueError, FSQCoerceError, FSQError, FSQReenqueueError,

constants as _c, path as fsq_path, construct,

hosts as fsq_hosts, FSQWorkItem

from .internal import rationalize_file, wrap_io_os_err, fmt_time,

coerce_unicode, uid_gid

# you can also use ../... etc. in sub-packages.

5. 让模块保持较小的规模

你的模块应当比较小。记住,那个使用你软件包的程序员会在软件包作用域进行导入,同时你会使用你的 __init__.py 文件来作为一个组织工具,来暴露一个完整的接口。

好的做法是一个模块只定义一个类,伴随一些帮助方法和工厂方法来协助建立这个模块。

Python


class APackageClass(object):

'''One class'''

def apackage_builder(how_many):

for i in range(how_many):

yield APackageClass()

如果你的模块暴露了一些方法,把一些相互依赖的方法分为一组放进一个模块,并且把不相互依赖的方法移动到单独的模块中:

Python


26

####### EXPOSED METHODS #######

def enqueue(trg_queue, item_f, *args, **kwargs):

'''Enqueue the contents of a file, or file-like object, file-descriptor or

the contents of a file at an address (e.g. '/my/file') queue with

arbitrary arguments, enqueue is to venqueue what printf is to vprintf

'''

return venqueue(trg_queue, item_f, args, **kwargs)

def senqueue(trg_queue, item_s, *args, **kwargs):

'''Enqueue a string, or string-like object to queue with arbitrary

arguments, senqueue is to enqueue what sprintf is to printf, senqueue

is to vsenqueue what sprintf is to vsprintf.

'''

return vsenqueue(trg_queue, item_s, args, **kwargs)

def venqueue(trg_queue, item_f, args, user=None, group=None, mode=None):

'''Enqueue the contents of a file, or file-like object, file-descriptor or

the contents of a file at an address (e.g. '/my/file') queue with

an argument list, venqueue is to enqueue what vprintf is to printf

if entropy is passed in, failure on duplicates is raised to the caller,

if entropy is not passed in, venqueue will increment entropy until it

can create the queue item.

'''

# setup defaults

trg_fd = name = None

# ...

上面的例子是 fsq/enqueue.py,它暴露了一系列的方法来为同一个功能提供不同的接口(就像 simplejson 中的l oad/loads)。尽管这个例子足够直观,让你的模块保持较小规模需要一些判断,但是一个好的原则是:

当你有疑问的时候,就去创建一个新的子模块吧。我有建立一个python学习交流群,在群里我们相互帮助,相互关心,相互分享内容,这样出问题帮助你的人就比较多,群号是301,还有056,最后是051,这样就可以找到大神聚合的群,如果你只愿意别人帮助你,不愿意分享或者帮助别人,那就请不要加了,你把你会的告诉别人这是一种分享。如果你看了觉得还可以的麻烦给我点个赞谢谢

学习是对自己最好的投资,而机会属于有准备的人,这是一个看脸的时代,但最终拼的是实力。人和人之间的差距不在于智商,而在于如何利用业余时间,所以没有等出来的辉煌,只有干出来的精彩。其实只要你想学习,什么时候开始都不晚,不要担心这担心那,你只需努力,剩下的交给时间,而你之所以还没有变强,只因你还不够努力,要记得付出不亚于任何人的努力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容