logo头像

更好的组织代码

总览

项目结构

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
from workers import Carpenter

class Table():
pass
class Chair():
pass

workers.py

1
2
3
4
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)。

不要使用下划线来组织命名空间,使用子模块更好:

1
2
3
4
# 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`清晰的说明想导入具体的哪个模块,将其放入全局的命名空间中。

糟糕的导入方法:

1
2
3
4
[...]
from modu import *
[...]
x = sqrt(4) # Is sqrt part of modu? A builtin? Defined above?

好一些的导入方法:

1
2
3
from modu import sqrt
[...]
x = sqrt(4) # sqrt may be part of modu, if not redefined in between

最好的示范:

1
2
3
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是最好的做法,如果子包或者更深层次的包不需要共享任何代码时。

当要导入嵌套的生层次的包时,可以给包命个别名,之后使用别名,不使用冗长的包名

1
import very.deep.module as mod

面向对象

In Python, everything is an object。

Functions, classes, strings,其它任意类型都是对象,他们有类型,可以作为参数传递,有方法,有属性。

选择编程范式:

  1. 使用面向对象:当有对象(windows, buttons, avatars)需要相对长的生命周期在计算机的内存中时
  2. 使用纯函数:
    • 纯函数的结果是确定的:给定一个输入,输出总是固定相同。
    • 当需要重构或优化时,纯函数更易于更改或替换。
    • 纯函数更容易做单元测试:很少需要复杂的上下文配置和之后的数据清除工作。
    • 纯函数更容易操作、修饰和分发。

装饰器

装饰器是一个函数或类,它可以 包装(或装饰)一个函数或方法。被’装饰’的函数或方法会替换原来的函数或方法。

原始方法写装饰器

1
2
3
4
5
6
7
8
def foo():
# do something

def decorator(func):
# 操作函数
return func

foo = decorator(foo) # 手动装饰

使用@decorators语法更清晰

1
2
3
4
@decorator
def bar():
# Do something
# bar() 是装饰器

这个机制对于分离概念和避免外部不相关逻辑“污染”主要逻辑很有用处。

您需要在table中储存一个函数的结果,并且下次能直接使用该结果,而不是再计算一次。这显然不属于函数的逻辑部分。

Context Managers

为人熟知的示例

1
2
with open('file.txt') as f:
contents = f.read()

两种方式实现

使用class(处理简单操作的情况建议用这种)

1
2
3
4
5
6
7
8
9
10
11
12
13
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(封装的逻辑量很大建议用这种)

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
a = 1
a = 'a string'
def a():
pass # Do something

Good

1
2
3
4
count = 1
msg = 'a string'
def func():
pass # Do something

可变和不可变类型

可变类型:可变类型允许内容的内部修改,有对应使其变化的对象函数,如:lists、dictionaries
不可变类型:无对应使其变化的对象函数,无对应使其变化的对象函数:tuple、x=2(变量x指向2)

string也是不可变类型,要连接字符串有以下几种方法:

最差:使用+操作符,效率最差

1
2
3
4
nums = ""
for n in range(20):
nums += str(n)
print nums

好:使用append方法

1
2
3
4
nums = []
for n in range(20):
nums.append(str(n))
print "".join(nums)

更好: 列表推导式,使用join

1
2
nums = [str(n) for n in range(20)]
print "".join(nums)

使用 join() 并不总是最好的选择, 要分情况:

1
2
3
4
5
6
7
foo = 'foo'
bar = 'bar'

foobar = foo + bar # 好,预先 确定数量的字符串创建一个新的字符串时,更快

foo += 'ooo' # 不好,添加到已存在字符串的情况下,使用join更好
foo = ''.join([foo, 'ooo'])

最好:使用map

1
2
nums = map(str, range(20))
print "".join(nums)

还可以使用格式化字符串连接确定数量的字符串字符:

1
2
3
4
5
6
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