Python进阶 -- 函数的进阶话题

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

从上面这个小例子中,我们可以知道对于自由变量xlambda函数是在运行时绑定值,而非在定义时绑定值的。为了让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中应该尽量避免在输入数据规模较大的时候采用递归。

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

推荐阅读更多精彩内容