Skip to content

Python 日常必备技巧备忘录(上)

发布于  at 11:21 AM

bytes 和 Str

Python 字符串的最佳实践:推荐将外部输入 bytes,然后转换成 str,最终给外部输出时再转换成 bytes,避免编码问题。

一定要把解码和编码操作放在界面最外层来做,让程序的核心部分可以使用 Unicode 数据来运作,这种办法通常叫作 Unicode 三明治(Unicode sandwich)​。

enumerate

enumerate 能够把任何一种迭代器(iterator)封装成惰性生成器(lazy generator)。

>>> a = ['a', 'b', 'c', 'd']
>>> it = enumerate(a)
>>> next(it)
(0, 'a')
>>> next(it)
(1, 'b')

另外,还可以通过 enumerate 的第二个参数指定起始序号,这样就不用在每次打印的时候去调整了。

>>> it = enumerate(a, 1)
>>> next(it)
(1, 'a')
>>> next(it)
(2, 'b')

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

zip

内置的 zip 函数可以同时遍历多个迭代器。zip 会创建惰性生成器,让它每次只生成一个元组,所以无论输入的数据有多长,它都是一个一个处理的。

>>> a = ['a', 'b', 'c']
>>> b = [1, 2, 3]
>>> for a, b in zip(a, b):
...     print(a, b)
...
a 1
b 2
c 3

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

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

使用星号 unpacking 操作来捕获多个元素

>>> a, b, *others = [1,2,3,4,5,6]
>>> a
1
>>> b
2
>>> others
[3, 4, 5, 6]

使用星号 * 相比用切片更加清晰,而且不容易出错。

字典顺序

从 Python 3.6 开始,字典会保留这些键值对在添加时所用的顺序,而且 Python 3.7 版的语言规范正式确立了这条规则。

由于 Python 不是静态语言,大多数代码都以 鸭子类型(duck typing)机制运作(也就是说,对象支持什么样的行为,就可以当成什么样的数据使用,而不用执着于它在类体系中的地位)​。

那么假定字典是按键值对顺序这个假设就未必成立。

遇到意外应该抛出异常,不应该使用 None

用返回值 None 表示特殊情况是很容易出错的,因为这样的值在条件表达式里面,没办法与 0 和空白字符串之类的值区分,这些值都相当于 False

def careful_div(a, b):
  try:
    return a / b
  except ZeroDivisionError:
    raise ValueError('Invalid inputs')

不要返回列表,而应该使用生成器

def index_words(text):
    result = []
    if text:
        result.append(0)

    for index, letter in enumerate(text):
        if letter == ' ':
            result.append(index + 1)
    return result

>>> address = 'Four score and seven years ago...'
>>> result = index_words(address)
>>> print(result[:10])
[0, 5, 11, 15, 21, 27]

这个方法有两个缺点:

用生成器就不会有这个问题,可以接受任意长度的输入,并且内存消耗量很低。

def index_words_iter(text):
    if text:
        yield 0

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

>>> it = index_words_iter(address)
>>> print(next(it))
0
>>> print(next(it))
5
>>> print(next(it))
11
>>> result = list(index_words_iter(address))
>>> print(result[:10])
[0, 5, 11, 15, 21, 27]

用纯属性与修饰器取代旧式的setter与getter方法

给新类定义接口时,应该先从简单的 public 属性写起,避免定义 setter 与 getter 方法。

class Resistor:
  def __init__(self, ohms):
    self.ohms = ohms
    self.voltage = 0
    self.current = 0

r1 = Resistor(50e3)
r1.ohms = 10e3

如果想设置属性的时候,实现特别的功能,那么可以使用 @property 修饰器封装获取属性的方法。

class VoltageResistance(Resistor):
  def __init__(self, ohms):
    super().__init__(ohms)
    self._voltage = 0

  @property
  def voltage(self):
    return self._voltage

  @voltage.setter
  def voltage(self, voltage):
    self._voltage = voltage
    self.current = self._voltage / self.ohms

@property 最大的缺点是,通过它而编写的属性获取及属性设置方法只能由子类共享。与此无关的类不能共用这份逻辑。

try / except / else / finally

在 Python 代码中处理异常,需要考虑四种情况,对应 try / except / else / finally

try / finally

无论某段代码有没有出现异常,与它配套的清理代码都必须得到执行,同时还想在出现异常的时候,把这个异常向上传播,那么可以将这两段代码分别放在 try/finally 结构的两个代码块里面。

最典型的例子,确保文件句柄能够关闭。(当然,用 with 是更优雅的选择)

