专栏首页极客起源Python高效编程之88条军规(1):编码规范、字节序列与字符串

Python高效编程之88条军规(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开头,代码如下:

a = b'h\x65llo'
print(list(a))
print(a)

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

[104, 101, 108, 108, 111]
b'hello'

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

a = 'a\u0300 propos'
print(list(a))
print(a)

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

['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个颜色将字节序列或字符串转换一个字符串:

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')))

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

'hello'
'world'

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

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')))

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

b'hello'
b'world'

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

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

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

print(b'hello ' + b' world')
print('hello ' + 'world')

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

b'hello  world'
hello world

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

print(b'hello ' + 'world')

抛出的异常如下:

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

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

print('hello ' + b'world')

抛出的异常如下:

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

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

print('hello' > 'world')
print(b'hello' < b'world')

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

False
True

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

print(b'hello' > 'world')

抛出的异常:

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。

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

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

False

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

print(b'hello %s' % b'world')
print('hello %s' % 'world')

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

b'hello world'
hello world

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

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

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

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的程序员而言。例如,假设我要向文件中写入一些二进制数据,下面的代码会抛出异常:

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

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

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

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

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

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

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

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

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”模式来打开二进制文件。

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

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

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

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

ñòóôõ

现在来总结一下:

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

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

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

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

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

本文分享自微信公众号 - 极客起源(geekculture),作者:geekori

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 微信小程序开发实战(21):发起HTTPS请求

    在wx对象中有一个request方法,可以发起HTTPS请求。该方法只有一个对象类型参数。该对象支持如下所示。

    蒙娜丽宁
  • Python编程思想(11):while循环

    循环语句的作用是重复执行某一段代码,这也是任何编程语言必备的功能之一,因为只有自动重复执行某一段代码,才能真正体现计算机CPU的运算速度。而且循环也是任何复杂程...

    蒙娜丽宁
  • 连Python产生器(Generator)的原理都解释不了,还敢说Python用了5年?

    最近有很多学Python同学问我,Python Generator到底是什么东西,如何理解和使用。Ok,现在就用这篇文章对Python Generator做一个...

    蒙娜丽宁
  • python学习之变量类型

      变量是保存在内存中的值,根据变量类型开辟不同的内存空间且只允许符合该数据类型的数据才可以被存储在该内存空间中

    py3study
  • json字符串使用注意问题

    json本身是字符串,即 json字符串 js使用 要把 json字符串 转为  javascript对象 json字符串转为js对象的方法: jquery的p...

    蓓蕾心晴
  • Jenkins X--(4)如何解决镜像下载不了问题

    从这篇文章开始就写写如何在虚拟机中通过minikube搭建一个K8s集群,并在这个K8s集群里安装Jenkins X,体验一把云原生下的CICD框架是如何运行的...

    DevOps亮哥
  • Kafka集群安装

    ①.kafka需要依赖zk管理,在搭建kafka集群之前需要先搭建zk集群: https://my.oschina.net/u/2486137/blog/153...

    用户1215919
  • 浏览器的overflow事件

    Webkit和Firefox其实是原生支持探测元素overflow状态改变的事件。参看这个DEMO:

    mmzhou
  • 【作者投稿】Slowhttptest攻击原理

    Slowhttptest其实是一个DoS压力测试工具,它集成有三种慢速攻击模式(slowloris、slow http post、slow read attac...

    信安之路
  • 腾讯网络安全T-Star高校挑战赛 正式启动!!!

    ? ? T-Star大赛简介 ? “腾讯网络安全T-star高校挑战赛”由腾讯安全应急响应中心(TSRC)与腾讯高校合作联合发起,覆盖全国二十余所高等院校的...

    腾讯高校合作

扫码关注云+社区

领取腾讯云代金券