小言_互联网的博客

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

491人阅读  评论(0)

目录

军规1:遵循PEP 8样式指南

军规2:了解字节序列(bytes)和字符串(str)的差异

 

用编程语言写代码是自由的,编译器不会强制你使用特定的格式编写程序(只要符合语法,编译器才不管你呢!)。所以很多程序员就会将Python当做自己熟悉的Java、C++等语言来用。 不过这些编码方式真的是最好的选择吗?本系列文章将为你揭秘68种在编写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开头,代码如下:


  
  1. a = b'h\x65llo'
  2. print(list(a))
  3. print(a)

执行这段代码,会输出如下的结果:


  
  1. [ 104, 101, 108, 108, 111]
  2. b'hello'

字符串的实例包含了Unicode编码,这些编码表示人类语言的文本字符:


  
  1. a = 'a\u0300 propos'
  2. print(list(a))
  3. print(a)

执行这段代码,会输出如下结果:


  
  1. ['a', '̀', ' ', 'p', 'r', 'o', 'p', 'o', 's']
  2. à 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个颜色将字节序列或字符串转换一个字符串:


  
  1. def to_str(bytes_or_str):
  2. if isinstance(bytes_or_str, bytes):
  3. # 将使用utf-8编码的字节序列转换为字符串
  4. value = bytes_or_str.decode( 'utf-8')
  5. else:
  6. # 将不含编码格式的字符串转换为字符串(其实就是该字符串本身)
  7. value = bytes_or_str
  8. return value # 返回字符串
  9. print(repr(to_str( b'hello')))
  10. print(repr(to_str( 'world')))

运行这段代码,会输出如下的结果:


  
  1. 'hello'
  2. 'world'

第2个函数用于将字节序列或字符串转换为字节序列:


  
  1. def to_bytes(bytes_or_str):
  2. if isinstance(bytes_or_str, str):
  3. value = bytes_or_str.encode( 'utf-8')
  4. else:
  5. value = bytes_or_str
  6. return value # 返回字节序列
  7. print(repr(to_bytes( b'hello')))
  8. print(repr(to_bytes( 'world')))

运行这段代码,会输出如下的结果:


  
  1. b 'hello'
  2. b 'world'

在Python中处理原始8位值和Unicode字符串时,有两个大陷阱。

第一个问题是字节和字符串的工作方式看似相同,但是它们的实例彼此并不兼容,因此你必须仔细考虑要传递的字符序列的类型。

字节序列与字符串都支持加号(+)运算,也就是说,可以用加号分别将字节序列和字符串连接起来,看下面的代码:


  
  1. print( b'hello ' + b' world')
  2. print( 'hello ' + 'world')

运行代码,会输出下面的内容:


  
  1. b 'hello world'
  2. hello world

但是不能将字节序列和字符串相加,例如,下面的代码会抛出异常:

print(b'hello ' + 'world')

抛出的异常如下:


  
  1. Traceback (most recent call last):
  2. File "/python/bytes_str.py", line 36, in < module>
  3. print(b 'hello ' + 'world')
  4. TypeError: can 't concat str to bytes

如果将字符串与字符序列相加,同样会抛出异常:

print('hello ' + b'world')

抛出的异常如下:


  
  1. Traceback (most recent call last):
  2. File "/python/bytes_str.py", line 37, in < module>
  3. print( 'hello ' + b 'world') # 抛出异常
  4. TypeError: can only concatenate str ( not "bytes") to str

如果两侧的操作数都是字节序列或字符串,那么也可以用于逻辑比较(<、<=、>、>=等运算符)。


  
  1. print( 'hello' > 'world')
  2. print( b'hello' < b'world')

执行代码,会输出如下的结果:


  
  1. False
  2. True

与加号类似,字符串与字节序列不能直接比较,如下面的代码会抛出异常:

print(b'hello' > 'world')

抛出的异常:


  
  1. Traceback (most recent call last):
  2. File "/python/bytes_str.py", line 41, in < module>
  3. print(b 'hello' > 'world')
  4. TypeError: '>' not supported between instances of 'bytes' and 'str'

