Python进阶开发

此开源图书由ithaiq原创,创作不易转载请注明出处

Pythonic 思维

PEP8风格

与命名有关的建议

  • 函数、变量及属性用小写字母来拼写,各单词之间用下划线相连,例如:lowercase_underscore

  • 受保护的实例属性,用一个下划线开头,例如:_leading_underscore

  • 私有的实例属性,用两个下划线开头,例如:__double_leading_underscore

  • 类(包括异常)命名时,每个单词的首字母均大写,例如:CapitalizedWord

  • 模块级别的常量,所有字母都大写,各单词之间用下划线相连,例如:ALL_CAPS

  • 类中的实例方法,应该把第一个参数命名为 self,用来表示该对象本身

  • 类方法的第一个参数,应该命名为 cls,用来表示这个类本身

与表达式和语句有关的建议

  • 采用行内否定即把否定词直接写在要否定的内容前面,而不要放在整个表达式的前面,例如应该写 if a is not b,而不是 if not a is b

  • 不要通过长度判断容器或序列是不是空的,例如不要通过 if len(somelist) == 0 判断 somelist 是否为 [] 或 '' 等空值,而是应该采用 if not somelist 这样的写法来判断,因为 Python 会把空值自动评估为 False 。

  • 如果要判断容器或序列里面有没有内容(比如要判断 somelist 是否为 [1] 或 'hi ' 这样非空的值),也不应该通过长度来判断,而是应该采用 if somelist 语句,因为 Python 会把非空的值自动判定为 True 。

用 f-string 取代 str.format 方法

a = 1
b = 2
print('one is %d, two is %d' % (a, b))
print('one is {}, two is {}'.format(a,b))
print(f'one is {a}, two is {b}')

尽量用 enumerate 取代 range

flavor_list = ['test1', 'test2', 'test3', 'test4']
for flavor in flavor_list:
    print(f'{flavor} is ok')

for i in range(len(flavor_list)):
    flavor = flavor_list[i]
    print(f'{i + 1}: {flavor}')

for i, flavor in enumerate(flavor_list, 2):
    print(f'{i}: {flavor}')
  • enumerate 函数可以用简洁的代码迭代 iterator,而且可以指出当前这轮循环的序号。

  • 不要先通过 range 指定下标的取值范围,然后用下标去访问序列,而是应该直接用 enumerate 函数迭代。

  • 可以通过 enumerate 的第二个参数指定起始序号(默认为 0)

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

names = ['test', 'test2', 'test3']
counts = [len(n) for n in names]
for name, count in zip(names, counts):
    print(name)
names.append('test4')
for name, count in itertools.zip_longest(names, counts):
    print(f'{name}: {count}')
  • 内置的 zip 函数可以同时遍历多个迭代器。

  • zip 会创建惰性生成器,让它每次只生成一个元组,所以无论输入的数据有多长, 它都是一个一个处理的。

  • 如果提供的迭代器的长度不一致,那么只要其中任何一个迭代完毕,zip 就会停止。

  • 如果想按最长的那个迭代器来遍历,那就改用内置的 itertools 模块中的 zip_ longest 函数。

赋值表达式通过(:= )操作符给变量赋值

pieces = 0
count = fresh_fruit.get('banana', 0)
if count >= 2:
    pieces = slice_bananas(count)

if (count := fresh_fruit.get('banana', 0)) >= 2:
    pieces = slice_bananas(count)

函数

遇到意外状况时应该抛出异常而不是 None

def careful_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None
x, y = 0, 5
result = careful_divide(x, y)
if result is None:
    print('Invalid inputs')
if not result: #这里判断就有问题
    print('Invalid inputs')  # This runs! But shouldn't
def careful_divide(a, b):
    try:
        return True, a / b
    except ZeroDivisionError:
        return False, None
x, y = 0, 5
success, result = careful_divide(x, y)
if not success:
    print('Invalid inputs')
def careful_divide(a: float, b:float) -> float:
    '''Divides a by b.

    Raises:
        ValueError: When the inputs cannot be divided.
    '''
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError('Invalid inputs') #确保不会返回None
    
x, y = 3, 0
result = careful_divide(x, y)
# if result is None: 不需要判断None自己处理异常就行
 
  • 用返回值 None 表示特殊情况是很容易出错的,因为这样的值在条件表达式里面,没办法与 0 和空白字符串之类的值区分,这些值都相当于 False 。

  • 用异常表示特殊的情况,而不要返回 None。让调用这个函数的程序根据文档里写的异常情况做出处理。

  • 通过类型注解可以明确禁止函数返回 None,即便在特殊情况下,它也不能返回这个值。

  • 类型注解:

    • int、long、float: 整型,长整形,浮点型

    • bool、str: 布尔型,字符串类型

    • List、Tuple、Dict、Set:列表,元组,字典, 集合

    • Iterable、Iterator、Generator:可迭代类型,迭代器类型,生成器类型

