第1章
引论
“罗马不是一天建成的”,编写代码水平的提升也不可能一蹴而就,通过一点一滴的积累,才能达成从量变到质变的飞跃。这种积累可以从很多方面取得,如一些语言层面的使用技巧、常见的注意事项、编程风格等。本章主要探讨Python中常见的编程准则,从而帮助读者进一步理解Pythonic的本质。本章内容包括如何编写Pythonic代码、在实际应用中需要注意的一些事项和值得提倡的一些做法。希望读者通过对本章的学习,可以在实际应用Pythonic的过程中得到启发和帮助。
建议1:理解Pythonic概念
什么是Pythonic?这是很难定义的,这就是为什么大家无法通过搜索引擎找到准确答案的原因。但很难定义的概念绝非意味着其定义没有价值,尤其不能否定它对编写优美的Python代码的指导作用。
对于Pythonic的概念,众人各有自己的看法,但大家心目之中都认同一个更具体的指南,那就是Tim Peters的《The Zen of Python》(Python之禅)。在这一充满着禅意的诗篇中,有几点非常深入人心:
美胜丑,显胜隐,简胜杂,杂胜乱,平胜陡,疏胜密。
找到简单问题的一个方法,最好是唯一的方法(正确的解决之道)。
难以解释的实现,源自不好的主意;如有非常棒的主意,它的实现肯定易于解释。
不仅这几点,其实《Python之禅》中的每一句都可作为编程的信条。是的,不仅是作为编写Python代码的信条,以它为信条编写出的其他语言的代码也会非常漂亮。
(1)Pythonic的定义
遵循Pythonic的代码,看起来就像是伪代码。其实,所有的伪代码都可以轻易地转换为可执行的Python代码。比如在Wikipedia的快速排序条目中有如下伪代码:
function quicksort('array')
if length('array') ≤ 1
return 'array' // an array of zero or one elements is already sorted
select and remove a pivot element 'pivot' from 'array' // see 'Choice of pivot' below
create empty lists 'less' and 'greater'
for each 'x' in 'array'
if 'x' ≤ 'pivot' then append 'x' to 'less'
else append 'x' to 'greater'
. return concatenate(quicksort('less'), list('pivot'), quicksort('greater'))
// two recursive calls
实际上,它可以转化为以下同等行数的可以执行的Python代码:
def quicksort(array):
less = []; greater = []
if len(array) <= 1:
return array
pivot = array.pop()
for x in array:
if x <= pivot: less.append(x)
else: greater.append(x)
return quicksort(less)+[pivot]+quicksort(greater)
看,行数一样的Python代码甚至可读性比伪代码还要好吧?但它真的可以运行,结果如下:
)))quicksort([9,8,4,5,32,64,2,1,0,10,19,27])
[0,1,2,4,5,8,9,10,19,27,32,64]
所以,综合这个例子来说,Pythonic也许可以定义为:充分体现Python自身特色的代码风格。接下来就看看这样的代码风格在实际中是如何体现的。
(2)代码风格
对于风格,光说是没有用的,最好是通过例子来看看,因为例子看得见,会显得更真实。下面以语法、库和应用程序为例给大家介绍。
在语法上,代码风格要充分表现Python自身特色。举个最常见的例子,在其他的语言(如C语言)中,两个变量交换需要如下的代码:
int a = 1, b = 2;
int tmp = a;
a = b;
b = tmp;
利用Python的packaging/unpackaging机制,Pythonic的代码只需要以下一行:
a, b = b, a
还有,在遍历一个容器的时候,类似其他编程语言的代码如下:
length = len(alist)
i = 0
while i < length:
do_sth_with(alist[i])
i += 1
而 Pythonic的代码如下:
for i in alist:
do_sth_with(i)
灵活地使用迭代器是一种Python风格。又比如,需要安全地关闭文件描述符,可以使用以下with语句:
with open(path, 'r') as f:
do_sth_with(f)
通过上述代码的对比,能让大家清晰地认识到Pythonic的一个要求,就是对Python语法本身的充分发挥,写出来的代码带着Python味儿,而不是看着像C语言代码,或者Java代码。
应当追求的是充分利用Python语法,但不应当过分地使用奇技淫巧,比如利用Python的Slice语法,可以写出如下代码:
a = [1,2,3,4]
c = 'abcdef'
print a[::-1]
print c[::-1]
如果不是同样追求每一个语法细节的“老鸟”,这段代码的作用恐怕不能一眼就看出来。实际上,这个时候更好地体现Pythonic的代码是充分利用Python库里reversed()函数的代码。
print list(reversed(a))
print list(reversed(c))
(3)标准库
写Pythonic程序需要对标准库有充分的理解,特别是内置函数和内置数据类型。比如,对于字符串格式化,一般这样写:
print 'Hello %s!' % ('Tom',)
其实%s是非常影响可读性的,因为数量多了以后,很难清楚哪一个占位符对应哪一个实参。所以相对应的Pythonic代码是这样的:
print 'Hello %(name)s!' % {'name': 'Tom'}
这样在参数比较多的情况下尤其有用。
#字符串
value = {'greet': 'Hello world', 'language': 'Python'}
print '%(greet)s from %(language)s.' % value
%占位符来自于大家的先验知识,其实对于新手而言,有点“莫名其妙”,所以更具有Pythonic风格的代码如下:
print '{greet} from {language}.'.format(greet = 'Hello world', language = 'Python')
str.format()方法非常清晰地表明了这条语句的意图,而且模板的使用也减少了许多不必要的字符,使可读性得到了很大的提升。事实上,str.format()也成了Python最为推荐的字符串格式化方法,当然也是最Pythonic的。
(4)Pythonic的库或框架
编写应用程序的时候的要求会更高一些。因为编写应用程序一般需要团队合作,那么可能你编写的那一部分正好是团队的另一成员需要调用的接口,换言之,你可能正在编写库或框架。
程序员利用Pythonic的库或框架能更加容易、更加自然地完成任务。如果用Python编写的库或框架迫使程序员编写累赘的或不推荐的代码,那么可以说它并不Pythonic。现在业内通常认为Flask这个框架是比较Pythonic的,它的一个Hello world级别的用例如下:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello world!"
if __name__ == "__main__":
app.run()
稍有编程经验的人都可以通过上例认识到利用Python编程极为容易这一事实。一个Pythonic的框架不会对已经通过惯用法完成的东西重复发明“轮子”,而且它也遵循常用的Python惯例。创建Pythonic的框架极其困难,什么理念更酷、更符合语言习惯对此毫无帮助,事实上这些年来优秀的Python代码的特性也在不断演化。比如现在认为像generators之类的特性尤为Pythonic。
另一个有关新趋势的例子是:Python的包和模块结构日益规范化。现在的库或框架跟随了以下潮流:
包和模块的命名采用小写、单数形式,而且短小。
包通常仅作为命名空间,如只包含空的__init__.py文件。
建议2:编写Pythonic代码
如何编写更加Pythonic的代码,与定义什么是Pythonic一样困难。在这里,只能给出一些经验之谈,希望对大家有所帮助。
(1)要避免劣化代码
与优化代码对应,劣化代码就是一开始写出来就是不合理的代码,比如不合适的变量命名等。通常有以下几个值得注意的地方:
1)避免只用大小写来区分不同的对象。如a是一个数值类型变量,A是String类型,虽然在编码过程中很容易区分二者的含义,但这样做毫无益处,它不会给其他阅读代码的人带来多少便利。
2)避免使用容易引起混淆的名称。容易引起混淆的名称的使用情形包括:重复使用已经存在于上下文中的变量名来表示不同的类型;误用了内建名称来表示其他含义的名称而使之在当前命名空间被屏蔽;没有构建新的数据类型的情况下使用类似于element、list、dict等作为变量名;使用o(字母O小写的形式,容易与数值0混淆)、l(字母L小写的形式,容易与数值1混淆)等作为变量名。因此推荐变量名与所要解决的问题域一致。有如下两个示例,示例二比示例一更好。
示例一:
))) def funA(list,num):
... for element in list:
... if num==element:
... return True
... else:
... pass
...
示例二:
))) def find_num(searchList,num):
... for listValue in searchList:
... if num==listValue:
... return True
... else:
... pass
...
3)不要害怕过长的变量名。为了使程序更容易理解和阅读,有的时候长变量名是必要的。不要为了少写几个字母而过分缩写。下例是一个用来保存用户信息的字典结构,变量名person_info比pi的可读性要强得多。
))) person_info={'name':'Jon','IDCard':'200304','address':'Num203,Monday Road',
'email':'test@gail.com'}
(2)深入认识Python有助于编写Pythonic代码
可以从以下几个方面进行着手:
全面掌握Python提供给我们的所有特性,包括语言特性和库特性。其中最好的学习方式应该是通读官方手册中的Language Reference和Library Reference。掌握了语言特性和库特性,以后许多“惯用法”自然而然就掌握了,写代码的时候,自然会使用常见的、公认的、简短的惯用法来实现预期效果,也使得代码显得尤为Pythonic。
随着Python的版本更新、时间的推移,Python语言不断演进,社区不断成长,还需要学习每个Python新版本提供的新特性,以及掌握它的变化趋势。从另一角度来看,一方面Python语言推荐使用大量的惯用法来完成任务(“完成任务的唯一方法”);另一方面,社区不断演变的新惯用法反过来又影响了语言的进化,以更好地支持惯用法。比如早年的Pythonista常用dict.has_key()方法来判断字典对象是否包含某个元素,但新版本的Python中提供了in操作符(支持多种容器类型)取代它。改变习惯的阻力很大,而克服这些阻力的唯一方法就是加深对Python的认识,因为在语言支持正确的惯用法之后,非推荐的代码通常执行起来更慢。所以说,不更新知识是不行的。
深入学习业界公认的比较Pythonic的代码,比如Flask、gevent和requests等。以requests这个通过HTTP(HTTPS)协议获取网络资源的程序库为例,要获取带有Basic Auth的网络资源时,代码如下:
import requests
r = requests.get('https://api.github.com', auth=('user', 'pass'))
print r.status_code
print r.headers['content-type']
# ------
# 200
# 'application/json'
而使用Python标准库httplib2时,代码就非常复杂,程序员需要了解相当多的关于HTTP协议和Basic Auth的知识才能编程。
import urllib2gh_url = 'https://api.github.com'
req = urllib2.Request(gh_url)
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, gh_url, 'user', 'pass')
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)
urllib2.install_opener(opener)
handler = urllib2.urlopen(req)
print handler.getcode()
print handler.headers.getheader('content-type')
# ------
# 200
# 'application/json'
看,使用一个Pythonic的程序库可以简化很多工作量!那么深入学习理解类似requests的高质量程序库带给我们的收获应该完全可以预期:一定是非常大的!
最后,除了修炼内功外,也可以尝试利用工具达到事半功倍的效果。所以接下来介绍风格检查程序PEP8。其实一开始PEP8是一篇关于Python编码风格的指南,它提出了保持代码一致性的细节要求。它至少包括了对代码布局、注释、命名规范等方面的要求,在代码中遵循这些原则,有利于编写Pythonic的代码。比如,对代码的换行,不好的风格如下:
if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()
而 Pythonic 的风格则是这样的:
if foo == 'blah':
do_blah_thing()
do_one()
do_two()
do_three()
如果要“人肉”检查代码是否符合PEP8规范,则比较困难,而且容易跟同僚引发争论。所以Johann C. Rocholl开发了一个应用程序来进行检测,就是应用程序PEP8。当然,它是使用Python开发的,安装它非常容易。
$ pip install -U pep8
在自己的shell中执行这一命令就可以安装成功了(首先需要安装pip)。然后即可简单地用它检测一下自己的代码。
$ pep8 --first optparse.py
optparse.py:69:11: E401 multiple imports on one line
optparse.py:77:1: E302 expected 2 blank lines, found 1
optparse.py:88:5: E301 expected 1 blank line, found 0
optparse.py:222:34: W602 deprecated form of raising exception
optparse.py:347:31: E211 whitespace before '('
optparse.py:357:17: E201 whitespace after '{'
optparse.py:472:29: E221 multiple spaces before operator
optparse.py:544:21: W601 .has_key() is deprecated, use 'in'
可以看到上面有许多错误和警告,然后我们按图索骥逐一修复它们就可以了。如果嫌这种报表不够细致,可以考虑使用--show-source参数让PEP8显示每一个错误和警告对应的代码。
$ pep8 --show-source --show-pep8 testsuite/E40.py
testsuite/E40.py:2:10: E401 multiple imports on one line
import os, sys
^
Imports should usually be on separate lines.
Okay: import os\nimport sys
E401: import sys, os
看,它甚至可以给出“正确”的写法!除了针对某一个源代码文件以外,它还可以直接检测一个项目的质量,并通过直观的报表给出报告。
$ pep8 --statistics -qq Python-2.5/Lib
232 E201 whitespace after '['
599 E202 whitespace before ')'
631 E203 whitespace before ','
842 E211 whitespace before '('
2531E221 multiple spaces before operator
4473E301 expected 1 blank line, found 0
4006E302 expected 2 blank lines, found 1
165 E303 too many blank lines (4)
325 E401 multiple imports on one line
3615E501 line too long (82 characters)
612 W601 .has_key() is deprecated, use 'in'
1188W602 deprecated form of raising exception
PEP8有优秀的插件架构,可以方便地实现特定风格的检测(例如,有些公司、团队会定义自己的风格);它生成的报告易于处理,可以很方便地与编辑器集成(例如,实现点击出错信息跳转到相应代码行)。所以这是一个非常有用的工具,它可以提升你对Pythonic的认识,达到编写高质量代码的目的。
PEP8不是唯一的编程规范,事实上,有些公司制定的编程规范也非常有参考意义,比如Google Python Style Guide。同样,PEP8也不是唯一的风格检测程序,类似的应用还有Pychecker、Pylint、Pyflakes等。其中,Pychecker是Google Python Style Guide推荐的工具;Pylint因可以非常方便地通过编辑配置文件实现公司或团队的风格检测而受到许多人的青睐;Pyflakes则因为易于集成到vim中,所以使用的人也非常多。
其实Pythonic的代码,往往是放弃自我风格的代码,而要有“放弃自我风格”的觉悟,是非常困难、非常痛苦的。要突破这种瓶颈,完成自我蜕变,除了需要付出许多精力去学习外,参考更好的书籍进行辅助也是相当有帮助的。目前市面上针对编写“高质量”的Python程序的方法的书籍并不多,本书应是一本比较好的参考资料。作为作者,我们也真心希望自己的一点点经验分享能够对读者有所帮助。
建议3:理解Python与C语言的不同之处
我们都知道,Python底层是用C语言实现的,但切忌用C语言的思维和风格来编写Python代码。对于那些在学习Python之前有其他编程语言(如Java、C#等)经验的程序员来说,尤其重要的是:不要使用之前的编程思想。Python与它们有很多不同,以下从语法方面来进行简单分析。
(1)“缩进”与“{}”
与C、C++、Java等语言使用花括号{}来分隔代码段不同,Python中使用严格的代码缩进方式分隔代码块,空格或者Tab键不再是你心血来潮的时候可以随便敲敲解闷的了,对于Python解释器而言,它们直接关乎代码的语法和逻辑,一不小心就会出现unexpected indent错误。Python的这一特点也曾引起不少争议,强制代码缩进就像一把双刃剑,有利有弊。对于从其他编程语言转过来学习Python的人来说,也许需要一段时间去适应。但不可否认,严格的缩进确实能让代码更加规范、整齐,可读性和可维护性都会更高。避免缩进带来的困扰的方法之一就是养成良好的习惯,统一缩进风格,不要混用Tab键和空格。
(2)'与"
C语言中单引号(')与双引号(")有严格的区别,单引号代表一个字符,它实际对应于编译器所采用的字符集中的一个整数值。例如在ASCII码中,'a'与97相对应。而双引号则表示字符串,默认以'\0'结尾。但在Python中,单引号与双引号没有明显区别,仅仅在输入字符串内容不同时,在使用上存在微小差异。
))) string1 = "He said, \"Hello\"" # 字符串中本身的双引号需要转义
))) string1
'He said, "Hello"'
))) string2 = 'He said,"Hello"' #字符串本身的双引号不需要转义
))) string2
'He said,"Hello"'
(3)三元操作符“?:”
三元操作符是if...else的简写方法,语法形式为C ? X: Y,它表示当条件C为True的时候取值X,C为False的时候取值为Y。其简洁的表达形式一直很受欢迎。但在Python 2.5之前并不支持三元操作符。为此,人们想出了不少替代方式,但在特殊情形下存在一些问题,因此很多人对Python语言本身加入该特征也提出了不少建议,最终PEP308被接受,根据提议采用if...else...形式实现条件表达式。C ? X: Y在Python中等价的形式为X if C else Y,即:
))) X=0
))) Y=-2
))) print X if X<Y else Y
-2
(4)switch...case
Python中没有像C语言那样的switch...case分支语句,不过这不是什么难事,Python中有很多替代的解决方法。假设,用C语言实现的switch...case语句如下:
switch(n) {
case 0:
printf("You typed zero.\n");
break;
case 1:
printf("You are in top.\n");
break;
case 2:
printf("n is an even number\n");
default:
printf("Only single-digit numbers are allowed\n");
break;
}
与以上C语言中switch... case对应的Python实现如下:
if n == 0:
print "You typed zero.\n"
elif n== 1:
print "You are in top.\n"
elif n == 2:
print "n is an even number\n"
else:
print "Only single-digit numbers are allowed\n"
或者使用以下跳转表来实现:
def f(x):
return {
0: "You typed zero.\n",
1: "You are in top.\n",
2: "n is an even number\n"
}.get(n, "Only single-digit numbers are allowed\n")
以上只是简单列举了几个Python和其他语言的不同点,事实上,其差异性远远不止这些。但总归一句话:不要被其他语言的思维和习惯困扰,掌握Python的哲学和思维方式才是硬道理。正如前面所说,要舍得抛弃具有自我风格的代码,尽量遵循Pythonic代码的编码规范和风格。
建议4:在代码中适当添加注释
Python中有3种形式的代码注释:块注释、行注释以及文档注释(docstring)。这3种形式的惯用法大概有如下几种:
1)使用块或者行注释的时候仅仅注释那些复杂的操作、算法,还有可能别人难以理解的技巧或者不够一目了然的代码。
2)注释和代码隔开一定的距离,同时在块注释之后最好多留几行空白再写代码。下面两行代码显然第一行的阅读性要好。
x=x+1# increace x by 1①
x=x+1 #increase x by 1 ②
3)给外部可访问的函数和方法(无论是否简单)添加文档注释。注释要清楚地描述方法的功能,并对参数、返回值以及可能发生的异常进行说明,使得外部调用它的人员仅仅看docstring就能正确使用。较为复杂的内部方法也需要进行注释。推荐的函数注释如下:
def FuncName(parameter1 , parameter2):
"""Describe what this function does.
#such as "Find whether the special string is in the queue or not"
Args:
parameter1: parameter type, what is this parameter used for.
#such as strqueue:string,string queue list for search
parameter2: parameter type, what is this parameter used for.
#such as str:string,string to find
Returns:
return type, return value.
#such as boolean,sepcial string found return True,else return False
"""
function body
...
...
4)推荐在文件头中包含copyright申明、模块描述等,如有必要,可以考虑加入作者信息以及变更记录。
"""
Licensed Materials - Property of CorpA
(C) Copyright A Corp. 1999, 2011 All Rights Reserved
CopyRight statement and purpose...
--------------------------------------------------------------------------
File Name: comments.py
Description : description what the main function of this file
Author: Author name
Change Activity:
list the change activity and time and author information.
--------------------------------------------------------------------------
"""
有人说,写代码就像写诗,你见过谁在自己写的诗里加注释吗?这种说法受到许多人的追捧,包括一些Python程序员。但我的看法是,代码跟诗不太一样,需要适当添加注释。注释直接关系到代码的可读性和可维护性,同时它还能够帮助发现错误的藏身之处。因为代码是说明你怎么做的,而好的注释能够说清楚你想做什么,它们相辅相成。但往往有些程序员并不重视它,原因是多方面的,有人觉得程序的实现才是最重要的,至于注释是一件浪费时间的事情;还有的人明明知道注释很重要,可是太懒,不愿意写或者随便应付;也有人重视注释但却不得要领,反而使其成为代码的一种累赘。下面针对以上几个心态举几个实际中常见例子。
示例一:代码即注释(不写注释)。没有注释的代码通常会给他人的阅读和理解带来一定困难,即使是自己写的代码,过一段时间再回头阅读可能也需要一定时间才能理解当初的思路。
a=3
n=5
count,sn,tn=1,0,0
while count<=n:
tn+=a
sn+=tn
a*=10
count+=1
print sn
示例二:注释与代码重复。注释应该是用来解释代码的功能、原因以及想法的,而不是对代码本身的解释。
# coding=utf-8
print "please input number n"
n=input()
print "please input number m"
m=input()
t=n/2 # t是n的一半
# 循环,条件为t*m/n小于n
while(t*m/(n+1) < n):
t=0.5*m+n/2 #重新计算t值
print t
示例三:利用注释语法快速删除代码。对于不再需要的代码,应该将其删除,而不是将其注释掉。即使你担心以后还会用到,版本控制工具也可以让你轻松找回被删除的代码。
x=2
y=5
z=3
"""following code is no longer needed since there is a better way
if x)y:t=x;x=y;y=t
if x)z:t=z;z=x;x=t
if y)z:t=y;y=z;z=t
print "the order from small to big is: %d %d %d" % (x,y,z)
"""
order_list=[x,y,z]
order_list.sort()
print order_list
其他比较常见的问题还包括:代码不断更新而注释却没有更新;注释比代码本身还复杂烦琐;将别处的注释和代码一起拷贝过来,但上下文的变更导致注释与代码不同步;更有个别人将注释当做自己的娱乐空间从而留下个性特征等,这几种行为都是平时要注意避免的。
建议5:通过适当添加空行使代码布局更为优雅、合理
布局清晰、整洁、优雅的代码能够给阅读它的人带来愉悦感,而且它能帮助开发者之间进行良好的沟通。在一个团队中,保持良好的代码格式需要团队成员在选取一套合适的代码格式规则的基础上遵从和应用。同时它需要每个团队成员树立正确的态度,因为实际工作中有很多开发者抱着这样的想法:“代码能工作才是最重要的”,但往往代码会不断修改,且可读性直接关系到可维护性和可扩展性。因此我们需要端正态度——“代码不是恒定的,只有风格才能延续,能工作的代码和整洁、优雅的格式同样重要”。
为了让读者更加深入地理解代码布局的重要性,我们先来看一个猜数字游戏的示例。下面两段代码,编码完全相同,只是在排版上做了一定修改,你觉得哪个更加容易阅读呢?
示例一:
import random
guesses_made = 0
name = raw_input('Hello! What is your name?\n')
number = random.randint(1, 20)
print 'Well, {0}, I am thinking of a number between 1 and 20.'.format(name)
while guesses_made < 6:
guess = int(raw_input('Take a guess: '))
guesses_made += 1
if guess < number:
print 'Your guess is too low.'
if guess ) number:
print 'Your guess is too high.'
if guess == number:
Break
if guess == number:
print 'Good job, {0}! You guessed my number in {1} guesses!'.format(name, guesses_made)
else:
print 'Nope. The number I was thinking of was {0}'.format(number)
示例二:
import random
guesses_made = 0
name = raw_input('Hello! What is your name?\n')
number = random.randint(1, 20)
print 'Well, {0}, I am thinking of a number between 1 and 20.'.format(name)
while guesses_made < 6:
guess = int(raw_input('Take a guess: '))
guesses_made += 1
if guess < number:print 'Your guess is too low.'
if guess ) number:print 'Your guess is too high.'
if guess == number:break
if guess == number:print 'Good job, {0}! You guessed my number in {1} guesses!'.
format(name, guesses_made)
else:print 'Nope. The number I was thinking of was {0}'.format(number)
看了上面两个例子,相信很多读者都倾向于阅读第一个例子,这就是代码布局和排版带给我们的最直观的感受(注意:本书其他章节的例子为了尽量使代码占用更少的篇幅,采取了第二种排版形式,但在实际项目开发中更推荐第一种)。和其他语言一样,Python代码布局也有一些基本规则可以遵循。
1)在一组代码表达完一个完整的思路之后,应该用空白行进行间隔。如每个函数之间,导入声明、变量赋值等。通俗点讲就是不要在一段代码中说明几件事。推荐在函数定义或者类定义之间空两行,在类定义与第一个方法之间,或者需要进行语义分割的地方空一行。如示例一在import声明和变量赋值之间插入了空行。但读者需要注意的是:空行是在不隔断代码之间内在联系的基础上插入的,也就是说有关联的代码还是需要保持紧凑、连续。在示例一中,如果你在if和else之间插入空行就显得非常没有必要,就像下面的代码:
if guess == number:
print 'Good job, {0}! You guessed my number in {1} guesses!'.format(name, guesses_made)
else:
print 'Nope. The number I was thinking of was {0}'.format(number)
2)尽量保持上下文语义的易理解性。如当一个函数需要调用另一个函数的时候,尽量将它们放在一起,最好调用者在上,被调用者在下。例如下面的代码:
def A():
B()
def B():
pass
3)避免过长的代码行,每行最好不要超过80个字符。以每屏能够显示完整代码而不需要拖动滚动条为最佳,超过的部分可以用圆括号、方括号和花括号等进行行连接,并且保持行连接的元素垂直对齐。例如下面的代码:
))) x=('this is a very long string'
... 'it is used for testing line limited characters')
))) print x
this is a very long stringit is used for testing line limited characters
)))def Draw_Line(ponitX1,pointY1,pointX2=1,pointY2=2,
color='green',width=2,style='bold'):
4)不要为了保持水平对齐而使用多余的空格,其实使阅读者尽可能容易地理解代码所要表达的意义更重要。如下列代码的主要目的是赋值,为了可以保持对齐往往会造成“喧宾夺主”。
X = 5
Year =2013
name ="Jam"
d2 = {'spam': 2,'eggs': 3}
同时也不要在一行有多个命令,如不要将X=1;Y=2;直接写在一行中。
5)空格的使用要能够在需要强调的时候警示读者,在疏松关系的实体间起到分隔作用,而在具有紧密关系的时候不要使用空格。具体细节如下:
二元运算符(赋值(=),比较(==,<,),!=,<),<=,)=,in,not in,is,is not)、布尔运算(and,or,not))的左右两边应该有空格,如x == 1。
逗号和分号前不要使用空格。
推荐: if x == 4: print x, y; x, y = y, x
不推荐: if x == 4 : print x , y ; x , y = y , x
函数名和左括号之间、序列索引操作时序列名和[ ]之间不需要空格,函数的默认参数两侧不需要空格。
强调前面的操作符的时候使用空格,如 -2 - 5(在-2和5之间的减号前后需要添加空格)、b*b + a*c(在加号前后需要添加空格)。
建议6:编写函数的4个原则
函数能够带来最大化的代码重用和最小化的代码冗余。精心设计的函数不仅可以提高程序的健壮性,还可以增强可读性、减少维护成本。先来看以下示例代码:
def SendContent(ServerAdr,PagePath,StartLine,EndLine,sender,
receiver,smtpserver,username,password):
http = httplib.HTTP(ServerAdr)
http.putrequest('GET', PagePath)
http.putheader('Accept', 'text/html')
http.putheader('Accept', 'text/plain')
http.endheaders()
httpcode, httpmsg, headers = http.getreply()
if httpcode != 200:raise "Could not get document: Check URL and Path."
doc = http.getfile()
data = doc.read()
doc.close()
lstr=data.splitlines()
j=0
for i in lstr:
j=j+1
if i.strip() == StartLine: slice_start=j #find slice start
elif i.strip() == EndLine: slice_end=j #find slice end
subject = "Contented get from the web"
msg = MIMEText(string.join(lstr[slice_start:slice_end]),'plain','utf-8')
msg['Subject'] = Header(subject, 'utf-8')
smtp = smtplib.SMTP()
smtp.connect(smtpserver)
smtp.login(username, password)
smtp.sendmail(sender, receiver, msg.as_string())
smtp.quit()
函数SendContent主要的作用是抓取网页中固定的内容,然后将其发送给用户。代码本身并不复杂,但足以说明一些问题。读者可以先思考一下:到底有什么不是之处?能否进一步改进?怎样才能做到一目了然呢?一般来说函数设计有以下基本原则可以参考:
原则1函数设计要尽量短小,嵌套层次不宜过深。所谓短小,就是跟前面所提到的一样尽量避免过长函数,因为这样不需要上下拉动滚动条就能获得整体感观,而不是来回翻动屏幕去寻找某个变量或者某条逻辑判断等。函数中需要用到if、elif、while、for等循环语句的地方,尽量不要嵌套过深,最好能控制在3层以内。相信很多人有过这样的经历:为了弄清楚哪段代码属于内部嵌套,哪段属于中间层次的嵌套,哪段属于更外一层的嵌套所花费的时间比读代码细节所用时间更多。
原则2函数申明应该做到合理、简单、易于使用。除了函数名能够正确反映其大体功能外,参数的设计也应该简洁明了,参数个数不宜太多。参数太多带来的弊端是:调用者需要花费更多的时间去理解每个参数的意思,测试人员需要花费更多的精力来设计测试用例,以确保参数的组合能够有合理的输出,这使覆盖测试的难度大大增加。因此函数参数设计最好经过深思熟虑。
原则3函数参数设计应该考虑向下兼容。实际工作中我们可能面临这样的情况:随着需求的变更和版本的升级,在前一个版本中设计的函数可能需要进行一定的修改才能满足这个版本的要求。因此在设计过程中除了着眼当前的需求还得考虑向下兼容。如以下示例:
))) def readfile(filename): ①函数实现的第一版本
... print "file read completed"
...
))) readfile("test.txt")
file read completed
)))
))) import logging
)))
))) def readfile(filename,logger): ②函数实现的第二版本
... print "file read completed"
...
))) readfile("test.txt")
Traceback (most recent call last):
File "<stdin)", line 1, in <module)
TypeError: readfile() takes exactly 2 arguments (1 given)
上面的代码是相同功能的函数不同版本的实现,唯一不同的是在更高级的版本中为了便于内部维护加入了日志处理,但这样的变动却导致程序中函数调用的接口发生了改变。这并不是最佳设计,更好的方法是通过加入默认参数来避免这种退化,做到向下兼容。上例可以将第一行代码修改为:
def readfile(filename,logger=logger.info):
原则4一个函数只做一件事,尽量保证函数语句粒度的一致性。如本节开头所示代码中就有3个不同的任务:获取网页内容、查找指定网页内容、发送邮件。要保证一个函数只做一件事,就要尽量保证抽象层级的一致性,所有的语句尽量在一个粒度上。如上例既有http.getfile()这样较高层级抽象的语句,也有细粒度的字符处理语句。同时在一个函数中处理多件事情也不利于代码的重用,在上例中,如果程序中还有类似发送邮件的需求,必然造成代码的冗余。
最后,根据以上几点原则,上面对本节最开始处的代码进行修改,来看看修改后的代码是不是可读性要好一些。
def GetContent(ServerAdr,PagePath):
http = httplib.HTTP(ServerAdr)
http.putrequest('GET', PagePath)
http.putheader('Accept', 'text/html')
http.putheader('Accept', 'text/plain')
http.endheaders()
httpcode, httpmsg, headers = http.getreply()
if httpcode != 200:
raise "Could not get document: Check URL and Path."
doc = http.getfile()
data = doc.read() # read file
doc.close()
return data
def ExtractData(inputstring, start_line, end_line):
lstr=inputstring.splitlines() #split
j=0 #set counter to zero
for i in lstr:
j=j+1
if i.strip() == start_line: slice_start=j #find slice start
elif i.strip() == end_line: slice_end=j #find slice end
return lstr[slice_start:slice_end] #return slice
def SendEmail(sender,receiver,smtpserver,username,password,content):
subject = "Contented get from the web"
msg = MIMEText(content,'plain','utf-8')
msg['Subject'] = Header(subject, 'utf-8')
smtp = smtplib.SMTP()
smtp.connect(smtpserver)
smtp.login(username, password)
smtp.sendmail(sender, receiver, msg.as_string())
smtp.quit()
Python中函数设计的好习惯还包括:不要在函数中定义可变对象作为默认值,使用异常替换返回错误,保证通过单元测试等。
建议7:将常量集中到一个文件
Python中存在常量吗?相信很多人的答案是否定的。实际上Python的内建命名空间是支持一小部分常量的,如我们熟悉的True、False、None等,只是Python没有提供定义常量的直接方式而已。那么,在Python中应该如何使用常量呢?一般来说有以下两种方式:
通过命名风格来提醒使用者该变量代表的意义为常量,如常量名所有字母大写,用下划线连接各个单词,如MAX_OVERFLOW、TOTAL。然而这种方式并没有实现真正的常量,其对应的值仍然可以改变,这只是一种约定俗成的风格。
通过自定义的类实现常量功能。这要求符合“命名全部为大写”和“值一旦绑定便不可再修改”这两个条件。下面是一种较为常见的解决方法,它通过对常量对应的值进行修改时或者命名不符合规范时抛出异常来满足以上常量的两个条件。
class _const:
class ConstError(TypeError): pass
class ConstCaseError(ConstError): pass
def __setattr__(self, name, value):
if self.__dict__.has_key(name):
raise self.ConstError, "Can't change const.%s" % name
if not name.isupper():
raise self.ConstCaseError, \
'const name "%s" is not all uppercase' % name
self.__dict__[name] = value
import sys
sys.modules[__name__]=_const()
如果上面的代码对应的模块名为const,使用的时候只需要import const,便可以直接定义常量了,如以下代码:
import const
const.COMPANY = "IBM"
上面的代码中常量一旦赋值便不可再更改,因此const.COMPANY = "SAP"会抛出const.ConstError:异常,而常量名称如果小写,如const.name = "Python",也会抛出const.ConstCaseError异常。
无论采用哪一种方式来实现常量,都提倡将常量集中到一个文件中,因为这样有利于维护,一旦需要修改常量的值,可以集中统一进行而不是逐个文件去检查。采用第二种方式实现的常量可以这么做:将存放常量的文件命名为constant.py,并在其中定义一系列常量。
class _const:
class ConstError(TypeError): pass
class ConstCaseError(ConstError): pass
def __setattr__(self, name, value):
if self.__dict__.has_key(name):
raise self.ConstError, "Can't change const.%s" % name
if not name.isupper():
raise self.ConstCaseError, \
'const name "%s" is not all uppercase' % name
self.__dict__[name] = value
import sys
sys.modules[__name__]=_const()
import const
const.MY_CONSTANT = 1
const.MY_SECOND_CONSTANT = 2
const.MY_THIRD_CONSTANT = 'a'
const.MY_FORTH_CONSTANT = 'b'
当在其他模块中引用这些常量时,按照如下方式进行即可:
from constant import const
print const.MY_SECOND_CONSTANT
print const.MY_THIRD_CONSTANT*2
print const.MY_FORTH_CONSTANT+'5'