Python,我只写坠吊的
阅读本文大概需要 11 分钟。
今天讨论 Python 编程风格,如何写出更加Pythonic的代码是本篇讨论的话题。
基本目录结构:
1 基本编程习惯
1.1 多余的空格
1.2 是否为 None 判断
1.3 lamda 表达式
1.4 最小化受保护代码
1.5 保持逻辑完整性
1.6 使用语义更加明确的方法
2 EAFP 防御编程风格
3 LBYL 防御编程风格
3.1 程序每次运行都要检查
3.2 很难一次考虑所有可能异常
3.3 代码的可读性下降
1 基本编程习惯
Python代码的编程习惯主要参考PEP8
:
https://www.python.org/dev/peps/pep-0008/
里面主要包括如每行代码长度不超过80,函数间空一行等。在此我强烈建议大家都去读一下google编写的python规范。
同时,我们可以使用一些好用的小工具辅助我们写出更加符合习惯的Python代码,如flake8
等小插件。
结合以上这些参考资料和工具,我们这篇专题总结就不会过多去讲语法相关的格式化。而是更多精力放在一些典型的、常用的对比分析上,告诉大家常用的代码书写习惯,哪些写法不够符合习惯等。
1.1 多余的空格
以下函数赋值符合习惯:
foo(a, b=0, {'a':1,'b':2}, (10,))
但是,下面出现的多余空格都不符合习惯:
# 这些空格都是多余的
foo ( a, b = 0, { 'a':1, 'b':2 }, (10, ))
下面代码,有空格又更符合习惯:
i += 1
num = num**2 + 1
def foo(nums: List)
尤其容易忽略的一个空格,增加函数元信息时要有一个空格:
def foo(nums: list): # 此处根据官方建议nums: list间要留有一个空格
pass
1.2 是否为 None 判断
判断某个对象是否为None
,下面符合习惯:
if arr is None:
pass
if arr is not None:
pass
下面写法不符合习惯,一般很少见:
if arr == None:
pass
特别的,对于list
,tuple
,set
,dict
,str
等对象,使用下面方法判断是否为None
更加符合习惯:
if not arr: #为 None 时,满足条件
pass
if arr: # 不为 None 时,满足条件
pass
1.3 lamda 表达式
lambda 表达式适合一些key参数赋值等,一般不习惯这么写:
f = lambda i: i&1
下面写法更加符合习惯:
def is_odd(i): return i&1
1.4 最小化受保护代码
要想代码更健壮,我们一般都做防御性的工作,最小化受保护的代码更加符合习惯,如下为了防御键不存在问题,加一个try:
try:
val = d['c']
except KeyError:
print('c' not existence)
上面写法是合理的,但是下面代码在捕获KeyError时,又嵌套一个函数是不符合习惯的:
try:
val = foo(d['c']) # 这样写也会捕获foo函数中的KeyError异常
except KeyError:
print('c' not existence)
这样写也会捕获foo函数中的KeyError异常,不符合习惯。
1.5 保持逻辑完整性
根据官方指南,只有if
逻辑return,而忽视可能的x
为负时的else
逻辑,不可取:
def foo(x):
if x >= 0:
return math.sqrt(x)
建议写法:
def foo(x):
if x >= 0:
return math.sqrt(x)
else:
return None
或者这样写:
def foo(x):
if x < 0:
return None
return math.sqrt(x)
所以,不要为了刻意追求代码行数最少,而忽视使用习惯。
1.6 使用语义更加明确的方法
判断字符串是否以ize
结尾时,不建议这样写:
if s[-3:] == 'ize':
print('ends ize')
使用字符串的endswith
方法判断是否以什么字符串结尾,显然可读性更好:
if s.endswith('ize'):
print('ends ize')
以上这些只要平时多加注意,理解起来不是问题。其实除了PEP8指定的这些代码编写习惯外,还有一种与代码健壮性息息相关的编程风格,今天重点介绍这方面的编程习惯。
2 EAFP 防御编程风格
为了提升代码的健壮性,我们要做防御性编程,Python中的try
和except
就是主要用来做这个:
d = {'a': 1, 'b': [1, 2, 3]}
try:
val = d['c']
except KeyError:
print('key not existence')
try
块中代码是受保护的,如果键不存在,except
捕获到KeyError
异常,并处理这个异常信息。
而下面的代码,一旦从字典中获取不存在的键,如果没有任何try
保护,则程序直接中断在这里,表现出来的现象就是app直接挂掉或闪退,这显然非常不友好。
d = {'a': 1, 'b': [1, 2, 3]}
val = d['c']
再举一个try
和except
使用的例子,如果目录已存在则触发OSError
异常,并通过except
捕获到然后在块里面做一些异常处理逻辑。
import os
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise # PermissionError 等异常
else:
# path 目录已存在
以上这种使用try
和except
的防御性编程风格,在Python中有一个比较抽象的名字:EAFP
它的全称为:
Easier to Ask for Forgiveness than Permission.
没必要纠结上面这句话的哲学含义。
知道在编程方面的指代意义就行:首先相信程序会正确执行,然后如果出错了我们再处理错误。
使用try
和except
这种防御风格,优点明显,try里只写我们的业务逻辑,except里写异常处理逻辑,几乎无多余代码,Python指南里也提倡使用这种风格。
但是任何事物都有两面性,这种写法也不例外。那么,EAFP防御风格有何问题呢?它主要会带来一些我们不想出现的副作用。
举一个例子,如下try块里的逻辑:出现某种情况修改磁盘的csv文件里的某个值,这些逻辑都顺利完成,但是走到下面这句代码时程序出现异常,进而被except
捕获,然后做一些异常处理:
try:
if condition:
revise_csv() # 已经污染csv文件
do_something() # 触发异常
except Exception:
handle_exception()
由于try块里的逻辑分为两步执行,它们不是一个原子操作,所以首先修改了csv文件,但是do_something
却出现异常,导致污染csv文件。
其实,除了以上EAFP防御性编程风格外,还有一种编程风格与它截然不同,它虽然能很好的解决EAFP的副作用,但是缺点更加明显,所以Python中不太提倡大量的使用此种风格。
3 LBYL 防御编程风格
再介绍另一种编程风格:LBYL
它的特点:指在执行正常的业务逻辑前做好各种可能出错检查,需要写一个又一个的if
和else
逻辑。
如EAFP
风格的代码:
d = {'a': 1, 'b': [1, 2, 3]}
try:
val = d['c']
except KeyError:
print('key not existence')
使用LBYL
来写就是如下这样:
if 'c' in d:
val = d['c']
else:
print('key not existence')
EAFP
风格的代码如下:
import os
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise # PermissionError 等异常
else:
# path 目录已存在
使用LBYL
来写就是如下这样:
import os
if not os.path.isdir(path):
print('不是一个合法路径')
else:
if not os.path.exists(path):
os.makedirs(path)
else:
print('路径已存在')
通过以上两个例子,大家可以看出LBYL
风格和EAFP
风格迥异。
LBYL
的代码if和else较多,这种风格会有以下缺点。
3.1 程序每次运行都要检查
程序每次运行都要检查,不管程序是不是真的会触发这些异常。
if 'c' in d: # 每次必做检查
val = d['c']
if not os.path.isdir(path): # 每次必做检查
print('不是一个合法路径')
else:
if not os.path.exists(path): # 每次必做检查
os.makedirs(path)
else:
print('路径已存在')
3.2 很难一次考虑所有可能异常
很难一次性考虑到所有可能的异常,更让人头疼的事情是,一旦遗漏某些异常情况,错误经常不在出现的地方,而在很外层的一个调用处。这就会导致我们花很多时间调试才能找到最终出错的地方。
def f1()
if con1:
# do1()
if con2:
# do2()
# 但是遗漏了情况3,未在f1函数中报异常
3.3 代码的可读性下降
要写很多与主逻辑无关的if-else
,程序真正的逻辑就变得难以阅读。最后导致我们很难看出这个只是判断,还是程序逻辑/业务的判断。但是,如果用try-catch
,那么try代码块里面可以只写程序的逻辑,在except
里面处理所有的异常。
结论:就Python语言,推荐使用EAFP
风格,个别受保护的块,若无法实现原子操作的地方可以使用LBYL
风格。
推荐阅读
1
2
3
4
崔庆才
静觅博客博主,《Python3网络爬虫开发实战》作者
隐形字
个人公众号:进击的Coder
长按识别二维码关注
好文和朋友一起看~