def try_finally_example(filename):
  print(" Opening file")

  # 有可能会 OSError,所以需要在 try 之前
  handle = open(filename, encoding='utf-8')

  try:
    print(" Reading data")
    return handle.read()
  finally:
    print(" Calling close()")
    handle.close()

try/except/else

如果你想在某段代码发生特定类型的异常时,把这种异常向上传播,同时又要在代码没有发生异常的情况下,执行另一段代码,那么可以使用 try/except/else 结构表达这个意思。

实现这样一个 load_json_key 函数,让它把 data 参数所表示的字符串加载成 JSON 字典,然后把 key 参数所对应的键值返回给调用方。

import json

def load_json_key(data, key):
  try:
    print("Loading JSON data")
    # 也许会有 ValueError
    result_dict = json.loads(data)
  except ValueError as e:
    print("Handling ValueError")
    raise KeyError(key) from e
  else:
    print("Looking up key")
    return result_dict[key]

完整的 try/except/else/finally

如果四个代码快都要用到,可以使用完整的 try/except/else/finally

例如,我们要把待处理的数据从文件里读出来,然后加以处理,最后把结果写回文件之中。

在实现这个功能时,可以把读取文件并处理数据的那段代码写在 try 块里面,并用 except 块来捕获 try 块有可能抛出的某些异常。

如果 try 块正常结束,那就在 else 块中把处理结果写回原来的文件(这个过程中抛出的异常会直接向上传播)​。

UNDEFINED = object()

def divide_json(path):
  print('Opening file')
  handle = open(path, 'r+')

  try:
    print('Reading data')
    data = handle.read()
    print('Loading JSON data')
    op = json.loads(data)
    print('Performing calculation')
    value = (
      op['numerator'] / op['denominator']
    )
  except ZeroDivisionError as e:
    print('Handling ZeroDivisionError')
    return UNDEFINED
  else:
    print('Writing calculation')
    op['result'] = value
    result = json.dumps(op)
    handle.seek(0)
    handle.write(result)
    return value
  finally:
    print('Calling close()')
    handle.close()

考虑用 contextlib 和 with 语句来改写 try/finally

Python 里的 with 语句可以用来强调某段代码需要在特殊情境之中执行。

例如,如果必须先持有互斥锁,然后才能运行某段代码。

from threading import Lock

lock = Lock()
with lock:
  # Do somthing

这样和 try/finally 是一样的,这是因为 Lock 类做了专门的设计,它结合 with 结构使用,也是一样的:

lock.acquire()

try:
  # Do something
finally:
  lock.release()

跟 try/finally 结构相比,with 语句的好处在于,写起来比较方便,我们不用在每次执行这段代码前,都通过 lock.acquire() 加锁,而且也不用总是提醒自己要在 finally 块里通过 lock.release() 解锁。

contextlib

如果想让其它对象和函数,也能像 Lock 这样用在 with 语句里,可以用内置的 contextlib 模块实现。

这个模块提供了 contextmanager 修饰器,可以让没有特殊处理的函数也能支持 with 语句。

相比于定义的 __enter____exit__ 的特殊方法,这样会方便很多。

例如,临时修改日志级别。

from contextlib import contextmanager

@contextmanager
def debug_logging(level):
  logger = logging.getLogger()
  old_level = logger.getEffectiveLevel()
  logger.setLevel(level)
  try:
    yield
  finally:
    logger.setLevel(old_level)

系统开始执行 with 语句时,会先把 @contextmanager 所修饰的 debug_logging 辅助函数推进到 yield 表达式所在的地方。

接着开始执行 with 结构的主体部分。如果执行 with 语句块(也就是主体部分)的过程中发生异常,那么这个异常会重新抛出到 yield 表达式所在的那一行里,从而为辅助函数中的 try 结构所捕获。

with…as…

with 语句还有一种写法 with...as...,可以把情景管理器所返回的对象赋给 as 右侧的局部变量。这样,with 结构的主体就可以通过局部变量与情景管理器所针对的情景交互了。

with open('my_output.txt', 'w') as handle:
  handle.write('This is some data!')

与手动打开并关闭文件句柄的写法相比,这种写法更符合 Python 的风格。

要实现这种这种效果,只需要 yield 后面带上该变量即可。

@contextmanager
def debug_logging(level):
  logger = logging.getLogger()
  old_level = logger.getEffectiveLevel()
  logger.setLevel(level)
  try:
    yield logger # 注意这里!
  finally:
    logger.setLevel(old_level)
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

上一篇
槽边往事:准备迎接后疫情时代
下一篇
在 Go 语言中实现依赖注入