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]
这个方法有两个缺点:
- 代码杂乱,每次都要
append; - 所有结果都先保存在列表中,如果数据特别多,那么程序可能会消耗太多内存;
用生成器就不会有这个问题,可以接受任意长度的输入,并且内存消耗量很低。
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 结构表达这个意思。
- 如果
try块代码没有发生异常,那么else块就会运行 try里面应该尽量少写一些代码,这样阅读起来比较清晰
实现这样一个 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)