与=、<、<=、>=、>=这些运算符不同,字节序列和字符串可以直接使用“==”判定是否相等。不过这是个陷阱,就算字节序列和字符串表面上每一个字符都是相同的,返回的结果仍然是False。

print(b'hello' == 'hello')

执行这行代码,会返回如下的结果:

False

百分号(%)用于分别格式化字符串和字节序列,


  
  1. print( b'hello %s' % b'world')
  2. print( 'hello %s' % 'world')

执行代码,会输出如下结果:


  
  1. b 'hello world'
  2. hello world

但是不能传递字符串到字节序列中(反过来可以),因为Python并不清楚使用何种编码格式将字符串转换为字节序列:


  
  1. print( 'hello %s' % b'world') # 正常格式化
  2. print( b'hello %s' % 'world') # 抛出异常

执行代码,会抛出下面的异常:


  
  1. Traceback (most recent call last):
  2. File "/python/bytes_str.py", line 50, in < module>
  3. print(b 'hello %s' % 'world') # 抛出异常
  4. TypeError: %b requires a bytes- like object, or an object that implements __bytes__, not 'str'

第2个问题是涉及文件句柄的操作(由打开的内置函数返回),写文件时默认Unicode字符串而不是字节序列。 这可能会导致抛出异常,尤其是对于习惯了Python 2的程序员而言。例如,假设我要向文件中写入一些二进制数据,下面的代码会抛出异常:


  
  1. with open( 'data.bin', 'w') as f:
  2. f.write( b'\xf1\xf2\xf3\xf4\xf5')

执行代码,会抛出如下异常:


  
  1. Traceback (most recent call last):
  2. File "/python/bytes_str.py", line 53, in < module>
  3. f.write(b '\xf1\xf2\xf3\xf4\xf5')
  4. TypeError: write() argument must be str, not bytes

抛出异常的原因是该文件是以写文本模式('w')而不是写二进制模式('wb')打开的。 当文件处于文本模式时,写操作期望字符串包含Unicode数据,而不是字节序列。 所以为了避免抛出异常,应该用“wb”模式打开data.bin文件。


  
  1. with open( 'data.bin', 'wb') as f:
  2. f.write( b'\xf1\xf2\xf3\xf4\xf5')

从文件读取数据也存在类似的问题。 例如,下面的代码尝试读取data.bin文件的内容:


  
  1. with open( 'data.bin', 'r') as f:
  2. data = f.read()

执行代码,会抛出如下的异常:


  
  1. Traceback (most recent call last):
  2. File "/python/bytes_str.py", line 61, in < module>
  3. data = f.read()
  4. File "/Users/lining/opt/anaconda3/lib/python3.7/codecs.py", line 322, in decode
  5. ( result, consumed) = self._buffer_decode( data, self.errors, final)
  6. 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”模式来打开二进制文件。


  
  1. with open( 'data.bin', 'rb') as f:
  2. data = f.read()
  3. assert data == b'\xf1\xf2\xf3\xf4\xf5' # 验证读取的数据是否与写入的数据一致

另外,还可以为open函数明确指定encoding参数(编码格式),以确保Python可以正确处理二进制的编码格式。 例如,下面的代码明确指定了使用cp1252编码格式以只读方式打开data.bin文件。


  
  1. with open( 'data.bin', 'r', encoding= 'cp1252') as f:
  2. data = f.read()
  3. print(data)

执行代码,会输出如下内容:

ñòóôõ

现在来总结一下:

(1)字节序列(bytes)包含8位的二进制数据,字符串(str)包含Unicode编码的值;

(2)为了让程序更健壮,需要使用专门的函数来校验输入的是字节序列,还是字符串。如前面的to_bytes函数和to_str函数;

(3)字节序列和字符串不能混合在一起进行运算(如+、>、<、%等);

(4)如果你想读写二进制格式的文件,应该使用二进制模式打开文件(例如,"rb"或"wb");

(5)如果你想读写文本格式的文件,需要考虑文本的编码格式。需要显式通过encoding参数传入正确的编码格式;

 


转载:https://blog.csdn.net/nokiaguy/article/details/108446446
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场