本文介绍了如何进行有效的Python编程,以提高编码质量。
本文主要参考的书籍《Effective Python》 [美] Brett Slatkin。
文章目录
用Pythonic方式思考
Python开发者用Pythonic这个形容词来描述那种符合特定风格的代码。
1 遵循PEP8风格指南
《Python Enhancement Proposal #8》又叫PEP8,是针对Python代码格式而编订的风格指南。下面列出几条常见的规则:
-
空格
- 使用space代替tab表示缩进
- 每行字符数不应超过79
- 文件中函数与类之间用2个空行,同一个类中,各方法之间用1个空行隔开
- 在使用下标获取列表元素、调用函数或给关键字参数赋值时,不要在两旁添加空格
- 为变量赋值时,赋值符号的左侧和右侧应各自写上一个空格
-
命名
- 函数、变量及属性应用小写字母来拼写,各单词之间以下划线相连
- 受保护的实例属性,应以单个下划线开头;私有的实例属性,应以两个下划线开头
- 类与异常,应以每个单词首字母均大写的形式命名
- 模块级别的常量,应全部用大写字母拼写,各单词之间以下划线相连
-
表达式和语句
- 采用内联形式的否定词,如: 写
if a is not b
而不是if not a is b
- 不要用检测长度的方法判断
somelist
是否空值,而是采用if not somelist
写法判断 import
语句应写文件开头,最好用绝对名称import
语句划分成三个部分,分别表示标准库模块、第三方模块以及自用模块,在每一部分中,各import
语句应按模块的字母顺序来排列
- 采用内联形式的否定词,如: 写
2 了解bytes、str和unicode的区别
Python3有两种表示字符序列的类型:bytes和str。前者的实例包含原始的8位值,后者包含Unicode字符。
Python2两种表示字符序列的类型为:str和unicode。str的实例包含原始的8位值,unicode则包含Unicode字符。
把Unicode字符表示为二进制数据有多种方法,最常见的编码方式就是UTF-8。
编写Python程序时候,一定要把编码和解码操作放在界面最外围来做。
对于Python3和Python2,我们需要两种辅助函数方便开发者在UTF-8和Unicode两种情况之间转换。
- Python3,接受str或bytes,返回str的方法:
def to_str(bytes_or_str):
if isinstance(bytes_or_str,bytes):
value = bytes_or_str.decode('utf-8')
else:
value = bytes_or_str
return value # Instance of str
- Python3,接受str或bytes,返回bytes的方法:
def to_bytes(bytes_or_str):
if isinstance(bytes_or_str,str):
value = bytes_or_str.encode('utf-8')
else:
value = bytes_or_str
return value # Instance of bytes
- Python2,接受str或unicode,返回unicode的方法:
def to_unicode(unicode_or_str):
if isinstance(unicode_or_str,str):
value = unicode_or_str.decode('utf-8')
else:
value = unicode_or_str
return value # Instance of unicode
- Python2,接受str或unicode,返回str的方法:
def to_str(unicode_or_str):
if isinstance(unicode_or_str,unicode):
value = unicode_or_str.encode('utf-8')
else:
value = unicode_or_str
return value # Instance of str
3 合理运用try/except/else/finally
Python的异常处理可能要考虑四种不同的时机,这些时机可以用try/except/else/finally块来表述。
finally
如果既要将异常向上传播,又要在异常发生时执行清理工作,那么可以使用try/finally结构。
这种结构常见的用途是确保程序能够可靠的关闭文件句柄。
handle = open('file.txt')
try:
data = handle.read()
finally:
handle.close()
else
try/except/else结构可以清晰地描述出哪些异常会由自己的代码来处理、哪些异常会传播到上一级。
如果try没有发生异常,那么执行else块。
例如,从字符串中加载JSON字典数据,然后返回键值。
def load_json_key(data,key):
try:
result_dic = json.joads(data) # May raise ValueError
except ValueError as e:
raise KeyError from e
else:
return result_dic[key] # May raise KeyError
try/except/else/finally
完整版
UNDEFNED = object()
def divide_json(path):
handle = open(path,'r+')
try:
data = handle.read()
op = data.json.loads(data)
value = (
op['numerator']/
op['denominator'])
except ZeroDivisionError as e:
raise UNDEFNED
else:
op['result'] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result)
return value
finally:
handle.close()
函数
4 用异常表示特殊情况,而不是返回None
假设对于除法,当结果没有定义,一般会返回None,如下
def divide(a,b):
try:
return a/b
except ZeroDivisionError:
return None
然而,当采用下面方式调用时,结果为0,就会出现错误:
x,y = 0,5
result = divide(x,y)
if not result:
print('Invalid inputs')
因此,不要返回None,而是把异常抛给上一级:
def divide(a,b):
try:
return a/b
except ZeroDivisionError as e:
raise ValueError("Invalid inputs") from e
5 在闭包中使用外围作用域变量
下面是一个闭包例子:
def sort_priority(values,group):
def helper(x):
if x in group:
return (0,x)
return (1,x)
values.sort(key=helper)
如输入:
numbers = [8,3,1,2,5,4,7,6]
group = {2,3,5,7}
sort_priority(numbers,group)
print(numbers)
>>>
[2,3,5,7,1,4,6,8]
对这个函数进行改进,使其能够返回是否出现优先级较高的元素的标志。
def sort_priority(values,group):
found = False
def helper(x):
if x in group:
found = True
return (0,x)
return (1,x)
values.sort(key=helper)
return found
然而,found的值永远是False,这是因为内部函数helpera重新定义了一个found,相当于局部变量。
那么如何使用外部变量呢?
python3可以使用nonlocal关键字,即在helper内部首先定义nonlocal found
,然后found = True
。
python2可以使用列表,即在外围定义found = [False]
,在内部使用found[0] = True
6 使用生成器
功能需求:查出字符串中每个单词的首字母,在整个字符串里的位置。
我们很容易可以写出下面的代码:
def index_words(text):
result = []
if text:
result.append(0)
for index,letter in enumerate(text):
if letter = ' ':
result.append(index+1)
return result
该代码的问题是过于繁琐,另外,如果当文本比较大时,需要全部读取完文本后,才能返回result。
因此,使用生成器会更好。
def index_words_iter(text):
if text:
yield 0
for index,letter in enumerate(text):
if letter == ' ':
yield index+1
调用方式如下:
result = list(index_words_iter(address))
7 使用数量可变的位置参数
令函数接受可选的位置参数(如*arg),能够使代码更清晰。
例如,定义log函数:
def log(mes,val):
if not val:
print(mes)
else:
val_str = ','.join(str(x) for x in val)
print('%s: %s'%(mes,val_str))
log('Hi there',[1,2])
log('Hi there',[])
>>>
Hi there: 1,2
Hi there
当val为空时,也需要传入[],这种写法并不好。如何改善呢?
只需要在第二个参数前加一个*,即:
def log(mes,*val):
...
log('Hi there')
>>>
Hi there
变长参数需要注意两个问题:
- 只有能够确定输入的参数个数比较少时,才应该令函数接受*arg的变长参数。
- 在给函数增加新的变长参数时,需要修改原来调用函数的旧代码。
8 用关键字参数来表达可选的行为
Python支持按照位置传递参数。
举例:
def remainder(number,divisor):
return number % divisor
下面这些调用都是等效的:
remainder(20,7)
remainder(20,divisor = 7)
remainder(number = 20, divisor = 7)
remainder(divisor = 7, number = 20)
需要注意的是,位置参数必须出现在关键字参数之前:
remainder(number = 20, 7)
>>>
SyntaxError: non-keyword arg after keyword arg
每个参数只能指定一次:
remainder(20, number = 7)
>>>
TypeError: remainder() got multiple values for argument 'number'
关键字参数的3个好处:
- 使阅读代码的人更容易理解其含义。
- 可以在函数定义中提供默认值。(如:
def remainder(number,divisor=1)
) - 可以提供一种扩充函数参数的有效方式,使得扩充之后的函数依然能与原有的那些调用代码相兼容。
如(def remainder(number,divisor=1, isFloat=True)
,调用时应该写成:remainder(22,divisor=1, isFloat=True)
,而不是:remainder(22,1,True)
)。
关键点总结:
- 关键字参数能够阐明每个参数的意图。
- 可选的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形式指定。
9 用None和文档字符串描述具有动态默认值的参数
当我们想用一种非静态的类型,来做关键字参数的默认值时,需要注意参数的实际默认值如果是可变类型,则应该用None作为形式上的默认值。举例:
def log(mes,when=datetime.now()):
print('%s: %s'%(when,mes))
log('hello!')
sleep(0.1)
log('hello!')
>>>
2021-04-25 13:10:12.232123: hello!
2021-04-25 13:10:12.232123: hello!
两条消息的时间戳一样,这是因为datetime.now只执行了一次。
参数的默认值,会在每个模块加载进来的时候求出,而很多模块都是在程序启动时加载的。包含这段代码的模块一旦加载进来,参数的默认值就固定不变了。
如何实现正确的动态默认值呢?改为以下方式即可:
def log(mes,when=None):
when = datetime.now() if when is None else when
print('%s: %s'%(when,mes))
log('hello!')
sleep(0.1)
log('hello!')
>>>
2021-04-25 13:10:12.232123: hello!
2021-04-25 13:10:12.425353: hello!
另一个例子:
def decode(data, default={}):
try:
return json.loads(data)
except ValueError:
return default
foo = decode('bad data')
foo['a'] = 5
abr = decode('bad too')
abr['b'] = 1
print('foo: ',foo)
print('bar: ',bar)
>>>
foo: {'a':5, 'b':1}
bar: {'a':5, 'b':1}
这里的foo和bar都使用的是写在default参数默认值中的那个字典。
如何解决呢?
def decode(data, default=None):
if default is None:
default = {}
try:
return json.loads(data)
except ValueError:
return default
10 用只能以关键字形式指定的参数来确保代码清晰
python3如何实现只能以关键字来指定的参数?
def safe_division(number, divisor, *, ignore_overflow=False, ignore_zero_division=False)
safe_division(1, 10**500, True, False)
>>>
TypeError: safe_division() takes 2 positional arguments but 4 were given
safe_division(1, 10**500, ignore_zero_division=True) # OK
python2如何实现只能以关键字来指定的参数?
def safe_division(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ignore_zero_division = kwargs.pop('ignore_zero_division', False)
if kwargs:
raise TypeError('Unexpected **kwargs: %r'%kwargs)