构建你的工程
使用构建这个词,意味着我们要注重怎么用最好的方式来达到项目的目标。我们需要考虑怎么才能最好的利用Python语言的特点从而写出干净高效的代码。实际中,组织意味着创建的代码的逻辑和依赖关系结构清晰明了,就像是文件和文件夹在文件系统中那样。
哪个函数在哪个模块里?数据怎么在项目中传输?什么样的特点和功能应该被组织在一起,什么样的又该孤立出来?通过回答这样的问题,你来计划你的项目展现的样子。
在这一部分,我们仔细分析Python包和它的导入方式,它们是使你项目结构清晰的重要元素。然后我们再探讨怎么易扩展和可靠测试。
结构是关键
多亏了Python包的导入方式,组织一个Python项目还是很简单的。在这,简单意味着你不用太多的约束,并且模块和模块之间的导入很容易控制。因此,剩下的就是纯粹是处理你自己项目中不同部分的交互了。
简单构建一个项目意味着容易分化的不够彻底。分化不够彻底的项目往往有以下这些特点。
·各种混乱的的循环依赖:如果你的table类和chair类在furn.py中,需要从workers.py中导入carpenter类,来回答一个类似于table.isdoneby()的问题,并且如果反过来carpenter类需要导入table和chair类,来会带类似carpenter.whatdo()之类的问题,这时你就有了一个循环依赖。在这种情况下,你将不得不求助于脆弱的技巧,比如使用import语句内部方法或函数。
·隐藏的耦合关系:table的实现代码中任何变动都会影响不相干的测试用例,因为它影响了carpenter的代码,这需要非常小心的改动才是适应这一改变。。这意味着你需要考虑很多种关于carpenter中table的代码或是table中carpenter的代码。
·繁重的无用的全局声明或环境:不是作为参数传递(height,width,type,wood),table和carpenter依赖可以被任何代理修改的全局变量。你需要仔细了解所有的全局变量才能发现为什么table从长方形变成个正方形了,并且发现原来是另外的一个模板也修改了环境,干扰了table的尺寸。
·面条式代码:多页的嵌套的if句块和for循环带有大量的复制粘贴的痕迹,不进行合适的分割,这样的代码被称作面条式代码。Python的靠空格作为语句块分割,这一特点让上述语句很难识别。好消息是你不会经常看到他们。
·饺子式代码在Python中更加友好:它由上百个相似的逻辑小块组成,通常是没有恰当组织对象或者类。如果你从不记你是否使用过FurnitureTable,AssetTable or Table, 或者TableNew仅仅为了顺手,那么你正遭受饺子式代码。
模块
Python模块是最主要的抽象层,并且是最自然的。抽象层允许把代码分离,保留相关的数据和函数。
例如,项目中应该有一层解决用户交互的接口,让后另一层解决底层数据更改。最自然的分离这两个层的方式是把用户交互层的所有接口放到一个文件中,把底层数据交互接口放到另一个文件当中。这样的话,接口文件需要导入底层文件。只要import或者from...import就可以了。
一旦你导入了你使用的模块。它可能是内置的os和sys模块,第三方模块或者是你工程里的内部模块。
遵循风格知道,保证模块名称简单,并且全小写并且避免特殊字符比如“.”“?”。如果一个文件命名为my.spam.py是应该避免的!这样命名会干扰Python查找模块。
my.spam.py在Python中是找my目录下的spam.py文件。为了避免这种情况,建议使用下划线“_”,即使下划线可以使用,我们也不建议在模块中使用太多的下划线。
除了一些命名限制外,Python文件作为一个模块没有其他的特殊限制了,但是你需要理解import语句的原理,从而可以灵活运用避免问题。
具体来说,import modu语句将会先寻找导入文件同一目录中的modu.py文件。如果同一目录中没有,解释器将会在“path”中寻找,如果还找不到,会抛出importError。
一旦找到modu.py,Python解释器将会运行这个模块,任何模块中的顶级语句讲执行,包括import语句和其他的一些东西。函数和类定义保存在模块字典中。
然后,模块中的值,函数,类将对导入者可用,通过模块的命名空间,命名空间是编程中一个重要概念。
许多其他语言中使用include文件时,预处理时就把所有的代码都导入到引用文件中。Python是不同的,导入的代码孤立的保存在模块的命名空间中,所有你不用担心引入的代码引起麻烦,或者是重载了已有同名函数。
可以模拟更多的标准行为,通过使用特殊的import语法,from modu import *。这通常被认为是个坏习惯。使用import *使得代码难以阅读,还使得依赖关系不易区分。
使用from modu import func是一种把函数引入全局命名空间中的一种方式。这比import*的坏处小得多,它明确指定了引入全局的函数,它的好处是比起import modu的唯一好处就是可以少敲几个字而已。
Very bad
[...]
frommoduimport*
[...]
x=sqrt(4) # Is sqrt partof modu? A builtin? Defined above?
Better
frommoduimportsqrt
[...]
x=sqrt(4) # sqrt may bepart of modu, if not redefined in between
Best
importmodu
[...]
x=modu.sqrt(4) # sqrt isvisibly part of modu's namespace
代码风格这一节当中,可读性是Python重要特色之一。可读意味着避免无用的引用和杂乱,因此要花费些精力来保证整洁。但是简洁也是有限度的。能够告知一个类或者函数来自于哪个模块在modu.func模板中,将大大提高模块的可读性和理解性,这是最简单的单文件工程。
Packages包
Python提供了非常直接的包系统,仅仅是把单文件扩展到目录。
任何目录如果有__init__.py文件,那么就被认为是一个Python的包。包中不同的文件有类的功能作为一个普通的模块,但是__init__.py如果有特殊行为,组织所有包的定义。看不太懂附上原文(The different modules in the package are imported in a similar manner asplain modules, but with a special behavior for the __init__.py file, which is used to gather all package-wide definitions.)
Modu.py文件在目录pack/时,用import pack.modu语句来引入。这个语句将会再pack目录下找__init__.py文件,执行所有顶层语句,然后然后寻找pack目录下的modu.py然后执行它的顶级语句。进行完这些操作以后,任何值,函数,或者类在modu.py中的,都包含在pack.modu的命名空间中。
一个常见的问题是__init__.py中添加了太多代码。当工程复杂性见长,会有许多下层的包,导致路径很深。这种情况下,引入很深层的一个对象时,需要加载所有路径中遇到的__init__.py。
把__init__.py文件置空被认为是一个正常甚至是推荐的惯例,如果包和模块和子包的模块不用共享代码。
最后,一个使用的语法在引入深层的包时:import very.deep.module as mod。这允许你用mod来来重复使用very.deep.module
面向对象编程
python有时被描述为一个面向对象编程语言。这可能有些误导作用,并且需要澄清一下。
此外,通过模块那一章节所讲的,Python处理模块和命名空间的方式给开发人员一个自然的方式确保封装和抽象层的剥离。所以程序员开发时,只要商务模型没有需求,那么不用过深得使用OO。
应该避免不必要的OO。定义自定义的类来组织语句和功能。函数式编程探讨中指出的问题,语句也是状态的一部分。
在一些架构当中,典型的web应用,多种Python实例同时运行可以同时相应多个请求。这种情况下把一些语句放进实例中,意味着保持静态,在并发和竞态情况下。有时,在初始化过程中,对象的状态发生改变,从而保存得状态已经过期了。比如一个请求请求一个对象,转成可读的给用户,如果另一个请求要删除这个对象同时发生,那么就可能发生请求一个已经删除了得对象。
这和其他问题导致使用无状态的函数是一个更好的编程范式。
建议使用函数式编程因为它要求上下文更少,并且副作用也小。一个函数的隐式上下文是函数内部访问任何全局变量或物品的持久层。副作用是隐式上下文的变化。如果一个函数保存或者删除全局变量或是数据持久层,那么就是有副作用。
小心地隔离功能与上下文和副作用的函数(称为纯函数)允许有以下好处:
•纯函数是确定的:给一个固定的输入,输出将永远是相同的。
•纯函数更容易改变或替换,如果他们需要重构或优化。
•纯函数更容易测试与单元测试:不需要复杂的上下文设置和数据清理。
•纯函数更容易操纵,装饰和传递。
总之,纯函数,没有任何上下文或副作用,更有效的构建块类和对象的架构。
显然,面向对象是有用的,甚至是必要的在许多情况下,例如当开发图形化桌面应用程序或游戏,操纵的事情(窗口、按钮、头像、车辆)有一个相对寿命长自己的计算机的内存。