To Be Python Ⅲ 函数

7erry

不要把函数返回的多个数值拆分到三个以上的变量中

函数返回多个返回值时实际上是返回了一个元组,同时返回的返回值就是元组中的元素。由于Python 中存在 unpacking 机制,函数返回的多个返回值组成的元组可以被等号左边的变量接收。当函数存在较多返回值时,接收返回值时很容易出现搞错顺序导致出错或代码过长而看起来很别扭的情况。为了避免这些问题,最好不要吧函数返回的多个值拆分到三个以上的变量里,如果要拆分的值确实很多,最好定义一个轻便的类或 namedtuple ,并让函数返回这样的实例

遇到意外情况应该返回异常而非返回 None

编写工具函数时,许多 Python 程序员喜欢用 None 这个返回值表示特殊情况。在一些情况下,这样做是有道理的。例如我们需要编写一个辅助函数计算两数相除的结果。在除数是 0 的情况下,返回 None 似乎相当合理,因为这种除法是没有意义的。

1
2
3
4
5
def devide(a,b):
try:
return a/b
except ZeroDivisionError:
return None

针对这个辅助函数的处理方式可能是

1
2
3
4
x , y = 1 , 0
result = devide(x,y)
if not result :
print("Invalid input")

但如果这个函数的被除数为 0 ,那么函数的返回结果为 0 ,但此时程序也会输出 “Invalid input”。这样的写法经常出现在 Python 代码中,因此像 devide 函数这样用 None 表示特殊状态的函数很容易出错。有两种办法可以减少这样的错误:
第一种方法是,利用二元组把计算的结果分成两部分返回,元组的首个元素表示操作是否成功,第二个元素表示计算的实际值。这样写可以促使调用函数的人去拆分返回值,他可以先看看这次运算是否成功,然后决定怎么处理运算结果

1
2
3
success , result = devide(x,y)
if not success:
print("Invalid input")

但问题是,有一些调用方总喜欢忽略返回元组的第一个部分,这样写成的代码乍一看没有问题,但还是存在无法区分返回 0 和返回 None 这两种情况

1
2
3
_ , result = devide(x,y)
if not result:
print("Invalid input")

第二种方法比第一种更好,就是不采用 None 表示特例,而是向调用方抛出异常或将异常向上传递,让调用方去处理,也就是

1
2
3
4
5
def devide(x,y):
try:
return a / b
except ZeroDivisionError:
raise ValueError("Invalid inputs")

这样,只要函数能拿到返回值,就说明函数肯定顺利执行完了,所以只需要用 try 把函数包起来并在 else 块中处理玉暖结果即可,输入、输出与异常都很清晰,可以大大减小调用方出错的概率

在闭包里使用外围作用域中的变量

又是我们要给列表中的元素排序,而且要优先把某个群组之中的元素放在其他元素的前面。例如,渲染用户界面的时候,关键的信息和特护的时间应该优先显示在其他信息之前。实现这种做法的一种常见方案,是通过辅助函数通过 key 参数传给列表的 sort 方法,让这个方法根据辅助函数所返回的值决定元素在列表中的先后顺序。这一功能建立在,体现在 Python 支持闭包,函数在 Python 里是头等对象等 Python 对函数式编程的支持上。在表达式中引用某个变量时,Python 解释器会按照下面的顺序,在各个作用域里面查找这个变量,以解析这一引用:

  1. 当前函数的作用域
  2. 外围作用域(例如包含按当前函数的其他函数所对应的作用域)
  3. 包含当前代码的哪个模块所对应的作用域(也叫全局作用域)
  4. 内置作用域(built-in scope ,也就是包含 len 与 str 等函数的那个作用)

这是变量出现在赋值符号右边时,怎么认定。而当变量出现在赋值符号左边,也就是当我们给变量赋值时,需要分两种情况处理。如果变量已经定义在当前作用域中,那么直接把新值交给它即可。如果不存在这个变量,那么即便外围作用域中有同名的变量,Python 还是会把这一赋值操作当成变量的定义来处理。这会产生一个重要的效果,也就是说,Python 会把包含赋值操作的这个函数当成新定义的这个变量的作用域。这种问题又是也称做作用域bug ( scoping bug ),Python 新手可能认为这样的赋值规则很奇怪,但实际上 Python 是故意这样设计的。因为这样可以防止函数中的局部变量污染外围模块。加入不这样做,那么函数中的每条赋值语句都有可鞥影响全局作用域的变量,这样不仅混乱,而且会让全局变量之间彼此交互影响,从而导致很多难以探查的 bug

Python 有一种特殊的写法,可以把闭包里面的数据赋给闭包外面的变量——用 nonlocal 语句描述变量,就可以让 Python 在处理这个变量的赋值操作时,去外围作用域查找。不过 nonlocal 有一个限制,就是不能侵入模块级别的作用域,以防污染全局作用域。nonlocal 语句可以清楚地表明,我们要把数据赋给闭包之外的变量,一个与它互补的语句,叫做 global , 用 global 描述某个变量后,在这个变量赋值时,Python 会直接把它放到全局作用域中

但全局变量显然不应该滥用,nonlocal 最好只使用在简单的函数中,因为它造成的副作用又是很难发现。尤其是在那种比较长的函数里,nonlocal 语句与其关联变量的赋值操作之间可能隔得很远

用数量可变的位置参数给函数设计清晰的参数列表

