Effictive Python:编写高质量Python代码的59个有效方法(2)

第二章 函数

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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容