在微信公众号「极客起源」中输入595586,可学习全部的《Python高效编程之88条军规》系列文章。
用编程语言写代码是自由的,编译器不会强制你使用特定的格式编写程序(只要符合语法,编译器才不管你呢!)。所以很多程序员就会将Python当做自己熟悉的Java、C 等语言来用。不过这些编码方式真的是最好的选择吗?本系列文章将为你揭秘88种在编写Python代码中的规则,这些规则将会让你Python程序更加健壮,运行效率更高。
军规1:遵循PEP 8样式指南
Python的PEP 8是Python官方提供了关于如何格式化Python代码的样式指南。尽管可以用任何有效的方式编写Python代码,但是,使用一致的样式会使你的代码更易于访问和阅读,以及与其他Python程序员使用同一种样式有助于项目上的分工协作。即使你是唯一会阅读代码的人,遵循样式指南也可以让你的代码更容易维护,并可以避免许多常见错误。
关于PEP 8的详细内容,读者可以查看下面的页面:
https://www.python.org/dev/peps/pep-0008/
下面挑出PEP 8中一些常见的应该注意的地方:
空格
在Python语言中,空格是有实际意义的。Python程序员应该更关注空格的用法,下面是与空格相关的一些建议(并不一定要遵守,但按照这个规范,会让你的Python程序看着更舒服):
(1)使用空格代替Tab进行缩进;
(2)尽管缩进可以使用任意多个空格,但建议统一使用4个空格进行缩进;
(3)每行不应该有过多的字符,建议最多不要超过79的字符;
(4)如果每行的字符过多(超过79个),应该折到下一行,而且应该在当前缩进的基础上再使用4个空格进行缩进,如下图所示:
(5)在文件中,如果函数和类相邻,建议使用两个空行将他们分开,这样会让代码一目了然;
(6)在类中,相邻的方法之间应该用一个空行分隔;
(7)在字典中,不要在key和冒号(:)之间放置空格,如果对应的值与key和冒号在同一行,应该在值前面放置一个空格;
(8)在变量赋值时,等号(=)前面和后面应该有一个空格;
(9)对于类型注释(type annotations),要确保变量和冒号直接没有空格,而且要在类型信息前面使用一个空格,如下图所示:
命名规则:
PEP 8程序的不同部分采用统一的命名风格。因为拥有统一风格的命名,会让代码更容易阅读,下面是一些推荐的命名规则:
(1)函数、变量和属性应该使用小写字母加下划线(_)的风格,例如,get_name,product_id等;
(2)被保护的(protected)的实例属性应该在名字前面加一个下划线,例如,_name,_product_id等;
(3)私有(private)实例属性应该在名字前面加两个下划线(__),例如,__name,__product_id等;
(4)类名应该使用大驼峰格式,也就是每一个单词首字母都要大写,例如,MyClass,Test,Product等;
(5)模块层常量的名字所有的字母都应该大写,如果包含多个单词,中间用下划线分隔,例如,PRODUCT_ID,OS_PATH等;
(6)类中的实例方法的第1个参数应该使用self(尽管可以使用任意参数名,但推荐使用self),该参数引用了对象本身;
(7)类方法的第1个参数应该使用cls(尽管可以使用任意参数名,但推荐使用cls),该参数引用了类的本身;
表达式和语句:
Python禅宗指出:“应该有一种(最好只有一种)明显的方式来完成你的工作。”。PEP 8正常尝试按这个规则确定表达式和语句的编写风格。
(1)使用内联求反(if a is not b)代替对正表达式的求反(if not a is b);
(2)如果要判断序列(字符串、列表、字典等)是否为空(是否有元素),并不建议通过序列长度是否为0来判断(if len(somelist) == 0),而要直接使用not进行判断,例如,if not somelist。如果somelist是空串或空序列,那么not somelist就为True,当然,如果somelist不为空,那么somelist就被认为是True;
(3)尽量避免单行的if、for和while语句,除非是复合语句,否则为了清晰起见,应该将它们分布在多行;
(4)如果表达式过长,建议将这样的表达式分布在多行,这样更容易阅读。但注意要在每行结尾加连接符,并且从第2行开始在第1行的基础上再往后缩进4个空格;
导入模块:
下面是PEP8关于导入模块的一些建议:
(1)将import语句(包括from x import y和import x语句)放在文件的最顶端;
(2)如果在import语句中涉及到模块名,应该使用绝对的模块名,而不要使用相对的模块名。例如,为了从bar包导入foo模块,应该使用from bar import foo,而不要使用Import foo;
(3)如果必须要使用相对的模块名,应该显式使用from . import foo形式;
(4)导入模块应该按下面的顺序:
a. 标准的模块
b. 第三方的模块
c. 自己编写的模块
而且每一个子部分在导入时应该按字母顺序排列;
军规2:了解字节序列(bytes)和字符串(str)的差异
在Python语言中,有两个数据类型可以表示字符序列:字节序列和字符串。其中字节序列中包含了原始的,8位无符号的值,通常以ASCII编码形式显示:
如果用字节序列表示字符序列,应该以b开头,代码如下:
代码语言:javascript复制a = b'hx65llo'
print(list(a))
print(a)
执行这段代码,会输出如下的结果:
代码语言:javascript复制[104, 101, 108, 108, 111]
b'hello'
字符串的实例包含了Unicode编码,这些编码表示人类语言的文本字符:
代码语言:javascript复制a = 'au0300 propos'
print(list(a))
print(a)
执行这段代码,会输出如下结果:
代码语言:javascript复制['a', '̀', ' ', 'p', 'r', 'o', 'p', 'o', 's']
à propos
值得注意的是,字符串并不包含与之关联的二进制编码,而字节序列也不包含与之关联的文本编码。为了将文本编码数据转换为二进制数据,必须调用字符串的encode方法。为了将二进制数据转换为文本编码数据,必须调用字节序列的decode方法。我们可以显式地指定这些方法的编码格式,或者接受这些方法的默认编码格式。默认编码格式通常是UTF-8,不过也并不是所有方法的默认编码格式都是UTF-8,具体情况请看下面的内容。
在编写Python程序时,在例接口最远的边界(也就是最初接触Unicode数据的地方)进行Unicode数据的编码和解码非常重要。这种方法通常被称为Unicode三明治。程序的核心应使用包含Unicode数据的str类型,并且不应对字符编码做任何假设。这种方法使你可以非常容易接受其他文本编码(例如Latin-1,Shift JIS和Big5),同时严格限制输出文本编码(理想情况下为UTF-8)。
字符类型之间的分拆将导致Python代码中出现两种常见情况:
(1)操作的是包含UTF-8编码(或其他编码)的8位字节序列;
(2)操作的是没有特定编码的Unicode字符串;
下面给出两个函数来完成这些情形下的转换:
第1个颜色将字节序列或字符串转换一个字符串:
代码语言:javascript复制def to_str(bytes_or_str):
if isinstance(bytes_or_str, bytes):
# 将使用utf-8编码的字节序列转换为字符串
value = bytes_or_str.decode('utf-8')
else:
# 将不含编码格式的字符串转换为字符串(其实就是该字符串本身)
value = bytes_or_str
return value # 返回字符串
print(repr(to_str(b'hello')))
print(repr(to_str('world')))
运行这段代码,会输出如下的结果:
代码语言:javascript复制'hello'
'world'
第2个函数用于将字节序列或字符串转换为字节序列:
代码语言:javascript复制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 # 返回字节序列
print(repr(to_bytes(b'hello')))
print(repr(to_bytes('world')))
运行这段代码,会输出如下的结果:
代码语言:javascript复制b'hello'
b'world'
在Python中处理原始8位值和Unicode字符串时,有两个大陷阱。
第一个问题是字节和字符串的工作方式看似相同,但是它们的实例彼此并不兼容,因此你必须仔细考虑要传递的字符序列的类型。
字节序列与字符串都支持加号( )运算,也就是说,可以用加号分别将字节序列和字符串连接起来,看下面的代码:
代码语言:javascript复制print(b'hello ' b' world')
print('hello ' 'world')
运行代码,会输出下面的内容:
代码语言:javascript复制b'hello world'
hello world
但是不能将字节序列和字符串相加,例如,下面的代码会抛出异常:
代码语言:javascript复制print(b'hello ' 'world')
抛出的异常如下:
代码语言:javascript复制Traceback (most recent call last):
File "/python/bytes_str.py", line 36, in <module>
print(b'hello ' 'world')
TypeError: can't concat str to bytes
如果将字符串与字符序列相加,同样会抛出异常:
代码语言:javascript复制print('hello ' b'world')
抛出的异常如下:
代码语言:javascript复制Traceback (most recent call last):
File "/python/bytes_str.py", line 37, in <module>
print('hello ' b'world') # 抛出异常
TypeError: can only concatenate str (not "bytes") to str
如果两侧的操作数都是字节序列或字符串,那么也可以用于逻辑比较(<、<=、>、>=等运算符)。
代码语言:javascript复制print('hello' > 'world')
print(b'hello' < b'world')
执行代码,会输出如下的结果:
代码语言:javascript复制False
True
与加号类似,字符串与字节序列不能直接比较,如下面的代码会抛出异常:
代码语言:javascript复制print(b'hello' > 'world')
抛出的异常:
代码语言:javascript复制Traceback (most recent call last):
File "/python/bytes_str.py", line 41, in <module>
print(b'hello' > 'world')
TypeError: '>' not supported between instances of 'bytes' and 'str'
与=、<、<=、>=、>=这些运算符不同,字节序列和字符串可以直接使用“==”判定是否相等。不过这是个陷阱,就算字节序列和字符串表面上每一个字符都是相同的,返回的结果仍然是False。
代码语言:javascript复制print(b'hello' == 'hello')
执行这行代码,会返回如下的结果:
代码语言:javascript复制False
百分号(%)用于分别格式化字符串和字节序列,
代码语言:javascript复制print(b'hello %s' % b'world')
print('hello %s' % 'world')
执行代码,会输出如下结果:
代码语言:javascript复制b'hello world'
hello world
但是不能传递字符串到字节序列中(反过来可以),因为Python并不清楚使用何种编码格式将字符串转换为字节序列:
代码语言:javascript复制print('hello %s' % b'world') # 正常格式化
print(b'hello %s' % 'world') # 抛出异常
执行代码,会抛出下面的异常:
代码语言:javascript复制Traceback (most recent call last):
File "/python/bytes_str.py", line 50, in <module>
print(b'hello %s' % 'world') # 抛出异常
TypeError: %b requires a bytes-like object, or an object that implements __bytes__, not 'str'
第2个问题是涉及文件句柄的操作(由打开的内置函数返回),写文件时默认Unicode字符串而不是字节序列。这可能会导致抛出异常,尤其是对于习惯了Python 2的程序员而言。例如,假设我要向文件中写入一些二进制数据,下面的代码会抛出异常:
代码语言:javascript复制with open('data.bin', 'w') as f:
f.write(b'xf1xf2xf3xf4xf5')
执行代码,会抛出如下异常:
代码语言:javascript复制Traceback (most recent call last):
File "/python/bytes_str.py", line 53, in <module>
f.write(b'xf1xf2xf3xf4xf5')
TypeError: write() argument must be str, not bytes
抛出异常的原因是该文件是以写文本模式('w')而不是写二进制模式('wb')打开的。当文件处于文本模式时,写操作期望字符串包含Unicode数据,而不是字节序列。所以为了避免抛出异常,应该用“wb”模式打开data.bin文件。
代码语言:javascript复制with open('data.bin', 'wb') as f:
f.write(b'xf1xf2xf3xf4xf5')
从文件读取数据也存在类似的问题。例如,下面的代码尝试读取data.bin文件的内容:
代码语言:javascript复制with open('data.bin', 'r') as f:
data = f.read()
执行代码,会抛出如下的异常:
代码语言:javascript复制Traceback (most recent call last):
File "/python/bytes_str.py", line 61, in <module>
data = f.read()
File "/Users/lining/opt/anaconda3/lib/python3.7/codecs.py", line 322, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 0: invalid continuation byte
失败是因为文件是在读取文本模式('r')而非读取二进制模式('rb')中打开的。当句柄处于文本模式时,它将使用系统的默认文本编码来使用bytes.encode(用于写入)和str.decode(用于读取)方法来解释二进制数据。在大多数系统上,默认编码为UTF-8,该编码不能接受二进制数据b' xf1 xf2 xf3 xf4 xf5',因此会抛出异常。所以应该使用“rb”模式来打开二进制文件。
代码语言:javascript复制with open('data.bin', 'rb') as f:
data = f.read()
assert data == b'xf1xf2xf3xf4xf5' # 验证读取的数据是否与写入的数据一致
另外,还可以为open函数明确指定encoding参数(编码格式),以确保Python可以正确处理二进制的编码格式。例如,下面的代码明确指定了使用cp1252编码格式以只读方式打开data.bin文件。
代码语言:javascript复制with open('data.bin', 'r', encoding='cp1252') as f:
data = f.read()
print(data)
执行代码,会输出如下内容:
代码语言:javascript复制ñòóôõ
现在来总结一下:
(1)字节序列(bytes)包含8位的二进制数据,字符串(str)包含Unicode编码的值;
(2)为了让程序更健壮,需要使用专门的函数来校验输入的是字节序列,还是字符串。如前面的to_bytes函数和to_str函数;
(3)字节序列和字符串不能混合在一起进行运算(如 、>、<、%等);
(4)如果你想读写二进制格式的文件,应该使用二进制模式打开文件(例如,"rb"或"wb");
(5)如果你想读写文本格式的文件,需要考虑文本的编码格式。需要显式通过encoding参数传入正确的编码格式;