0%

pythonic编码思维

第1条:获取Python版本

在代码中,可以使用内置的sys模块来查询当前python的版本

1
2
3
import sys
print(sys.version_info)
print(sys.version)

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 xxxfrom ..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
    4
    print('red %s' % b'blue')

    >>>
    red b'blue'

另外一个需要注意的就是使用open函数对文本文件进行读写的问题,如果在打开一个文本文件使用'r'模式,则系统会采用默认的编码格式对二进制数据进行处理。如果要以二进制方式读取的话需要使用rb模式。

第4条:用f-string 取代 C 风格的格式化字符串和str.format方法

python 中对字符串进行格式化有多种方法,下面分别对这几种方法进行介绍和对比。

第一种:

采用%格式化操作符,这是python中最常用的字符串格式化方式。这个操作符左边的文字模板叫做格式字符串,%操作符右边是一个值或者多个值构成的元组,例如:

1
2
3
4
5
6
a = 0b101111011
b = 0xc5f
print('binary is %d, hex is %d' % (a, b))

>>>
binary is 187, hex is 3167
格式说明符的写法来自C语言的printf函数,所以常见的printf选项都可以当成python的格式说明符来用,例如%s, %x, %f,此外还可以控制小数点的位值,并指定填充与对其方式。

C风格的格式化字符串,在python里有三个缺点:

  1. 如果%右侧元组里的值在类型或顺序上有变化,那么程序可能会因为类型转换时发生不兼容问题而出现错误。
1
2
3
4
5
6
key = 'my_var' 
value = 1.234
print('%-10s = %.2f' % (value, key))

>>>
TypeError: must be real number, not str
如果%右侧的写法不变,但是左侧的格式字符串里说明符调换了位置,程序同样会发生这个错误。
  1. 在填充模板之前,经常要先对准备填写进去的这个值稍稍做一些处理,但这样以来,整个表达式可能就会写得很长。

  2. 如果想用一个值来填充格式字符串里的多个位置,那么必须在%操作符右侧的元组中相应地多次重复该值。如果需要修改,那么必须同时修改多次

虽然%操作符允许我们使用dict来取代tuple,让格式字符串里面的说明符与dict里面的键以相应的名称对应起来以解决第三个问题,但是这样会让第二个问题更严重,每个键至少需要写两次

1
2
3
4
format_string = '%(key)-10 = %(value).2f' % {
'key': 'my_var',
'value': 1.234
}

第二种:

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
2
3
4
5
6
print('%.2f%%' % 12.5)
print('{} replaces {{}}'.fromat(1.23))

>>>
12.50%
1.23 replaces {}
使用format函数可以避免使用`%`操作符带来的第一个问题:格式字符串中的此项发生变动后,程序也不会有问题。另外format还支持为`{}`指定索引,这样就不需要把多个值重复地传给format方法,于是就解决了前面的第三个缺点。
1
2
3
4
5
6
format_string = '{} loves food, see {0} cook.'.format('tom')
print(format_string)
format_string = '{name} loves food, see {name} cook.'.format(name='tom')
print(format_string)
>>>
tom loves food, see tom cook.
但是format 方法也没有解决上面的第二个问题。 第三种: python 3.6之后增加了一种新的特性,叫做插值格式字符串,简称f-string,可以解决上面的所有问题。新语法特性要求在格式字符串的前面加字母`f`作为前缀,这跟字符`b`和`r`(分别表示字节形式的字符串与原始未经转义的字符串)的用法类似。
1
2
3
4
key = 'my_var'
value = 1.234

print(f'{key} = {value}')
同时f-string也支持在`{}`加冒号用于指定格式
1
2
3
key = 'my_var'
value = 1.234
print(f'{key:<10}')
另外python表达式也可以出现在f-string的格式说明符中
1
2
3
4
5
6
places = 3
number = 1.23456
print(f'my number is {number:{places}f}')

>>>
my number is 1.235
f-string可以简洁而清晰地表达出许多种逻辑,这使它成为程序员的最佳选择。

第5条:用辅助函数取代复杂的表达式

python拥有很强大的语法,有时候一条表达式就可以实现比较复杂的逻辑,但是有时候这种表达式会不利于代码阅读,编写同样功能的辅助函数反而是一个不错的选择。

当你发现表达式越写越复杂,那就应该考虑把它拆分成多个部分,并把这套逻辑写道辅助函数中。

第6条:将数据结构直接拆分到多个变量里,不要专门通过下标访问

该条实际上建议多使用python中的unpacking机制,例如对于有两个元素的元组,可以通过下标来访问,也可以直接unpacking到两个变量中

1
2
3
4
5
6
item = ('Peanut butter', 'Jelly')
# unrecommended
first = item[0]
second = item[1]
# recommend
first, second = item #unpacking

并且unpacking还支持快速交换连个变量的值

1
2
3
item = ('Peanut butter', 'Jelly')
first, second = item
first, second = second, first

本质上python是先将=号右边的值放入一个临时元组内,然后对这个临时元组再做unpacking

第7条:尽量使用enumerate取代range

python内置的range函数比较适合来迭代一系列整数:

1
2
for i in range(32):
do something

如果要迭代的是某种数据结构,例如字符串列表,则可以直接在这个序列上进行迭代:

1
2
3
fruit_list = ['vanilla', 'chocolate', 'pecan', 'starwberry']
for fruit in fruit_list:
print(fruit)

如果即需要知道index,也想要知道元素的值,使用range可以如下实现:

1
2
3
fruit_list = ['vanilla', 'chocolate', 'pecan', 'starwberry']
for i in range(len(fruit_list)):
print(i, fruit_list[i])

python 还有一个内置函数,叫做enumerate,它可以方便地获取到元素的下标和元素值。enumerate本质上是将任何一种迭代器(例如list,dict)封装成惰性生成器,这样的话,每次循环的时候,它只需要从iterator里面获取下一个值就可以了。每一次取出的是一个包括元素下标和对应的值的元组:

1
2
3
4
5
6
fruit_list = ['vanilla', 'chocolate', 'pecan', 'starwberry']
it = enumerate(fruit_list)
print(next(it))

>>>
(0, 'vanilla')

在for循环中使用,加上unpacking:

1
2
for i, fruit in enumerate(fruit_list):
print(i, fruit)

另外enumerate还可以指定第二个参数,用于指定启始的排序序号,注意这不是表示从下标为1的数据开始遍历

1
2
3
4
5
6
7
8
for i, fruit in enumerate(fruit_list, 1):
print(i, fruit)

>>>
1 vanilla
2 chocolate
3 pecan
4 starwberry

第8条:用zip函数同时遍历多个迭代器

python的内置zip函数,能够两个或更多的迭代器封装成惰性生成器,每次循环时,它分别从这些迭代器里获取下一个元素,并把这些值放在一个元组里,可以利用unpacking将这个元组拆分到for语句里的那些变量之中。

1
2
3
4
5
names = ['name1', 'name2', 'name3']
addresses = ['address1', 'address2', 'address3']
for name, address in zip(name, address):
print(name)
print(address)

但是,如果输入zip的那些列表的长度不一致,在这种情况下,zip表现的行为如下:如果其中任何一个迭代器处理完毕,则完成迭代。如果需要使最长的列表迭代,可以用itertools模块中的zip_longest。

1
2
3
4
from itertools import zip_longest
for name, address in zip_longest(name, address):
print(name)
print(address)

第9条:不要再for与while循环后面紧跟else代码块

python的循环支持一项特性,即可以把else代码块紧跟在整个循环结构的后面

1
2
3
4
5
for in range(3):
print(f''loop {i})

else:
print('else block')

但是,整个else语句的意思不是如果for循环没有执行完,就执行else语句。恰恰相反,else语句在for循环完成后接着执行,如果循环中途退出,是不会执行else语句的。另外,如果循环一次没有执行,也会执行else代码块中的内容。

所以这种语句是一种不利于代码阅读的语法,不要使用这种语法。

第10条:用赋值表达式减少重复代码

赋值表达式是python3.8新引入的语法,它会用到海象操作符。a = b是一条普通的赋值语句,而a := b则是赋值表达式。这个符号为什么叫海象操作符呢,因为把:=瞬时间旋转90度之后,冒号就是海象的一双眼睛,等号就是它的獠牙,(感觉有点牵强…,完全不像好吧)。

这种表达式很有用,可以在赋值语句无法应用的场合实现赋值,例如可以用在if语句中。赋值表达式的值,就是赋给海象操作符左侧的那个标识符的值。例如,如果有一筐水果要给果汁店做食材,那我们就可以定义其中的内容:

1
2
3
4
5
fruit = {
'apple': 10,
'banana': 8,
'lemon': 5
}

顾客点柠檬汁之前,我们先得确认现在还有柠檬:

1
2
3
4
5
6
7
8
9
10
11
def make_lemonade(count):
pass

def out_of_stock():
pass

count = fruit.get('lemon', 0)
if count:
make_lemonade(count)
else:
out_of_stock()

count 只会在if语句中使用,把count写在外面,会给人一种后面还会使用到count变量的感觉,这时候就可以使用赋值表达式

1
2
3
4
if count := fruit.get('lemon', 0):
make_lemonade(count)
else:
out_of_stock()

新代码虽然只节省了一行,但是读起来却清晰很多,因为这种写法明确体现出count变量只与if块有关。这个赋值表达式先把:=右边的值赋给左边的count变量,由于然后判断是否为0来决定是否要执行if代码块。如果不是和0比较,则需要将赋值表达式用括号扩起来:

1
2
3
4
if (count := fruit.get('lemon', 0)) >= 4:
make_lemonade(count)
else:
out_of_stock()

另外,赋值表达语句还可以解决if/else语句嵌套的问题

假设现在客户的要求是点柠檬汁,但是如果柠檬不够就做香蕉冰沙,如果香蕉不够就做苹果汁,现在做柠檬汁需要3个柠檬,做香蕉冰沙需要2个香蕉,做苹果汁需要1一个苹果,不适用赋值表达式的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def make_bananas(count):
pass

def make_apple(count):
pass


count = fruit.get('lemon', 0)
if count >= 3:
make_lemonade(count)
else:
count = fruit.get('banana', 0)
if count >= 2:
make_banana(count)
else:
count = fruit.get('apple', 0)
if count >= 1:
make_apple(count)

使用赋值表达式:

1
2
3
4
5
6
if (count := fruit.get('lemon', 0)) >= 3:
make_lemonade(count)
elif (count := fruit.get('banana', 0)) >= 2:
make_banana(count)
elif (count := fruit.get('apple', 0)) >= 1:
make_apple(count)

另外赋值表达式也可以用在while循环的条件判断中,甚至来实现do/while的效果(python中不支持do/while 语法)。例如,当前需要随机选一种水果来做果汁,pick_fruit函数实现随机选水果的操作,店员一直做果汁,直到没有水果,不使用赋值表达式来实现:

1
2
3
4
5
while True:
fruit = pick_fruit()
if fruit is None:
break
make_juice(fruit)

使用赋值表达式来实现:

1
2
while (fruit := pick_fruit()):
make_juice(fruit)