第二章 函数
14. 尽量用异常来表示特殊情况,而不要返回None。
- 用None这个返回值来表示特殊意义的函数,很容易使调用者犯错,因为None和0及空字符串之类的值,在条件表达式里都会评估为False。
- 函数在遇到特殊情况时,应该抛出异常,而不要返回None。调用者看到该函数的文档中所描述的异常之后,应该就会编写相应的代码来处理它们了。
示例
编写工具函数(utility function)时,程序员喜欢给None这个返回值赋予特殊含义。例如,两数相除,除数为0时返回None表示除数为0,似乎是合理的。
def divide(a, b):
try:
return a/b
except ZeroDivisionError:
return None
#在调用该函数时,调用者会对这个特殊含义做出解读。
result = divide(x, y)
if result is None:
print('Invalid inputs')
如果分子是0,那么结果为0 。但是对于if result is None
这句来说,会出问题。因为我们可能不会专门去判断函数的返回值是否为None,而是会假设如果返回结果与False等效的结果,就说明函数出错了。(如空值,0,空列表等)
x, y = 0, 5
result = divide(x, y)
if not result:
print('Invalid inputs') #这段代码是错误的
让函数返回None,可能会使调用它的人写出错误的代码。解决办法是不返回None,而是把异常抛向上一级,让调用者必须面对它。
def divide(a, b):
try:
return a/b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
这样调用者需要处理因输入值引起的异常了。
x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
print('Invalid inputs')
else:
print('Result is %.1f' % result)
15.了解如何在闭包里使用外围作用域中的变量。
- 对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量。
- 使用默认方式对闭包内的变量赋值,不会影响外围作用域中的同名变量。
- 在Python3中,程序可以在闭包内用nonlocal语句来修饰某个名称,使该闭包能够修改外围作用域中的同名变量。
16. 考虑用生成器来改写直接返回列表的函数
- 使用生成器比把收集到的结果放入列表里返回给调用者更加清晰。
- 由生成器函数所返回的那个迭代器,可以把生成器函数体中传给yield表达式的那些值依次产生出来。
- 无论输入量多大,生成器都能产生一系列输出,因为这些输入量和输出量都不会影响它在执行时所耗的内存。
#找到字符串中每个单词的首字母在整个字符串的位置
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index+1
调用该生成器函数返回的迭代器,可以传给内置的list函数,将其转换为列表,避免了占用大量内存。
address = 'Four score and seven years ago...'
result = list(index_words_iter(address))
17.在参数上面迭代时,要多加小心。
- 函数在输入的参数上面多次迭代时要当心,如果参数是迭代器,可能会出现奇怪的行为并错失某些值。因为迭代器有状态,只能产生一轮结果,并且在用完的(exhausted,又称耗尽的)迭代器上面继续迭代时不会报错。
- python的迭代器协议,描述了容器和迭代器应该如何与iter和next内置函数、for循环及相关表达式相互配合。
- 把iter方法实现为生成器,即可定义自己的容器类型。
- 想判断某个值是迭代器还是容器,可以拿该值为参数,两次调用iter函数,若结果相同,则是迭代器,调用内置的next函数即可令该迭代器前进一步。
18.用数量可变的位置参数减少视觉干扰(visual nosie)。
- 在def语句中使用*args,即可令函数接受数量可变的位置参数。
- 调用函数时,可以采用*操作符,把序列中的元素当成位置参数,传给该函数。
- 对生成器使用*操作符,可能导致程序耗尽内存并崩溃。
- 在已经接受*args参数的函数上面继续添加位置参数,可能会产生难以排查的bug。
#定义log函数以便打印调试函数,参数固定,即使没有值要打印也要传递空列表
def log(message, values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str)
#改为可选的位置参数(星号参数)能更清晰,减少干扰。
def log(message, *values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str)
#如果传递列表给可变长的函数,调用时可以在列表前加上*,这样python会把列表元素视为位置参数。
favorites = [7, 33, 99]
log('Favorite colors', *favorites)
#采用数量可变的位置参数会引起两个问题。第一,变长参数在传递时先转化成元组。
#如果把(*生成器)作为参数,python必须迭代一轮,并放入元组,可能会消耗大量内存。
def my_generator():
for i in range(10):
yield id
def my_func(*args):
print(args)
it = my_generator()
my_func(it)
#第二个问题是在已经接受*args参数的函数上面继续添加位置参数,可能会产生难以排查的bug。
def log(sequence, message, *values):
if not values:
print('%s: %s' % (sequence, message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s: %s' % (sequence, message, values_str)
log(1, 'Favorites', 7, 33)#New usage is OK
log('Favorite numbers', 7, 33)#Old usage breaks
>>>
1: 'Favorites': 7, 33
'Favorite numbers': 7: 33
#这里的问题在于第二条log语句是以前写好的,当时的log函数还没有sequence参数,
#现在多了这个参数,使7变成了message参数的值,这种bug很难追踪,因为代码仍然运行且不抛出异常。
#为了避免这种问题,我们应该使用只能以关键字形式指定的参数,来扩展这种接收*args的函数(21条)
19.用关键字参数来表达可选的行为。
- 函数参数可以按位置或关键字来指定。
- 只使用位置参数来调用函数,可能会导致这些参数值的含义不够明确,而关键字参数则能够阐明每个参数的意图。
- 给函数添加新的行为时,可以使用带默认值的关键字参数,以便与原有的函数调用代码保持兼容。
- 可选的关键字参数总是应该以关键字形式来指定,而不应该以位置参数的形式来指定。
20.用None和文档字符串来描述具有动态默认值的参数。
- 参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次。对于{}或[]等动态的值,可能会导致奇怪的行为。
- 对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为None,并在函数的文档字符串里面描述该默认值所对应的实际行为。
def log(message, when=datetime.now()):
print('%s: %s' % (when, message))
#参数的默认值会在模块加载进来的时候求出,一旦加载进来,默认值就固定不变了,程序不会再次执行datetime.now()
>>> log('Hi there!')
2019-03-19 11:16:00.139197: Hi there!
>>> log('Hi, again!')
2019-03-19 11:16:00.139197: Hi, again!
#若要实现动态默认值,习惯上是把默认值设为None,并在文档字符串中把None对应的实际行为描述出来
def log(message, when=None):
'''Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occured.
Defaults to the present time.
'''
when = datetime.now() if when is None else when
print('%s: %s' % (when, message))
>>> log('Hi there!')
2019-03-19 11:23:34.145581: Hi there!
>>> log('Hi, again!')
2019-03-19 11:23:41.577505: Hi, again!
##注意下面
Note = '如果参数的实际默认值是可变类型(mutable),如{},[],则一定要用None作为形式上的默认值。'
21. 用只能以关键字形式指定的参数来确保代码清晰。
- 关键字参数能够使函数调用的意图更加明确。
- 对于各参数之间很容易混淆的函数,可以声明只能以关键字形式指定的参数,以确保调用者必须通过关键字来指定它们。对于接受多个Boolen标志的函数,更应该这么做。
- 在编写函数时,Python3有明确的语法来定义这种只能以关键字形式指定的参数。
#要计算两数相除,同时忽略OverflowError异常并返回0,忽略ZeroDivisionError,返回无穷。
def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
#调用函数
result = safe_division(1.0, 10**500, True, False)
print(result) >>>0.0
result = safe_division(1, 0, False, True)
print(result)>>>inf
#但是调用者调用代码时可能分不清这两个参数。
#提升代码可读性的方法是采用关键字参数。
def safe_division(number, divisor, ignore_overflow=False, ignore_zero_division=Flase):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
result = safe_division(1.0, 10**500, ignore_overflow=True)
result = safe_division(1, 0, ignore_zero_division=True)
#但这种方式也有缺陷,因为关键字参数是可选的,所以没办法确保函数调用者一定会使用关键字来指定这些参数的值,依然可以使用位置参数的形式调用它。
result = safe_division(1.0, 10**500, True, False)
#对于这种复杂的函数来说,最好保证调用者必须以清晰的调用代码来阐明调用该函数的意图。
#python3可定义必须以关键字形式指定参数。
#*标志位置参数就此终结,之后的参数只能以关键字形式指定。
def safe_division(number, divisor, * ignore_overflow=False, ignore_zero_division=Flase):
#这样不能以位置参数的形式来指定关键字参数了。
result = safe_division(1.0, 10**500,True,False)#错误
#但依然可以用关键字的形式指定,如果不指定,依然会采用默认值。
result = safe_division(1.0, 10**500, ignore_overflow=True)