翻译《Writing Idiomatic Python》(二):函数、异常
原书参考:http://www.jeffknupp.com/blog/2012/10/04/writing-idiomatic-python/
上一篇:翻译《Writing Idiomatic Python》(一):if语句、for循环
下一篇:翻译《Writing Idiomatic Python》(三):变量、字符串、列表
1.3 函数
1.3.1 避免使用可变对象作为函数参数的默认值
当Python解释器遇到一个函数定义的时 候,如果参数里有默认值,会求值来决定默认的参数值。然而这种求值仅会被触发一次。当调用函数的时候不会再次对默认参数求值。因为首次求值后的值会被接下 来所有的调用使用,所以使用一个可变的对象作为默认值往往会导致并不想要的结果。
一个可变对象是那些值可以被直接改变的对象。list,dict,set和大部分类的实例都是可变的,比如我们可以通过调用append来给一个list添加一个元素,进而改变了该list的值。
相对地,不可变对象,指那些创建后就不能被改变的对象。string,int和tuple都是不可变对象的例子。举例来说,我们没法直接改变string对象的值,所有改变string内容的操作事实上都会给当前string变量赋上一个新的string对象。
所 以为什么这个对函数参数默认值很重要呢?前面提到默认的参数值在初始化之后就会在每次被调用时不断地被重复使用。对于不可变对象而言,这没什么影响,因为 我们在函数定以后没法直接改变参数的默认值。然而对于可变对象,值的改变会在之后的所有调用中体现出来。在下面的例子(Python官方辅导中的例子) 中,一个空的list被当做默认参数值,如果在这样一个函数中,执行的操作是给这个list增加一个元素。那么每次调用后增加的元素会被保留在默认参数中的list里,而这个增加的元素会被保留住,进而在下一次的函数调用中被当做默认值赋给参数。该list参数不会被重置为空,而是不断地被接下来的每次函数调用所使用。
不良风格:
1 def f(a, L=[]):
2 L.append(a)
3 return L
4
5 print(f(1))
6 print(f(2))
7 print(f(3))
8
9 # This will print
10 #
11 # [1]
12 # [1, 2]
13 # [1, 2, 3]
地道Python:
1 def f(a, L=None):
2 if L is None:
3 L = []
4 L.append(a)
5 return L
6
7 print(f(1))
8 print(f(2))
9 print(f(3))
10
11 # This will print
12 #
13 # [1]
14 # [2]
15 # [3]
// 如果我们查看第一个函数的字节码指令,会发现对L的处理只是一个简单的 LOAD_FAST 1 (L),这在效果上和传入一个自己定义的list是没有任何区别的。而在第二个函数里默认空list的实现会对应一个 BUILD_LIST 0 和 STORE_FAST 1 (L) 来实现一个默认的空list。
1.3.2 使用return直接对表达式求值并返回
很多情况下,你编写的函数会返回一个或多个值。需要注意的是,return语句实际上并不仅仅是返回变量包含的值,而是返回对return后语句求值后的结果(例如: return is_hotdog == True )。所以如果仅仅返回一个单独的值的话,相当于对值本身进行了一次求值并返回。
鉴于此,和吧表达式的结果赋给一个新创建的变量并在下一行返回这个变量的写法比起来,直接把表达式放在return语句后面会显得更加简单明了。
不良风格:
1 def all_equal(a, b, c): 2 result = False 3 if a == b == c: 4 result = True 5 return result
地道Python:
1 def all_equal(a, b, c): 2 return a == b == c
// 第一个函数的字节码对应如下:
1 LOAD_FAST 3 (result) 2 RETURN_VALUE
只是单纯地加载然后返回,所以看上去确实是有些冗余的写法。然而在有些代码和流程中,用一个单独的值来存储返回的值会让结构更加清晰并且调试也更加方便,所以这是一个见仁见智的问题。个人看法,对于很短小简单的函数还是应该直接返回表达式,其他情况要具体分析。
1.3.3 学会恰当地使用关键字参数(keyword argument)
当我们写一个函数时,很多时候会发现使用时函数仅仅需要所有参数中的几个参数值,只有在少数情况下才需要其他一些不常用的参数值。打印函数就是一个很好的例子。绝大多数情况下,我们只是简单地把要打印的对象作为参数传递给打印函数。然而有些时候我们可能希望实现用不同的字符来隔开不同值的打印对象的效果,比如默认情况下用' '分隔打印的字符串,同时可以设置自己想要的其他字符(串)作为打印对象间的分隔符。
这时用关键字参数可以轻松实现这种效果。从形式上而言,关键字参数和其他“普通”参数的区别在于是否有=并赋给默认值的形式。就我们这里的打印函数例子而言,函数的形式类似于 def print_list(*values, sep=' ') 。*values包含我们要打印的所有值。sep是一个默认值为' '的关键字参数。
关键字参数最有用的一个特性是在调用函数时作为可选的参数。也就是说我们可以通过关键字参数来在少数需要的情况下添加额外的输入信息,而在大多数情况下直接使用默认的参数作为输入。就我们上面提到的打印函数而言,如果不这么做,我们则总是需要显式地在参数中指定分隔符,即使大多数情况下我们已经知道分隔符为' ',这种重复的劳动显得有些没意义。
不良风格:
1 def print_list(list_value, sep): 2 print('{}'.format(sep).join(list_value)) 3 4 the_list = ['a', 'b', 'c'] 5 the_other_list = ['Jeff', 'hates', 'Java'] 6 print_list(the_list, ' ') 7 print_list(the_other_list, ' ') 8 print_list(the_other_list, ', ')
地道Python:
1 def print_list(list_value, sep=' '): 2 print('{}'.format(sep).join(list_value)) 3 4 the_list = ['a', 'b', 'c'] 5 the_other_list = ['Jeff', 'hates', 'Java'] 6 print_list(the_list) 7 print_list(the_other_list) 8 print_list(the_other_list, ', ')
1.3.4 使用*args和**kwargs来接受任意参数输入
很多情况下,函数需要接受包含任意位置参数和关键字参数的列表作为输入,使用其中的一部分,再把其他参数传递给另一个函数。这时可以使用*args和**kwargs作为函数的参数来分别接收位置参数和关键字参数。
这种用法同时在维护API的后向兼容性上非常有用。如果我们的函数可以接收任何形式的参数,那么在新版本的代码中我们就可以很自由地添加新的参数作为函数的输入,同时以最小的代价保证不破坏原有的代码。
虽然*arg和**kwargs如此灵活,但并不是说我们就应该停止在函数中使用命名的参数。事实上大多数情况下我们仍然应该使用命名参数,只有当*args和**kwargs非常有用或必要时才使用它们。
不良风格:
1 def make_api_call(foo, bar, baz): 2 if baz in ('Unicorn', 'Oven', 'New York'): 3 return foo(bar) 4 else: 5 return bar(foo) 6 7 # I need to add another parameter to make_api_call 8 # without breaking everyone's existing code. 9 # I have two options... 10 11 def so_many_options(): 12 # I can tack on new parameters, but only if I make 13 # all of them optional 14 def make_api_call(foo, bar, baz, qux=None, foo_polarity=None, 15 baz_coefficient=None, quux_capacitor=None, 16 bar_has_hopped=None, true=None, false=None, 17 file_not_found=None): 18 # ... and so on ad infinitum 19 return file_not_found 20 21 def version_graveyard(): 22 # ... or I can create a new function each time the signature 23 # changes 24 def make_api_call_v2(foo, bar, baz, qux): 25 return make_api_call(foo, bar, baz) - qux 26 27 def make_api_call_v3(foo, bar, baz, qux, foo_polarity): 28 if foo_polarity != 'reversed': 29 return make_api_call_v2(foo, bar, baz, qux) 30 return None 31 32 def make_api_call_v4(foo, bar, baz, qux, foo_polarity, baz_coefficient): 33 return make_api_call_v3(foo, bar, baz, qux, foo_polarity) * baz_coefficient 34 35 def make_api_call_v5(foo, bar, baz, qux, foo_polarity, 36 baz_coefficient, quux_capacitor): 37 # I don't need 'foo', 'bar', or 'baz' anymore, but I have to 38 # keep supporting them... 39 return baz_coefficient * quux_capacitor 40 41 def make_api_call_v6(foo, bar, baz, qux, foo_polarity, baz_coefficient, quux_capacitor, bar_has_hopped): 42 if bar_has_hopped: 43 baz_coefficient *= -1 44 return make_api_call_v5(foo, bar, baz, qux, foo_polarity, baz_coefficient, quux_capacitor) 45 46 def make_api_call_v7(foo, bar, baz, qux, foo_polarity, baz_coefficient, quux_capacitor, bar_has_hopped, true): 47 return true 48 49 def make_api_call_v8(foo, bar, baz, qux, foo_polarity, baz_coefficient, quux_capacitor, bar_has_hopped, true, false): 50 return false 51 52 def make_api_call_v9(foo, bar, baz, qux, foo_polarity, baz_coefficient, quux_capacitor, bar_has_hopped, 53 true, false, file_not_found): 54 return file_not_found
地道Python:
1 def make_api_call(foo, bar, baz): 2 if baz in ('Unicorn', 'Oven', 'New York'): 3 return foo(bar) 4 else: 5 return bar(foo) 6 7 # I need to add another parameter to `make_api_call` 8 # without breaking everyone's existing code. 9 # Easy... 10 11 def new_hotness(): 12 def make_api_call(foo, bar, baz, *args, **kwargs): 13 # Now I can accept any type and number of arguments 14 # without worrying about breaking existing code. 15 baz_coefficient = kwargs['the_baz'] 16 17 # I can even forward my args to a different function without 18 # knowing their contents! 19 return baz_coefficient in new_function(args)
1.3.5 把函数当成值
在Python中,所有的东西都是对象,包括函数也不例外。因此函数也可以被当做值被赋给变量,或是传递到其他的函数中,又或是作为值从其他函数中返回。有的时候把函数当做值来处理会让代码更加清晰。
不良风格:
1 def print_addtion_table(): 2 for x in range(1, 3): 3 for y in range(1, 3): 4 print(str(x + y) + '\n') 5 6 def print_subtraction_table(): 7 for x in range(1, 3): 8 for y in range(1, 3): 9 print(str(x - y) + '\n') 10 11 def print_multiplication_table(): 12 for x in range(1, 3): 13 for y in range(1, 3): 14 print(str(x * y) + '\n') 15 16 def print_division_table(): 17 for x in range(1, 3): 18 for y in range(1, 3): 19 print(str(x / y) + '\n') 20 21 print_addtion_table() 22 print_subtraction_table 23 print_multiplication_table 24 print_division_table
地道Python:
1 import operator as op 2 3 def print_table(operator): 4 for x in range(1, 3): 5 for y in range(1, 3): 6 print(str(operator(x, y)) + '\n') 7 8 # op.add, sub, mul, and div are all functions with the same implementation 9 # as their literal equivalents (i.e. `op.add` is the same as `+`) 10 for operator in (op.add, op.sub, op.mul, op.div): 11 print_table(operator)
1.3.6 使用函数版本的print
在Python 3.0中,print从一个语句关键字变成了内建的函数。这样做的原因在PEP 3105中有讲到:
• print作为一个特殊的语法不是必要的
• 常常有需要用其他方式实现print功能的需求,然而如果print只能作为特殊语法的话这样的需求会很难实现(相比起print作为函数)
• 用空格以外的字符来分开要打印的对象会很困难
从Python 2.6开始,通过__future__模块引入了后向移植机制,使我们可以在Python 2中限制只使用print函数。基于以上列出的各种原因,更重要的为了你的程序能够顺利从Python 2过渡到Python 3,应该优先使用函数版本的print。
不良风格:
1 print l, 'foo', __name__
地道Python:
1 from __future__ import print_function 2 print(l, 'foo', __name__)
// 事实上Python之所以会一开始就带print语句,很大程度上是受到ABC的影响,ABC是Guido在发明Python之前参加设计的另一种教学语言。
1.4 异常
1.4.1 不要害怕使用异常
许多语言里,异常被设计为只有非常特别的情况下才使用。举个例子,一个函数用文件名作为输入,在函数内部读取文件内容并做一些处理。如果文件没有找到,这个时候似乎我们不应该想到使用异常,因为这个情况相对来说不是很罕见。更合理的情况是,如果文件系统自身有问题,没能支持文件的读取,这个时候才应该抛出异常。
在其他语言中,合适抛出一个异常常常是取决于经验或者偏好,尽管常常看到的情况是,语言的初学者会比较倾向于使用异常来对付它们在写代码时遇到的各种意想不到的情况。然而对异常过度使用常常会导致许多问题:无论是调试还是读代码的时候,程序的流程都会变得很不清晰,另外在代码中产生另一个调用链常常会带来异常巨大的开销。基于这些原因在一些语言中异常的使用显得格外地不受欢迎,甚至一些公司或组织的标准中明确声明禁止使用异常(比如Google的C++风格指南)
在Python中,对此的有不同的观点。在许多非常流行的第三方包里,你会发现异常到处都是,在Python的标准库里也随处可见。事实上,异常在Python中已成为语言最基本的一部分,比如,你或许不知道的是,当你使用一个for循环的时候,你已经无意当中使用了异常了。
乍一听可能很奇怪,不过这是事实。不知你是否想过在Python中一个for循环是如何停止的?如果是list之类,很显然有确定的长度,这不是个问题。然而对于生成器(generator)呢,这种可以无休止地产生值情况又是如何处理的呢。
任何时候你遍历一个可遍历结构时(一般来说指那些有__iter__()和__getitem__()),都需要知道何时应该停止遍历,我们来看下面的代码:
1 #!py 2 words = ['exceptions', 'are', 'useful'] 3 for word in words: 4 print(word)
如何才能知道何时到达了最后一个元素并停止遍历呢?答案你可能会让你觉得惊讶:这个list会抛出一个StopIteration异常。
事实上,所有的可遍历结构都是如此。当一个for语句被解释时,会对将要遍历的对象调用iter()。这会给该对象创建一个iterator,作用是一个个返回该对象中的内容。为了能成功调用iter(),该对象要么得支持迭代协议(定义__iter__()),要么得支持序列协议(定义__getitem__())。
当遍历结束时,__iter__()和__getitem__()都需要抛出一个异常。__iter__()会抛出StopIteration,而__getitem__()会抛出IndexError,于是遍历就会停止。
所以每当你犹豫是否应该在Python中使用异常时,请记住:你很可能已经不知不觉使用了异常了。
1.4.2 使用异常写"EAFP"风格的代码
不使用异常的代码里经常会对要执行的操作进行预先检查。在下面的不良风格代码里,print_first_row函数看上去显得过于小心了。我们可以想想以下:“我想打印出一个数据库查询语句返回的第一条结果。那么我是否已经建立了一个和数据库的有效连接?我的查询是否顺利完成了?是否返回了结果?”
在考虑各种可能情况的场景下,写出的代码必须对这各种可能出现的问题进行与检查才能保证能顺利的执行某个操作。更重要的是,如果这各种我们想到的问题都已经被检查,那么我们就相当于在代码里假设我们接下来将要执行的部分,不管是如何的有风险,都一定会成功。
在这样的代码里,到处都是的if语句会让程序编写者和阅读代码的人都对代码的安全性有个错误的认识。编写程序的人会考虑到所有他能考虑到的可能出现错误的情况,于是他会理所应当地认为在做了这些预检查之后代码里已经没有任何错误。而读他代码的人也往往会类似地假设这些if语句已经处理各种可能出错的情况,所以代码中已经不可能出现有错误的情形,于是调用的时候也不需要考虑错误处理。
以这种风格编写的代码被称为“Look Before You Leap (LBYL)”风格,所有(自认为)需要考虑的情况都会被预先显式地检查。在这种方法中有一个非常明显的问题:如果代码并没有考虑到实际上所有可能发生的错误呢?那么结果通常很糟糕。而事实上,考虑到所有可能发生错误的情况是非常困难的。此外,就如在Python文档中指出的一样,这种风格的代码会在多线程环境中造成很大的风险,例如,一个在上一行代码中为真的if语句中的条件,可能在接下来的一行代码中就(因为别的线程中的改动)为假了。
另一种被称为"[it's] Easier to Ask for Forgiveness than Permission (EAFP)"的风格假设所有的代码都是好的,如果不好则捕获异常来处理。这种风格让程序编写者能够专注于代码本身要处理的事物,而不是考虑各种可能出现的错误,以及对应的处理办法。EAFP风格的代码将程序要实现的最终目的放到了最重要的位置,并且提高了代码的可读性,当然使用前提是你已经知道代码中可能会出现错误。
不良风格:
1 def get_log_level(config_dict): 2 if 'ENABLE_LOGGING' in config_dict: 3 if config_dict['ENABLE_LOGGING'] != True: 4 return None 5 elif not 'DEFAULT_LOG_LEVEL' in config_dict: 6 return None 7 else: 8 return config_dict['DEFAULT_LOG_LEVEL'] 9 else: 10 return None
地道Python:
1 def get_log_level(config_dict): 2 try: 3 if config_dict['ENABLE_LOGGING']: 4 return config_dict['DEFAULT_LOG_LEVEL'] 5 except KeyError: 6 # if either value wasn't present, a 7 # KeyError will be raised, so 8 # return None 9 return None
// 其实个人觉得在非常确定代码正确的情况下,两种风格都不用是最好的。当然这是非常难的。
1.4.3 避免用什么都不干的except语句块"吞噬"有用的异常
新手在不得不使用异常时常常容易犯的一个错误是对任何异常都用一个except捕获。这种情况在使用第三方包是尤为明显。程序作者把第三方包的使用都放在一个try语句块里,并且用一个except语句捕获,最后再打印一个笼统的报错语句,比如“出错了”。
异常之所以可以被追溯并且带有相应消息是有原因的:通过异常帮助调试并发现是哪里出错。如果用一个简单的except语句"吞噬"了所有的异常,那么异常自带的调试信息也被舍弃了。当然如果你确实不想处理捕获到的异常,而仅仅是想记录相应地信息,那么可以在except语句快中简单地放上一句raise,这样做不仅能让代码顺利执行下去,还可以保留可能有用的调试信息。
当然了,对于有些代码块,你需要的不是异常处理,而是保证代码绝对正确并且不会抛出任何异常,这本书中我们不会讨论这个问题,因为这得靠你自己的大脑。
不良风格:
1 import requests 2 def get_json_response(url): 3 try: 4 r = requests.get(url) 5 return r.json() 6 except: 7 print('Oops, something went wrong!') 8 return None
地道Python:
1 import requests 2 def get_json_response(url): 3 return requests.get(url).json() 4 5 # If we need to make note of the exception, we 6 # would write the function this way... 7 def alternate_get_json_response(url): 8 try: 9 r = requests.get(url) 10 return r.json() 11 except: 12 # do some logging here, but don't handle the exception 13 # ... 14 raise
转载请注明出处:達聞西@博客园