第1条:获取Python版本
在代码中,可以使用内置的sys模块来查询当前python的版本
1 | import sys |
python2从2020.01.01号之后就不再有新版本发布了,所以尽量使用python3进行开发
第2条:PEP 8 风格指南
PEP 8的全称为Python Enhencement Proposal #8,他是一份针对python代码格式而编订的风格指南。PEP 8非常详细的描述了如果编写清晰的python代码,而且随着python语言的发展,这份指南也在不断更新。这里只简单说明几种
空格与空行
- 用空格表示缩进(一般四个空格),而不使用tab表示缩进,现代的IDE都有将tab转换为空格的设置
- 同一份文件中,函数与类之间用两个空行隔开
- 同一个类中,方法与方法之间用一个空行隔开
现代IDE都有控制空格与空行规范的format工具,可以借用IDE的format工具来实现空格与空行的规范,而不必手动进行改动。
变量命名相关建议
- 函数、变量及属性用小写+下划线命名
- 受保护的实例属性,用一个下划线开头,例如:
_leading_underscore
- 私有的实力属性,用两个下划线开头,例如
__double_leading_underscore
- 类(包括异常)命名时,使用首字母大写+驼峰命名法,例如:
CaptializedWord
- 模块级别的常量,所有字母都大写,各单词之间用下划线相连,例如:
ALL_CAPS
- 类中的实例方法,第一个参数命名为self,用来表示该对象本身
- 类方法的第一个参数,应该命名为cls,用来表示这个类本身
表达式相关建议
- 否定表达式将否定词直接写在要否定的内容前面,而不要放在整个表达式的前面,例如应该写
if a is not b
而不是if not a is b
与导入包相关的建议
- 引入模块时,尽量使用绝对名称,而不应该根据当前模块路径来使用相对名称。如果一定要用相对名称来导入,也应明确的写出
from .xxx import xxx
或from ..xxx import xxx
- 文件中import语句按顺序划分为三个部分:首先引入标准库里的模块,然后引入第三方模块,最后引入自己编写的模块。属于同一个部分的import语句尽量按照字母顺序排列(老实说个人理解做到按字母顺序排列有点难,除非靠专门的format工具)
Pylint是一款流行的Python源码静态分析工具。它可以自动检测代码是否符合PEP 8风格指南,很多IDE都包含这样的linting工具
第3条:了解bytes与str的区别
python有两种类型可以表示字符序列,一种是bytes,一种是str,bytes实例包含的是原始数据,即8位的无符号值(通常按照ASCII编码标准来显示)。str实例包含的是unicode码点,这些码点与人类语音之中的文本字符相对应。要把Unicode数据转换成二进制数据,必须调用str的encode
方法,要把二进制数据转换为Unicode数据,必须调用bytes的decode
方法。
编写python程序的时候,一般把字符的解码和编码放在最外层来做,让程序的核心使用Unicode数据来运作(在程序的核心部分,用str类型来表示Unicode数据)。
string和string之间,bytes和bytes之间可以使用
+
号连接,但是string和bytes之间不可以string和string之间,bytes和bytes之间可以使用
>
、<
比较大小,但是string和bytes之间不行string 和 bytes之间使用
==
比较大小总是会得到false,即使两个实例表示的字符完全相同string和string之间,bytes和bytes之间,可以使用
%s
操作符来进行格式替换。但是如果格式字符串是bytes类型,那么不能使用str实例来替换其中的%s
,因为python不知道这个str应该按照什么方式来编码。但是反过来,如果格式字符串时str类型,虽然可以用str实例来替换其中的%s
,但是最终的结果可能和你想要的不一样。1
2
3
4print('red %s' % b'blue')
>>>
red b'blue'
另外一个需要注意的就是使用open函数对文本文件进行读写的问题,如果在打开一个文本文件使用'r'
模式,则系统会采用默认的编码格式对二进制数据进行处理。如果要以二进制方式读取的话需要使用rb
模式。
第4条:用f-string 取代 C 风格的格式化字符串和str.format方法
python 中对字符串进行格式化有多种方法,下面分别对这几种方法进行介绍和对比。
第一种:
采用%
格式化操作符,这是python中最常用的字符串格式化方式。这个操作符左边的文字模板叫做格式字符串,%
操作符右边是一个值或者多个值构成的元组,例如:
1 | a = 0b101111011 |
C风格的格式化字符串,在python里有三个缺点:
- 如果%右侧元组里的值在类型或顺序上有变化,那么程序可能会因为类型转换时发生不兼容问题而出现错误。
1 | key = 'my_var' |
如果%右侧的写法不变,但是左侧的格式字符串里说明符调换了位置,程序同样会发生这个错误。
在填充模板之前,经常要先对准备填写进去的这个值稍稍做一些处理,但这样以来,整个表达式可能就会写得很长。
如果想用一个值来填充格式字符串里的多个位置,那么必须在
%
操作符右侧的元组中相应地多次重复该值。如果需要修改,那么必须同时修改多次
虽然%操作符允许我们使用dict来取代tuple,让格式字符串里面的说明符与dict里面的键以相应的名称对应起来以解决第三个问题,但是这样会让第二个问题更严重,每个键至少需要写两次
1 | format_string = '%(key)-10 = %(value).2f' % { |
第二种:
python3 添加了高级字符串格式化机制,它的表达能力比老式的格式字符串要强,且不再使用%操作符,而是用str的format方法,format方法不使用%d
这样的C风格说明符。而是把格式有待调整的那些位置在字符串里面先用{}
代替,然后按从左至右的顺序,依次填写到format方法的参数中
1 | format_string = '{} = {}'.format('my_var', 1.234) |
你也可以在{}
中写个冒号,然后把格式说明符写在冒号的右边,用以规定format方法所接受的这个值应该按照怎样的格式来调整。
1 | format_string = '{:<10} = {.2f}'.format('my_var', 1.234) |
C风格的格式字符串采用%
操作符来引导格式说明符,所以如果要将这个符号按照原样输出,就必须进行转义,也就是连写两个%
。同理,在调用str.format的时候,如果想把str里面的{}
按原样输出,那么也得转义
1 | print('%.2f%%' % 12.5) |
1 | format_string = '{} loves food, see {0} cook.'.format('tom') |
1 | key = 'my_var' |
1 | key = 'my_var' |
1 | places = 3 |
第5条:用辅助函数取代复杂的表达式
python拥有很强大的语法,有时候一条表达式就可以实现比较复杂的逻辑,但是有时候这种表达式会不利于代码阅读,编写同样功能的辅助函数反而是一个不错的选择。
当你发现表达式越写越复杂,那就应该考虑把它拆分成多个部分,并把这套逻辑写道辅助函数中。第6条:将数据结构直接拆分到多个变量里,不要专门通过下标访问
该条实际上建议多使用python中的unpacking机制,例如对于有两个元素的元组,可以通过下标来访问,也可以直接unpacking到两个变量中
1 | item = ('Peanut butter', 'Jelly') |
并且unpacking还支持快速交换连个变量的值
1 | item = ('Peanut butter', 'Jelly') |
本质上python是先将=
号右边的值放入一个临时元组内,然后对这个临时元组再做unpacking
第7条:尽量使用enumerate取代range
python内置的range函数比较适合来迭代一系列整数:
1 | for i in range(32): |
如果要迭代的是某种数据结构,例如字符串列表,则可以直接在这个序列上进行迭代:
1 | fruit_list = ['vanilla', 'chocolate', 'pecan', 'starwberry'] |
如果即需要知道index,也想要知道元素的值,使用range可以如下实现:
1 | fruit_list = ['vanilla', 'chocolate', 'pecan', 'starwberry'] |
python 还有一个内置函数,叫做enumerate,它可以方便地获取到元素的下标和元素值。enumerate本质上是将任何一种迭代器(例如list,dict)封装成惰性生成器,这样的话,每次循环的时候,它只需要从iterator里面获取下一个值就可以了。每一次取出的是一个包括元素下标和对应的值的元组:
1 | fruit_list = ['vanilla', 'chocolate', 'pecan', 'starwberry'] |
在for循环中使用,加上unpacking:
1 | for i, fruit in enumerate(fruit_list): |
另外enumerate还可以指定第二个参数,用于指定启始的排序序号,注意这不是表示从下标为1的数据开始遍历
1 | for i, fruit in enumerate(fruit_list, 1): |
第8条:用zip函数同时遍历多个迭代器
python的内置zip函数,能够两个或更多的迭代器封装成惰性生成器,每次循环时,它分别从这些迭代器里获取下一个元素,并把这些值放在一个元组里,可以利用unpacking将这个元组拆分到for语句里的那些变量之中。
1 | names = ['name1', 'name2', 'name3'] |
但是,如果输入zip的那些列表的长度不一致,在这种情况下,zip表现的行为如下:如果其中任何一个迭代器处理完毕,则完成迭代。如果需要使最长的列表迭代,可以用itertools模块中的zip_longest。
1 | from itertools import zip_longest |
第9条:不要再for与while循环后面紧跟else代码块
python的循环支持一项特性,即可以把else代码块紧跟在整个循环结构的后面
1 | for in range(3): |
但是,整个else语句的意思不是如果for循环没有执行完,就执行else语句。恰恰相反,else语句在for循环完成后接着执行,如果循环中途退出,是不会执行else语句的。另外,如果循环一次没有执行,也会执行else代码块中的内容。
所以这种语句是一种不利于代码阅读的语法,不要使用这种语法。
第10条:用赋值表达式减少重复代码
赋值表达式是python3.8新引入的语法,它会用到海象操作符。a = b
是一条普通的赋值语句,而a := b
则是赋值表达式。这个符号为什么叫海象操作符呢,因为把:=
瞬时间旋转90度之后,冒号就是海象的一双眼睛,等号就是它的獠牙,(感觉有点牵强…,完全不像好吧)。
这种表达式很有用,可以在赋值语句无法应用的场合实现赋值,例如可以用在if
语句中。赋值表达式的值,就是赋给海象操作符左侧的那个标识符的值。例如,如果有一筐水果要给果汁店做食材,那我们就可以定义其中的内容:
1 | fruit = { |
顾客点柠檬汁之前,我们先得确认现在还有柠檬:
1 | def make_lemonade(count): |
count 只会在if语句中使用,把count写在外面,会给人一种后面还会使用到count变量的感觉,这时候就可以使用赋值表达式
1 | if count := fruit.get('lemon', 0): |
新代码虽然只节省了一行,但是读起来却清晰很多,因为这种写法明确体现出count变量只与if块有关。这个赋值表达式先把:=
右边的值赋给左边的count变量,由于然后判断是否为0来决定是否要执行if
代码块。如果不是和0比较,则需要将赋值表达式用括号扩起来:
1 | if (count := fruit.get('lemon', 0)) >= 4: |
另外,赋值表达语句还可以解决if/else
语句嵌套的问题
假设现在客户的要求是点柠檬汁,但是如果柠檬不够就做香蕉冰沙,如果香蕉不够就做苹果汁,现在做柠檬汁需要3个柠檬,做香蕉冰沙需要2个香蕉,做苹果汁需要1一个苹果,不适用赋值表达式的实现:
1 | def make_bananas(count): |
使用赋值表达式:
1 | if (count := fruit.get('lemon', 0)) >= 3: |
另外赋值表达式也可以用在while循环的条件判断中,甚至来实现do/while
的效果(python中不支持do/while
语法)。例如,当前需要随机选一种水果来做果汁,pick_fruit
函数实现随机选水果的操作,店员一直做果汁,直到没有水果,不使用赋值表达式来实现:
1 | while True: |
使用赋值表达式来实现:
1 | while (fruit := pick_fruit()): |