Function Annotation
在写函数的时候,如果是C++这样的静态语言,会对参数的类型有明确的要求,但是python这样的动态语言中,并没有类型检查,变量类型是在运行期决定的。作为使用者有时就会很困惑,这个传入参数的类型,是什么类型都可以呢?还是作者有希望我传入某一些类型,而不要用其他类型。
要解决这个问题,我们可以采用Function annotation,提示使用者我们期望传入的参数。以一个简单例子来说:
def add_two_nums(x, y):
return sum((x, y)) # 当然通常不会这么写,只是作为演示
if __name__ == '__main__':
print(add_two_nums(1, 3)) # 4
print(add_two_nums("hello", "there")) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
尽管我们在函数名中已经提示了是数字的加和,但是难免有用户会进行不适当的输入。我们想要在不改变函数主体的情况下,提示用户我们期待的参数类型,可以用python 3.x提供的函数注释功能,用:
为参数增加注释,而用->
为返回值增加注释(这种注释在各种OJ网站中,如leetcode等尤其常见):
def add_two_nums(x: float, y: float) -> float:
return sum((x, y)) # 当然通常不会这么写,只是作为演示
if __name__ == '__main__':
print(add_two_nums(1, 3)) # 4
print(add_two_nums("hello", "there")) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
这样我们在使用pycharm等IDE时,会在第二次调用函数处出现警告:Expect type 'float', got 'str' instead
。当然这个提示信息视具体IDE而定,也可能不出现提示。
python解释器不会对这些注释添加语意或进行类型检查,但是它会保存在函数的数字签名中,如果用functionName.__annotation__
查看,就会给出相应的提示:
print(add_two_sums.__annotation__)
输出:
{'x': <class 'float'>, 'y': <class 'float'>, 'return': <class 'float'>}
此外也可以用inspect
模块查看函数完整的签名:
import inspect
def add_two_nums(x: float, y: float) -> float:
return sum((x, y)) # 当然通常不会这么写,只是作为演示
if __name__ == '__main__':
sig = inspect.signature(add_two_nums)
print(sig) # (x: float, y: float) -> float
函数参数的默认值
默认值的加入是想让函数在有些参数没有传入时能够使用默认值正常的工作。这是比较容易实现的:在函数定义时给参数一个默认值,需要注意的是有默认值的参数需要放到参数列表的最后,如:
def add_two_nums(x, y=1):
return sum((x, y))
if __name__ == '__main__':
print(add_two_nums(2)) # 3
print(add_two_nums(3, 2)) # 5
这样,函数在传入一个参数和两个参数时都可以正常工作了。但是设置参数的默认值也需要注意一些问题(这是读《python cookbook》学到的,强烈建议阅读):
- 默认参数如果传入的是不可变参数,只会在函数定义时被赋值一次,之后的调用中是不会再更改的:
someVal = 1
def add_two_nums(x, y=someVal):
return sum((x, y))
if __name__ == '__main__':
print(add_two_nums(2)) # 3
someVal = 10 # 修改someVal,再行调用add_two_nums试试
print(add_two_nums(3, 2)) # 5, y的默认值并没有更改
- 传入默认参数时不要传入可变类型,否则可能产生意想不到的问题:
someLst = [1, 2]
def add_two_nums(x, y=someLst):
return sum((x, *y))
if __name__ == '__main__':
print(add_two_nums(0)) # 3
someLst.append(10) # 修改可变类型someLst
print(add_two_nums(3)) # 16, 10被加入了someLst中
这个问题是由于python编译器Cpython中对不可变对象和可变对象在内存中的处理方式不同引起的。
- 测试是否有参数传入时,不要使用
None
或者0/False
,因为这些是可能会被用户传入的参数。
def check_param_nums(x, y=None):
if y is None: # 尝试检查是否有传入参数
print("输入了一个参数")
else:
print("输入了两个参数")
if __name__ == '__main__':
check_param_nums(0) # 输入了一个参数, 正常情况,没有问题
check_param_nums(3, None) # 输入了一个参数,
# 但是我们实际上给定了两个参数!
# 问题在于用户确实可能会将None传入的,它是合法且常用的Python类型
如果我们用如上函数检查输入了几个参数,尽管我们在第二次调用时确实输入了两个参数,但是反馈给我们的时我们仅输入了一个。这就是我们对例外值的设定不合理。
为了解决这个问题,我们在需要检查到底传入了几个参数的时候,最好使用自定义的一个类型,这样用户在传入值的时候并不可能预先知道我们设定的内置类型,也就能保证用户不管传入了几个参数我们都能准确探知:
_no_value = object() # 自定义的无传入值类型
def check_param_nums(x, y=_no_value):
if y is _no_value: # 尝试检查是否有传入参数
print("输入了一个参数")
else:
print("输入了两个参数")
if __name__ == '__main__':
check_param_nums(0) # 输入了一个参数
check_param_nums(3, None) # 输入了两个参数
- lambda函数的默认参数值:
if __name__ == '__main__':
x = 10
fun1 = lambda y: x + y
x = 20
fun2 = lambda y: x + y
rsl1 = fun1(10)
rsl2 = fun2(20)
print(rsl1, rsl2) # 30 40 而不是 20 40
从上面这个小例子中,我们可以知道对于自由变量x
,lambda
函数是在运行时绑定值,而非在定义时绑定值的。为了让lambda
函数在定义时就绑定到值,我们可以将需要绑定的变量设置为默认参数值:
if __name__ == '__main__':
x = 10
fun1 = lambda y, x = x: x + y
x = 20
fun2 = lambda y, x = x: x + y
rsl1 = fun1(10)
rsl2 = fun2(20)
print(rsl1, rsl2) # 20 40
这样在函数定义的时候,我们就默认为fun1
中的x
绑定了值10,为 fun2
中的x
绑定了值20。
partial
在写函数时,考虑到用户使用的方便程度,很多时候我们需要为用户提供一些更方便调用的函数,这些函数是通过调用更通用的函数来实现的。在实现这些类似“快捷方式”的函数时,我们可以用到functools
中的partial
来生成callable
对象。
例如我们写了个计算给定序列n阶矩的函数,但是考虑到计算二阶矩的需求很频繁,我们想要写一个专门用来计算2阶矩的函数,那么我们需要重新写一个么?并不需要!我们只需要用partial
为通用函数指定一部分变量就可以了,看下面的例子:
from functools import partial
def nth_order_moment(seq, order):
"""
计算给定序列的n阶矩
"""
seq_len = len(seq)
mean_val = sum(seq) / seq_len
return sum(pow(seq[i] - mean_val, order) for i in range(len(seq))) / seq_len
def second_order_moment(seq):
return partial(nth_order_moment, order=2)(seq)
if __name__ == '__main__':
seq = [1, 2, 3]
print(nth_order_moment(seq, 2)) # 0.6666666666666666
print(second_order_moment(seq)) # 0.6666666666666666
partial(func, *args, **kwargs)
基于传入的函数与可变(位置/关键字)参数来构造一个新函数。所有对新函数的调用,都会在合并了当前调用参数与构造参数后,代理给原始函数处理。通过查看源码,我们可以发现它工作的方式类似于利用类构造的装饰器。
尽量避免递归函数
python对递归的支持是很差的,表现在两个方面:
- 递归层数有限:python对最大递归深度有比较严格的限制,通过
sys.getrecursionlimit()
可以看到支持的最大递归深度,对于我的机器仅有1000。在计算量大的场合是难以使用这么小的递归深度的。 - 不能对尾递归进行优化:如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。尾递归可以认为改写为循环的形式。对于一般的高级语言,编译器和解释器会对尾递归进行优化,使得尾递归无论调用多少次,都使用一个栈帧,从而避免栈溢出。但是python并不会这么做。
因此,在python中应该尽量避免在输入数据规模较大的时候采用递归。