用 None 和 docstring 来描述默认值会变的参数

from datetime import datetime
from time import sleep
from typing import Optional

def log_typed(message: str,
              when: Optional[datetime]=None) -> None:
    """Log a message with a timestamp.

    Args:
        message: Message to print.
        when: datetime of when the message occurred.
            Defaults to the present time.
    """
    if when is None:
        when = datetime.now()
    print(f'{when}: {message}')

log_typed('Hi there!')

log_typed('Hello again!')
  • 参数的默认值只会计算一次,也就是在系统把定义函数的那个模块加载进来的时候。所以,如果默认值将来可能由调用方修改(例如 {} 、[])或者要随着调用时的情况变化(例如 datetime.now()),那么程序就会出现奇怪的效果。

  • 如果关键字参数的默认值属于这种会发生变化的值,那就应该写成 None ,并且要在 docstring 里面描述函数此时的默认行为。

推导和生成

用列表推导取代 map 与 filter

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = []
for x in a:
    squares.append(x**2)
print([x**2 for x in a])
print(list(map(lambda x: x ** 2, a)))

print([x**2 for x in a if x % 2 == 0])
print(list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))))
print({x: x**2 for x in a if x % 2 == 0})
print({x**3 for x in a if x % 3 == 0})
stock = {
    'nails': 125,
    'screws': 35,
    'wingnuts': 8,
    'washers': 24,
}

order = ['screws', 'wingnuts', 'clips']

def get_batches(count, size):
    return count // size

result = {}
for name in order:
  count = stock.get(name, 0)
  batches = get_batches(count, 8)
  if batches:
    result[name] = batches

print(result)
{name: get_batches(stock.get(name, 0),8) for name in order if get_batches(stock.get(name, 0), 8)}
{name: batches for name in order if (batches:=get_batches(stock.get(name, 0), 8))}
  • 列表推导要比内置的 map 与 filter 函数清晰,因为它不用另外定义 lambda 表达式。

  • 列表推导可以很容易地跳过原列表中的某些数据,假如改用 map 实现,那么必须搭配 filter 才能实现。

  • 字典与集也可以通过推导来创建。

不要让函数直接返回列表

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

def index_words_iter(text):
    if text:
        yield 0
    for index, letter in enumerate(text):
        if letter == ' ':
            yield index + 1
            
address = 'Four score and seven years ago our fathers brought forth on this continent a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal.'

print(index_words(address))
print(list(index_words_iter(address)))

[len(x) for x in open('my_file.txt')]
it = (len(x) for x in open('my_file.txt'))
next(it)
next(it)
  • 用生成器来实现比让函数把结果收集到列表里再返回,要更加清晰一些。

  • 生成器函数所返回的迭代器可以产生一系列值,每次产生的那个值都是由函数体的下一条 yield 表达式所决定的。

  • 不管输入的数据量有多大,生成器函数每次都只需要根据其中的一小部分来计算当前这次的输出值。它不用把整个输入值全都读取进来,也不用一次就把所有的输出值全都算好,节约内存。

  • 其他: itertools 包里面有三套函数可以拼装迭代器与生成器,它们分别能够连接多个迭代器、过滤源迭代器中的元素,以及用源迭代器中的元素合成新元素。

通过 yield from 把多个生成器连起来用

def move(period, speed):
    for _ in range(period):
        yield speed

def pause(delay):
    for _ in range(delay):
        yield 0
def animate():
    for delta in move(4, 5.0):
        yield delta
    for delta in pause(3):
        yield delta
    for delta in move(2, 3.0):
        yield delta
def render(delta):
    print(f'Delta: {delta:.1f}')
    # Move the images onscreen

def run(func):
    for delta in func():
        render(delta)


run(animate)

def animate_composed():
    yield from move(4, 5.0)
    yield from pause(3)
    yield from move(2, 3.0)
run(animate_composed)
  • 如果要连续使用多个生成器,那么可以通过 yield from 表达式来分别使用这些生成器,这样做能够免去重复的 for 结构。

  • yield from 的性能要胜过那种在 for 循环里手工编写 yield 表达式的方案。

最后更新于