0%

Pythonic Coding

《Effective Python》学习笔记

程序风格

遵循PEP8风格指南

《Python Enhancement Proposal #8》,简称PEP8,是针对Python代码格式编订的风格指南。

bytes、str与unicode区别

首先区分Python3与Python2的两种表示字符序列的类型

  • Python3,bytes与str,前者的实例包含原始的8位值,即原始的字节,包含8个二进制位;后者的实例包含Unicode字符
  • Python2,str与unicode,前者的实例包含原始的8位值;后者的实例包含Unicode字符

二进制数据与Unicode字符相互转换

常见编码方式为UTF-8
Unicode字符 → 二进制数据,encode()方法
二进制数据 → Unicode字符,decode()方法

def to_bytes(bytes_or_str):
if isinstance(bytes_or_str, str):
value = bytes_or_str.encode('utf8')
else:
value = bytes_or_str
return value
def to_str(bytes_or_str):
if isinstance(bytes_or_str, bytes):
value = bytes_or_str.decode('utf8')
else:
value = bytes_or_str
return value

注:

  • Python程序中,编码和解码操作放在程序外围,核心部分使用Unicode字符类型
  • 在只处理7位ASCII时,Python2的str和unicode类型的实例可以等价,而Python3中bytes与str的实例绝对不等价
  • Python3中,使用内置open()函数获取文件句柄,该句柄默认采用UTF-8格式来操作文件,问题在于Python3给open()函数添加了名为encoding的新参数,其默认值为’utf-8’,要求必须传入包含Unicode字符的str实例,而不接受包含二进制数据的bytes实例
    总结为必须使用二进制写入模式open(path, ‘wb’)来开启待操作文件

用辅助函数取代复杂表达式

如,从字典中查询并返回得到的第一个整数值:
red = my_values.get('red', [''])[0] or 0
未查询到或值为0为空统一返回0,该表达式不易理解,若要频繁使用,将其总结为辅助函数:

def get_first_int(values, key, default=0):
found =values.get(key, [''])
if found[0]:
found = int(found[0])
else:
found = default
return found

red = get_first_int(my_values, 'red')

切片操作

基本写法

somelist[start:end],其中start所指元素涵盖在切割后的范围内,end所指元素不包括在切割结果之中。例:

#start从0开始,end倒数从-1开始
a[:] #[1,2,3,4,5,6]
a[:3] #[1,2,3]
a[2:] #[3,4,5,6]
a[2:5] #[3,4,5]
a[-3:-1] #[4,5]

切割列表时,start和end越界不会出问题,利用该特性可以限定输入序列的最大长度。

first_nine_items = a[:9]
last_nine_items = a[9:]

切片后不影响原列表,对list赋值,若使用切片操作,会把原列表处在相关范围内的值替换为新值,即便长度不同也可以替换。

a[2:5] = [1,1]                          #[1,2,1,1,6]
a[:] = [1,1] #[1,1]

步进式切割

somelist[start:end:stride]

a = [1,2,3,4,5,6]
odds = a[::2]
evens = a[1::2]
b = b'abc'
reverse = b[::-1] #负值为反向步进

注:

  • 负步长只对字节串和ASCII字符有效,对已编码成UTF-8字节串的Unicode字符无效
  • 尽量使用stride为正数,且不带start和end索引
  • 同一切片操作内,不要同时指定start、end和stride,考虑将其拆解为一条步进切割,一条范围切割

列表与迭代

用列表推导取代map和filter

列表推导(list comprehension),根据一份列表来制作另外一份。
字典(dict)与(set)也支持推导表达式

a = [1, 2, 3, 4, 5, 6, 7, 8, 9]

#使用map,创建lambda函数,结合filter
squares = map(lambda x: x**2, a)
even_squares = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))

#使用列表推导
squares = [x**2 for x in a]
even_squares = [x**2 for x in a if x % 2 == 0]

列表推导内含的表达式不宜超过两个

列表推导支持多重循环

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

#矩阵简化为一维列表
flat = [x for row in matrix for x in row] #[1, 2, 3, 4, 5, 6, 7, 8, 9]

squared = [[x**2 for x in row] for row in matrix] #[[1, 4, 9], [16, 25, 36], [49, 64, 81]]

每一级循环也支持多重条件

#处在同一循环级别中的多项条件, 彼此之间默认形成and表达式
b = [x for x in a if x > 4 if x % 2 == 0]
b = [x for x in a if x > 4 and x % 2 == 0]

#从矩阵中取出本身能被3整除,且其所在行所有元素之和大于等于10的元素
filtered = [[x for x in row if x % 3 == 0]
for row in matrix if sum(row) >= 10]

用生成器表达式改写数据量较大的列表推导

首先,列表推导的缺点是:
在推导过程中,对于输入序列中的每个值,可能都要创建一个仅含一个元素的新列表,若输入数据量较大,会消耗大量内存。
如,读取一份文件并返回每行的字符数,采用列表推导

value = [len(x) for x in open(file)]

生成器表达式(generator expression):
对列表推导和生成器的一种泛化(generalization),生成器表达式运行时,不会呈现整个输出序列,而是估值为迭代器(iterator),该迭代器每次根据生成器表达式产生一项数据。

#生成器表达式,立刻返回一个迭代器
it = (len(x) for x in open(file))
next(it)

#生成器表达式可以互相组合
roots = ((x, x**0.5) for x in it)

用enumerate取代range

enumerate可以把各种迭代器包装为生成器,可以在遍历迭代器时获得每个元素的索引,在同时需要下标和值的时候使用。如range

list_num = ['a', 'b', 'c', 'd']
for i in range(len(list_num)):
num = list_num[i]
print('%d:%s', %(i+1, num))

使用enumerate

list_num = ['a', 'b', 'c', 'd']
for i, num in enumerator(list_num, 1): #起始下标指定为1
print('%d:%s', %(i, num))

用zip函数同时遍历两个迭代器

Python3中的zip函数,能将两个及以上的迭代器封装为生成器,在遍历过程中逐次产生元组。
Python2中,直接产生所有元组,并一次性返回整份列表
若提供的迭代器长度不等,zip会提前自动终止

names = ['adad', 'bob', 'alen']
letters = [len(x) for x in names]

for name, letter in zip(names, letters):
print(name+str(letter))

函数

函数的问题主要体现在参数作用域返回值三个方面

参数

作用域

在闭包里使用外围作用域中的变量

注意:

  • Python支持闭包(closure):闭包是定义在某个作用域中的函数,可以引用那个作用域中的变量。
  • Python的函数时一级对象,可以直接引用函数、将函数赋给变量或者将函数作为参数传递。

例如,对一份数字列表进行排序,要求出现的特定数字在其他数字排序之前。

def sort_priority(values, groups):
def helper(x):
if x in group:
return (0, x)
return(1, x)
values.sort(key=helper)

numbers = [18, 9, 89, 66, 4, 1, 23]
group = [1, 4, 66]
sort_priority(numbers, group)
print(numbers)

output:
[1, 4, 66, 9, 18, 23, 89]

稍作修改为

def sort_priority2(values, groups):
found = False
def helper(x):
if x in group:
found = True
return (0, x)
return(1, x)
values.sort(key=helper)
return found

numbers = [18, 9, 89, 66, 4, 1, 23]
group = [1, 4, 66]
found = sort_priority2(numbers, group)
print(found)
print(numbers)

output:
False
[1, 4, 66, 9, 18, 23, 89]

found的输出结果与预期的True不符,这里我们首先要了解,在表达式中引用变量,Python解释器遵循以下顺序遍历各作用域:

  • 当前函数作用域
  • 任何外围作用域(如包含当前函数的其他函数)
  • 包含当前代码的那个模块的作用域(全局作用域,global scope)
  • 内置作用域(包含str及len等函数的那个作用域)
  • 未定义过名称相符变量,抛出NameError异常

sort_priority2函数中将found赋值为True是在闭包函数helper内进行的,实则是在闭包函数的作用域中定义了一个found变量并赋值为True,与其外围函数sort_priority2作用域中定义的found不同,最后返回的是sort_priority2中赋值为False的found变量。
因此,我们需要获取闭包内的数据,可使用nonlocal语句,不支持Python2

def sort_priority2(values, groups):
found = False
def helper(x):
nonlocal found #声明该found为闭包外围作用域中的found
if x in group:
found = True
return (0, x)
return(1, x)
values.sort(key=helper)
return found

output:
True
[1, 4, 66, 9, 18, 23, 89]

而实际开发中,nonlocal容易遭到滥用,且副作用难以追踪,难以理解,不适用于较长较复杂的函数。有以下两个解决办法:

  1. 将相关状态封装为辅助类
    class Sorter(object):
    def __init__(self, group):
    self.group = group
    self.found = False
    def __call__(self, x):
    if x in self.group:
    self.found = True
    return (0, x)
    return(1, x)

    numbers = [18, 9, 89, 66, 4, 1, 23]
    group = [1, 4, 66]
    sorter = Sorter(group)
    numbers.sort(key=sorter)
    print(sorter.found)
  2. 用Python的作用域规则,Python2可用
    def sort_priority2(values, groups):
    found = [False]
    def helper(x):
    if x in group:
    found[0] = True
    return (0, x)
    return(1, x)
    values.sort(key=helper)
    return found[0]

    返回值

    尽量用异常表示特殊情况,而非返回None

    例如,两数相除的情况。
    def divide(a, b):
    try:
    return a/b
    except ZeroDivisionError:
    return None

    result = divide(0, 2)
    if result is None: #这种情况没有问题
    print('Invalid inputs')

    if not result: #错误情况
    print('Invalid inputs')
    当分子为0时not result结果为True,结果应为0,却显示Invalid inputs。用异常来表示这种情况:
    def divide(a, b):
    try:
    return a/b
    except ZeroDivisionError as e:
    raise ValueError('Invalid inputs') from e

    try:
    result = divide(0, 2)
    except ValueError:
    print('Invalid inputs')
    else:
    print('Result is %.1f' % result)

    用生成器改写直接返回列表的函数

    若函数产生一系列结果,最简单的方法是返回一个包含所有结果的列表,此方法主要有两个缺点:
  • 代码拥挤,不清晰
  • 返回前将所有结果放在列表里,若输入量非常大,会导致内存耗尽

例如以下函数返回字符串中每个单词首字母的位置。

def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == ' ':
result.append(index+1)
return result

text = 'seven years ago'
result = index_words(text)
print(result)

output:
[0, 6, 12]

用生成器改写:

def index_words(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index+1

text = 'seven years ago'
result = list(index_words(text))