让函数接收数量可变的位置参数,可以把函数设计得更加清晰(这些位置参数通常简称 varargs,或者叫做 star arg,因为我们习惯用*args指代)。在很多情况下我们都无法确认有多少需要传给函数的参数,或者其实没有参数需要传入,但我们也必须传一个值给函数。使用位置参数与用在赋值语句左边的 unpacking 操作非常相似。而如果在传递参数时,想要把一个序列的所有元素都作为参数传递给函数,可以对序列使用*操作符让 Python 把序列中的元素都当成位置参数传给这个函数。

让函数接收数量可变的位置参数,可能导致两个问题:

第一个问题是,程序总是必须先把这些参数转化成一个元组,然后才能把它们当成可选的位置参数传给函数。这意味着,如果调用函数时,把带*操作符的生成器传了过去,那么程序必须先把这个生成器里的所有元素迭代玩以便形成元组,然后才能继续往下执行。这个元组包含生成器所给出的每个值,这可能耗费大量内存,甚至会让程序崩溃。接收*args参数的函数,适合处理输入值不太多,而且数量可以提前预估的情况。在调用这种函数时,传给*args这一部分的应该是许多个字面值或变量名。

第二个问题是,如果用了 *args 之后,又要给函数添加新的位置参数,那么原有函数的所有操作都需要更新,这容易导致一些很难排查的 bug 。为了避免这种漏洞,在给这种函数添加参数时,应该使用只能通过关键字来指定的参数 ( keyword-only argument )。如果想做的更稳妥一些,可以添加类型注解

用关键字参数来表示可选行为

Python 允许在调用函数时按照位置传递参数。除了按位置传递外,还可以按关键字传递。如果两种方式混用,位置参数必须出现在关键字参数之前,否则会出错。每个参数只能指定一次,不能既通过位置指定,又通过关键字指定。如果有一份字典,而且字典里面的内容能够用来调用函数,即字典的 key 与函数的参数名一致,那么可以通过 ** 运算符让 Python 把字典里的键值对以关键字参数的形式传给函数。这种调用方式也可以和位置参数或关键字参数混用,只要不重复指定即可。

最推荐使用的关键字参数的灵活用法可以带来三个好处:

第一个好处是,用关键字参数调用函数可以提高代码的可读性,每个参数的含义都相当明了

第二个好处是,它可以带有默认值(设置为可选参数),该值在函数定义时指定,有效减少重复代码使代码看上去更干净

第三个好处是,开发者可以很灵活地拓展函数的参数,而不用担心会影响原有的函数调用代码,并有助于维护向后兼容

用 None 和 docstring 描述默认值会变的参数

可选参数的默认值可以被设置为一些函数的返回值。例如将 time 参数的默认值设置为 datetime.datetime.now() ,Python 会默认把调用函数的这一刻,也就是触发事件的那一刻的值作为 time 参数的默认值,而不会每一次调用函数都重新执行 now 函数重新计算。

要想在 Python 中实现默认值会变的参数,惯用的解决方案是把参数的默认值设置为 None ,同时在 docstring 文档里写明这个参数为 None 时函数的运作方式。把参数的默认值写成 None 还有个重要的意义,就是用来表示那种以后可能由调用者修改内容的默认值。例如对于这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test(arg = {}):
print(arg)
return arg

foo = test()
foo['a'] = 'a'
bar = test()
bar['b'] = 'b'

print(foo)
print(bar)

#{}
#{'a': 'a'}
#{'a': 'a', 'b': 'b'}
#{'a': 'a', 'b': 'b'}

我们的本意是通过这两次调用得到两个空白字典,分别用于存放对应的数据。但实际上,只要修改其中一个字典,另一个字典的内容就会受影响。这实际上是因为 foo 和 bar 实际上是同一个字典,即 Python 一开始给 default 参数确认默认值时分配的字典。它们表示的时同一个字典对象。要解决这个问题,可以使用上述方式把默认值设置为 None 解决

用只能以关键字指定和只能按位置传入的参数设计清晰的参数列表

在定义函数时,可以使用 * 符号把参数分成两组,* 左边的参数和普通参数相同,而星号右边的参数只能用关键字指定参数的方式进行传参。

Python 3.8 引入了一项新特性,即只能按照位置传递的参数,与 * 类似,开发者可以使用 / 符号,使得位于它左边的参数只能由位置指定,对于以下代码

1
def function(arg1,arg2,/,arg3,arg4,*,arg5,arg6)

位于 / 左侧的参数时只能按位置指定的参数,位于 * 符号右侧的参数则是只能按关键字形式指定的参数,位于两个符号之间的参数,则是普通的既可以按照位置又可以按照关键字形式指定的参数

用 functools.wraps 定义函数修饰器

使用高级函数包装函数后,对象序列化器或一些其他的函数相关的工具例如内置的 help 函数会无法正常运作,因为它不能确定受修饰的那个原始函数的位置。要想解决这些问题,可以改用 functool 内置模块之中的 wraps 辅助函数实现,这个修饰器可以把重要的元数据从内部函数复制到外部函数。除了它们之外,Python 函数还有很多标准属性,例如 __name__,__module__,__annotations__也应该在封装时进行保留,才能让相关接口正常运作。wraps 可以帮助保留这些属性,使程序表现出正确的行为

  • Title: To Be Python Ⅲ 函数
  • Author: 7erry
  • Created at : 2024-02-14 19:36:55
  • Updated at : 2024-02-14 19:36:55
  • Link: http://7erry.com/2024/02/14/To-Be-Python-Ⅲ/
  • License: This work is licensed under CC BY-NC 4.0.