更好的组织代码
总览
项目结构
README.rst
LICENSE
setup.py
requirements.txt # 或者pipfile
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py
核心代码在./sample/
,如果核心的文件只有一个,可以直接放在项目根目录下./sample.py
。
License: 开源许可,也可以不要开源许可发布。
docs:包参考文档。
tests: 单元测试。
Makefile:管理任务。
混乱的代码结构
多重混乱的循环依赖
furn.py
from workers import Carpenter
class Table():
pass
class Chair():
pass
workers.py
from furn import Table,Chair
class Carpenter():
pass
如果非要使用循环依赖,那只能使用不太好的方式来导入:在method和function中导入使用。
隐藏耦合
因为有太多关联,每次修改Table的实现,都需要小心翼翼,容易造成Carpenter的代码逻辑问题。
大量使用全局变量或上下文
不显示的传递,而使用大量的全局状态,如height, width, type, wood,这些全局状态容易被代理快速的修改了。一旦被莫名修改后,还需要仔细的检查,能够访问这些全局变量的地方(或者是远程的代码修改了这些全局的状态)。
面条式代码(Spaghetti code)
源代码的控制流程复杂,条件句和循环语句中互相嵌套,混乱而难以理解, 大量重复的代码,没有适当的分割,被视为套管程序。python的缩进特性使得它很难维护这样的代码,最好的方式就是不要写太多这样的代码。
Python中更可能出现混沌代码(Ravioli code)
这类代码包含上百段相似的逻辑碎片,通常是缺乏合适结构的类或对象,如果写代码时弄不清具体的逻辑,就可能出现混沌代码。
模块
Python模块是最主要的抽象层之一,抽象层允许将代码分为不同部分,每个部分包含相关的数据与功能。
例如在项目中,一层控制用户操作相关接口,另一层处理底层数据操作。
为了保持风格的一致,模块名称应该保持简短,小写,不要使用特殊字符等。不叫用.符号,影响python路径查找(my.spam.py的模块名称误让python以为要找my文件夹下的spam)。
不要使用下划线来组织命名空间,使用子模块更好:
# OK
import library.plugin.foo
# not OK
import library.foo_plugin
python导入模块原理
import modu
会从当前文件夹寻找modu.py文件,如未找到,python解释器会从’path’中递归去寻找,仍然未找到,则抛出ImportError。
找到该模块,解释器会在隔离的范围执行该模块,任何顶级modu声明都会被执行,包括其他的imports,模块中的函数和类的定义存储在模块的字典中,在模块命名空间的调用者,可以直接调用模块中的变量,函数,和类。
在其他很多语言中,导入文件的逻辑是,解释器会将该文件的代码复制一份到调用的文件中,这与python是有很大的不同的。python中,导入的module在一个独立的命名空间中,这表示,不需要担心覆盖了当前同名的函数等。
不要使用*导入所有的模块:from modu import *
,使用import *
使代码难以阅读和且无法很好的区分依赖。使用 from modu import func
清晰的说明想导入具体的哪个模块,将其放入全局的命名空间中。
糟糕的导入方法:
[...]
from modu import *
[...]
x = sqrt(4) # Is sqrt part of modu? A builtin? Defined above?
好一些的导入方法:
from modu import sqrt
[...]
x = sqrt(4) # sqrt may be part of modu, if not redefined in between
最好的示范:
import modu
[...]
x = modu.sqrt(4) # sqrt is visibly part of modu's namespace
packages
python提供了非常直观的包系统,即简单地将模块管理机制扩展到一个目录上。任何包含了__init__.py
文件的目录,组成了python的包。
import pack.modu
首先找到pach目录下的__init__.py
文件,执行所有顶层声明,然后再找到pack/modu.py
文件,执行其所有顶层声明。执行完这些所有的操作,modu.py
中定义的variable, function, class,在pack.modu的命名空间中都变成了可用状态。
一个常见的问题是__init__.py
添加太多的代码,当项目的结构越来越复杂,包含子包,子包有包含更深层次的包,当导入深层次中的包时,就需要执行很多的__init__.py
文件。
留空__init__.py
是最好的做法,如果子包或者更深层次的包不需要共享任何代码时。
当要导入嵌套的生层次的包时,可以给包命个别名,之后使用别名,不使用冗长的包名
import very.deep.module as mod
面向对象
In Python, everything is an object。
Functions, classes, strings,其它任意类型都是对象,他们有类型,可以作为参数传递,有方法,有属性。
选择编程范式:
- 使用面向对象:当有对象(windows, buttons, avatars)需要相对长的生命周期在计算机的内存中时
- 使用纯函数:
- 纯函数的结果是确定的:给定一个输入,输出总是固定相同。
- 当需要重构或优化时,纯函数更易于更改或替换。
- 纯函数更容易做单元测试:很少需要复杂的上下文配置和之后的数据清除工作。
- 纯函数更容易操作、修饰和分发。
装饰器
装饰器是一个函数或类,它可以 包装(或装饰)一个函数或方法。被’装饰’的函数或方法会替换原来的函数或方法。
原始方法写装饰器
def foo():
# do something
def decorator(func):
# 操作函数
return func
foo = decorator(foo) # 手动装饰
使用@decorators语法更清晰
@decorator
def bar():
# Do something
# bar() 是装饰器
这个机制对于分离概念和避免外部不相关逻辑“污染”主要逻辑很有用处。
您需要在table中储存一个函数的结果,并且下次能直接使用该结果,而不是再计算一次。这显然不属于函数的逻辑部分。
Context Managers
为人熟知的示例
with open('file.txt') as f:
contents = f.read()
两种方式实现
使用class(处理简单操作的情况建议用这种)
class CustomOpen(object):
def __init__(self, filename): # 首先被实例化
self.file = open(filename)
def __enter__(self): # 然后,调用enter,返回值在 as f 语句中被赋给 f
return self.file
def __exit__(self, ctx_type, ctx_value, ctx_traceback): #with块中的代码执行完i调用exit
self.file.close()
# 使用自定义的context
with CustomOpen('file') as f:
contents = f.read()
使用generator(封装的逻辑量很大建议用这种)
from contextlib import contextmanager
@contextmanager
def custom_open(filename): # custom_open 函数一直运行到 yield 语句
f = open(filename)
try:
yield f # 运行到这里将控制权返回给 with 语句
finally: # with块代码执行完后,执行finally
f.close()
with custom_open('file') as f: # 控制权到with后 as f 部分将yield的 f 赋值给f
contents = f.read()
动态类型
python是动态类型语言,变量没有固定的类型。变量不是计算机内存中的一段,而是指向该类型对象的某个名称或者tag。
例如:’a’ 设置指向value 1, 随后设置指向 value ‘a string’, 随后设置指向一个function.
要避免同一个变量指向不同的东西!
Bad
a = 1
a = 'a string'
def a():
pass # Do something
Good
count = 1
msg = 'a string'
def func():
pass # Do something
可变和不可变类型
可变类型:可变类型允许内容的内部修改,有对应使其变化的对象函数,如:lists、dictionaries
不可变类型:无对应使其变化的对象函数,无对应使其变化的对象函数:tuple、x=2(变量x指向2)
string也是不可变类型,要连接字符串有以下几种方法:
最差:使用+
操作符,效率最差
nums = ""
for n in range(20):
nums += str(n)
print nums
好:使用append方法
nums = []
for n in range(20):
nums.append(str(n))
print "".join(nums)
更好: 列表推导式,使用join
nums = [str(n) for n in range(20)]
print "".join(nums)
使用 join() 并不总是最好的选择, 要分情况:
foo = 'foo'
bar = 'bar'
foobar = foo + bar # 好,预先 确定数量的字符串创建一个新的字符串时,更快
foo += 'ooo' # 不好,添加到已存在字符串的情况下,使用join更好
foo = ''.join([foo, 'ooo'])
最好:使用map
nums = map(str, range(20))
print "".join(nums)
还可以使用格式化字符串连接确定数量的字符串字符:
foo = 'foo'
bar = 'bar'
foobar = '%s%s' % (foo, bar) # OK
foobar = '{0}{1}'.format(foo, bar) # better
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # best