【Python】Effective Python

本文记录了《Effective Python》的重要内容。

Pythonic-确认自己所用的 Python 版本

查看版本

1
$ python --version

sys 模块内查询

1
2
3
4
5
6
>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=6, micro=8, releaselevel='final', serial=0)
>>> sys.version
'3.6.8 |Anaconda custom (64-bit)| (default, Dec 29 2018, 19:04:46) \n[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]'
>>>

优先考虑 Python3 版本!

Pythonic-遵循 PEP 8 风格指南

PEP 8 的意义

《Python Enhancement Proposal 8》又叫 PEP 8,它是针对 Python 代码格式而编制的风格指南。

链接:https://www.python.org/dev/peps/pep-0008/

  • 主要作用:
    • 有利于编写更加通俗易懂的代码。
    • 有利于多人协作,有利于修改。

几条重要的规则

命名

  • 函数、变量以及属性应该用小写字母来拼写。
1
2
def sum_a_+_b(int a, int b):
...
  • 与异常,应该以每个单词首字母大写的形式来命名。
1
2
3
4
class SmallCar():
...
#
CapitalizedWord
  • 模块级别的常量,应该全部采用大写字母来拼写,各单词下划线连接。
1
ALL_CAPS
  • 受保护的实例属性,应该以单个下划线开头。
1
_leading_underscore
  • 私有的实例属性,应该以两个下划线开头。
1
__double_leading_underscore

空白

  • 在变量赋值的时候,赋值符号的左侧和右侧应该各自写上一个空格。
  • 文件中的 函数 和 类 之间应该用两个空行隔开。
  • 在同一个类中,各方法 之间应该用一个空行隔开。
  • 对于占据多行的长表达式来说,除了首行之外的其余各行都应该在通常的缩进级别之上再加4个空格。

表达式和语句

  • 不要用 if len(a) == 0 来判断 a 列表是否为空值。
1
2
3
4
if not a: # 判断 a 是空
...
if a: # 判断 a 不空
...
  • 采用内联形式的否定词,而不是把否定词放到整个表达式前面。
1
2
if a is not b: # 推荐
if not a is b: # 不推荐
  • 不要编写单行的 if 语句、for 循环、while 循环及 except 复合语句。

  • import
    • import 语句总是放在文件开头。
    • import 语句根据顺序分为三个部分:
      • 标准库模块
      • 第三方模块
      • 自用模块
    • 在每一模块中,按照字母顺序排列。

Pylint 流行的 Python 源码静态分析工具。

它可以自动检测受测代码是否符合 PEP 8 的风格。

Pythonic-了解 bytes、str 与 unicode 区别

字符序列类型

字符序列类型 Python 3 Python 2
8位值 (8个二进制) bytes str
Unicode 字符 str unicode

Unicode 字符(Python 3 的 str 实例和 Python 2 的 unicode 实例)都没有和特定的二进制编码形式相关联;

换句话说,Unicode字符 转换为 二进制数据 有很多编码方式,其中最常见的是 UTF-8。

编码与解码

  • Unicode 字符 --> 二进制数据,称为编码 encode
  • 二进制数据 --> Unicode 字符,称为解码 decode

编写 Python 程序的时候,一定要把编码和解码放在最外围来做。

使用情形(Python 3)

  • 使用情形
    • 需要将 Unicode 字符 --> UTF-8 编码后的二进制数据
    • 需要操作没有特定编码形式的 Unicode字符
  • 解码
1
2
3
4
5
6
def to_str(bytes_or_str):
if isinstance(bytes_or_str, bytes):
value = bytes_or_str.decode('utf-8')
else:
value = bytes_or_str
return value # Instance of str
  • 编码
1
2
3
4
5
6
def to_str(bytes_or_str):
if isinstance(bytes_or_str, str):
value = bytes_or_str.encode('utf-8')
else:
value = bytes_or_str
return value # Instance of bytes

可能的问题(Python 3)

如果使用内置函数 open 获取了文件句柄(file handle)。那么请注意,该句柄默认采用 UTF-8 的编码格式来操作文件。

  • 问题:如果向文件中随机写入一些二进制数据,下面代码可能会出错。
1
2
3
4
with open('/tmp/random.bin', 'w')as f:
f.write(os.urandom(10))
>>>
TypeError: must be str, not bytes
  • 原因:Python 3 给 open 函数添加了名为 encoding 的新参数,而这个参数的默认值就是 'utf-8'。

  • 解决方案,用二进制写入模式('wb')来开启待操作的文件。

1
2
with open('/tmp/random.bin', 'wb')as f:
f.write(os.urandom(10))
  • 读取数据也类似,用('rb')来打开文件。

Pythonic-用辅助函数来替代复杂式

在处理网页,或者说处理字符串时,我们经常获得到一个字典,对于字典里的键值对的处理挺关键的。然而有些朋友过度使用 Python 的语法特性,为了炫技,写成很复杂的难以理解的单行表达式,就有点小题大做了。

  • 比如这样一个栗子。
1
2
3
4
5
from urllib.parse import parse_qs
# 解析字符串,返回字典
my_value = parse_qs('red=5&blue=0&green=', keep_blank_values=True)
print(my_value)
# {'red': ['5'], 'blue': ['0'], 'green': ['']}
  • 对于上面待处理的字典,我们对其值感兴趣。
  • 也就是我们想查询我们需要的值。
  • 现在一个目标是,若待查询的参数没有出现在字符串中,我们希望返回默认值 0,而不返回 None 等。该如何处理呢?
    • 也许我们会考虑 dic.get(key, defalt=None)
    • key 指代键,如果查不到对应值,返回第二个参数 None
  • 对于想要的目标,我们做出以下一步步调试。

复杂方式_1

  • 空字符串、空列表及零值,都会评估为 False。
1
2
3
4
5
6
7
8
9
10
# For query string 'red=5&blue=0&green='
red = my_value.get('red', [''])[0] or 0 # or 操作符
green = my_value.get('green', [''])[0] or 0 # 如果 or 前判断 False
opacity = my_value.get('opacity', [''])[0] or 0 # 赋值 or 后语句
print('Red: %r' %red) # %r 用 rper()方法处理对象
print('green: %r' %green) # 打印时能够重现它所代表的对象
print('opacity: %r' %opacity)
# Red: '5'
# green: 0
# opacity: 0

炫技方式_2

1
2
3
4
red_1 = my_value.get('red', [''])
red_1 = int(red_1[0]) if red_1[0] else 0
print(red_1)
# 5

简单方式_3

1
2
3
4
5
6
7
green_1 = my_value.get('green', [''])
if green_1[0]: # 如果有值
green_1 = int(green_1[0])
else:
green_1 = 0
print(green_1)
# 0

范式_4

  • 用简单函数的表示来取代复杂的表达式。
1
2
3
4
5
6
7
8
9
10
def get_int_value(dic_kv, key, default=0):
value = dic_kv.get(key, [''])
if value[0]:
return int(value[0])
else:
return default
print(get_int_value(my_value, 'opacity'))
print(get_int_value(my_value, 'red'))
# 0
# 5

Pythonic-了解切割序列的办法

Python 提供了一种把序列切成小块的方法。 针对内置的 list、str 和 bytes 的切割。

切割序列

  • 一个列表
1
2
3
list_num = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
# index = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
# index = -0(-10),-9,-8,-7,-6,-5,-4,-3,-2,-1
  • 正常的切割序列 [start_index:end_index]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
print(list_num[:])
# print all [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

print(list_num[1:4])
# index-123 [2, 3, 4]

print(list_num[2:])
# index-2... [3, 4, 5, 6, 7, 8, 9, 0]

print(list_num[:8])
# index-...7 [1, 2, 3, 4, 5, 6, 7, 8]

print(list_num[7:2])
# if start_index > end_index []
  • 负值切割
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
print(list_num[-4:-2])
# index~-4,-3 [7, 8]
print(list_num[-4:])
# index~-4... [7, 8, 9, 0]

print(list_num[-0:])
# print all [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
print(list_num[-12:])
# print all [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

print(list_num[-1:0])
# if start_index > end_index []
print(list_num[-1:-1])
# []

print(list_num[-1:])
# [0]
print(list_num[-12:6])
# [1, 2, 3, 4, 5, 6]

传值

  • 对于元组而言,传值纬度 必须保持一致。
1
2
3
4
5
6
7
8
c = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
c1, c2 = c[2:4]
print(c1, c2)
# 3 4

c3, c4 = c[5:8]
print(c3, c4)
# ValueError: too many values to unpack (expected 2)
  • 不同于元组,列表的切片不考虑纬度,可以伸缩。
1
2
3
4
5
6
7
8
9
10
11
12
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

a[2:4] = ['a', 'b', 'c', 'd']
print(a)
# index_start: 2 扩张
# [1, 2, 'a', 'b', 'c', 'd', 5, 6, 7, 8, 9, 0]

b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
b[2:7] = ['123','456']
print(b)
# index_start: 2 缩减
# [1, 2, '123', '456', 8, 9, 0]