之前和朋友聊天,说到 Python 项目的结构问题,Python 确实是一门比较灵活的语言,你可以单独执行某个文件,把这些文件扔到一起看起来像个工程,但是外人来看其实就是一个个独立的小文件,小模块,对于小型项目来说可能并不会造成什么问题,但是对于大型项目,尤其是如果需要多人合作,有一个关于工程结构的基本认识是非常必要的。
Python 没有强制的结构规范,但是有一个大家的共识
README.rst
LICENSE
setup.py
requirements.txt
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py
项目的结构是项目的门面,简单易懂的项目结构能够让介入的第三人快速的熟悉项目,Python 项目结构没有太多的约束和限制,Python 提供的导入和模块管理使得结构化 Python 相对简单,但也要注意一些比如循环依赖的问题。
模块
为了解决项目结构的问题就不得不提到模块(module),这是 Python 最主要的抽象层,模块允许将代码分为不同的部分,每个部分包含相关的数据和功能。
模块名尽量要短,使用小写,并且避免使用特殊符号,比如(.
)或者 (?
) 等。不推荐在模块名中使用下划线。
# ok
import library.plugin.foo
# not ok
import library.foo_plugin
import 的工作原理,比如 import modu
将会寻找合适的文件,调用目录下的 modu.py
文件,如果没有找到,Python 解释器会递归地在 PYTHONPATH
环境变量中查找该文件,如果没有则抛出 ImportError 异常。一旦找到 modu.py
Python 解释器将在隔离的作用域内执行模块。所有顶层语句都会被执行,包括其他的引用。方法和类的定义会存储到模块字典中。这个模块的变量、方法和类通过命名空间暴露给调用方。
import 语句也可以为 from modu import *
,使用 from modu import func
能够精确定位想要导入的方法,并将其放入到全局命名空间。
# 较差的写法
[...]
from modu import *
[...]
x = sqrt(4) # sqrt 是模块 modu 的一部分么?或是内建函数么?上文定义了么?
# 稍好
from modu import sqrt
[...]
x = sqrt(4) # 如果在 import 语句与这条语句之间,sqrt 没有被重复定义,它也许是模块 modu 的一部分。
# 最好的做法
import modu
[...]
x = modu.sqrt(4) # sqrt 显然是属于模块 modu 的。
包
Python 提供非常简单的包管理系统,简单地将模块管理机制扩展到一个目录上,就是包。任意包含 __init__.py
文件的目录都被认为是一个 Python 包。如果包内的模块和子包没有代码共享的需求,使用空白 __init__.py
是正常的做法。
导入深层嵌套包可以使用 import very.very.deep.module as mod
可以简化导入冗长子包。
对象
使用无状态的函数是一种更好的编程范式。
把有隐式上下文和副作用的函数与仅包含逻辑的函数(纯函数)谨慎地区分开来,会带来 以下好处:
- 纯函数的结果是确定的:给定一个输入,输出总是固定相同。
- 当需要重构或优化时,纯函数更易于更改或替换。
- 纯函数更容易做单元测试:很少需要复杂的上下文配置和之后的数据清除工作。
- 纯函数更容易操作、修饰和分发。
装饰器
装饰器本质上是一个 Python 函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func() # 把 foo 当做参数传递进来时,执行 func() 就相当于执行 foo()
return wrapper
def foo():
print('i am foo')
foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo() # 执行 foo() 就相当于执行 wrapper()
使用 @
符号,装饰器的语法糖
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper
@use_logging
def foo():
print("i am foo")
foo()
这样就可以省略掉 foo = use_logging(foo)
这句赋值。
带参数的装饰器
def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator
@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()
类装饰器
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
print ('class decorator runing')
self._func()
print ('class decorator ending')
@Foo
def bar():
print ('bar')
bar()
通过 functools.wrap
来找回原函数的元信息
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ # 输出 'f'
print func.__doc__ # 输出 'does some math'
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
Python 类相关的内置装饰器
@staticmathod、@classmethod、@property
上下文管理器
上下文管理器是一个 Python 对象,为操作提供了额外的上下文信息。with 语句初始化上下文
with open('file.txt') as f:
contents = f.read()
实现这个功能有两种简单的方法:使用类或使用生成器。 让我们自己实现上面的功能,以使用类方式开始:
class CustomOpen(object):
def __init__(self, filename):
self.file = open(filename)
def __enter__(self):
return self.file
def __exit__(self, ctx_type, ctx_value, ctx_traceback):
self.file.close()
with CustomOpen('file') as f:
contents = f.read()
生成器使用 Python 自带的 contextlib
from contextlib import contextmanager
@contextmanager
def custom_open(filename):
f = open(filename)
try:
yield f
finally:
f.close()
with custom_open('file') as f:
contents = f.read()
由于这两种方法都是一样的,所以我们应该遵循 Python 之禅来决定何时使用哪种。 如果封装的逻辑量很大,则类的方法可能会更好。 而对于处理简单操作的情况,函数方法可能会更好。