Python高效编程之88条军规(1):编码规范、字节序列与字符串

2020-09-15 17:02:23 浏览数 (1)

在微信公众号「极客起源」中输入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参数传入正确的编码格式;

0 人点赞