流畅的python:一等函数-Part1

把函数视作对象

接下来将要发布的部分是重点内容,除了标记选读部分,其余的都很重要,我在面试的时候不止一次被问到函数装饰器,函数这部分是进阶的证明,所以重视起来吧。

1、函数是一个对象

先看下面的这个简单例子:

def myprod(n):
    '''计算阶乘n!'''
    if n == 1:
        return 1
    else:
        return n*myprod(n-1)

我们生成了一个函数对象,三引号引起来的是函数的说明,当你调用myprod.__doc__属性时(或者help(myprod)),会返回该内容。同时,函数对象有自己的id,你也可以赋值给别的变量名。

>>>myprod.__doc__
'计算阶乘n!'
>>>another_prod = myprod
>>>another_prod(5)
120

函数也支持被其他函数调用,接受函数为参数,这种把函数作为结果返回的函数是高阶函数,比如map、sorted等

>>>list(map(myprod,range(1,6)))
[1, 2, 6, 24, 120]
>>>a = '12,345,342,45r,f,32'
>>>alist = a.split(',')
>>>sorted(alist, key=len)
['f', '12', '32', '345', '342', '45r']

2、匿名函数

匿名函数就是使用lambda关键字创建的函数,只能包含纯表达式,也就说定义体中既不能赋值,也不能使用while和try等Python语句。但是请注意,除了作为参数传给高阶函数之外,Python很少使用匿名函数。所以永远不要在这个上面展现你的才智,我曾经爱写这种表达式来实现简单函数,让匿名函数有自己的名字:

f = lambda x, y: x + y
print(f(2,3))

但这种简单的书写方法从来没给我带来任何好处,所以对于自定义函数来说,忘掉lambda,使用def是最合适的方法。

3、可调用对象

  • Python中有各种各样可调用的类型,常见的可调用对象包括用户自定义函数,内置函数,内置方法,方法,类,类的实例,生成器函数等共7种。

  • 调用运算符是(),就是写函数后面带的小括号,()除了是数学中的小括号,也是元组的表达符号,也是调用运算符哦,用处很多。

  • 判断对象能否调用,最安全的方法是使用内置的callable()函数:

    [callable(obj) for obj in (str,abs,13)] # [True, True, False]
    

事实上,只要一个对象实现了__call__魔法方法,它同样可以表现的像个函数

class When():
    def ask(self):
        print('啥时候能毕业呀')

    def __call__(self):
        print('啥时候能上会呀')


when = When()
when.ask() # 啥时候能毕业呀
when()     # 啥时候能上会呀
print(callable(when)) # True

如果没有实现__call__方法,callable(when)返回False

4、函数内省(选读)

函数中有很很多属性,可以使用dir函数进行查看:

>>> def f(): pass
>>> dir(f)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

大多数的属性都是python对象共有的。可以采用集合差集查看函数独有的属性:

def ask(): # 函数对象
    pass

class C():
    pass

obj = C() # 创建一个类实例
print(set(dir(ask)) - set(dir(obj))) 
# 返回
{'__qualname__', '__defaults__', '__get__', '__closure__', '__globals__', '__kwdefaults__', '__annotations__', '__name__', '__code__', '__call__'}

下面是这些属性的解释:

函数特有方法

5、函数参数

Python提供了极为灵活的参数处理机制,而且Python 3进一步提供了仅限关键字参数(keyword-only argument)。在开始之前,先介绍一下inspect模块,可以方便展示一个函数的参数。

inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。除此之外,inspect.Parameter对象还有一个annotation(注解)属性,它的值通常是inspect._empty,表示没有默认值。

from inspect import signature
def f(a, *b, c, d=None, **e):
    g = 3
    print("a=", a, "b=", b, 'c=', b, 'd=', d, 'e', e)
sig = signature(f)
for name, param in sig.parameters.items():
    print('参数类型:%s, 参数名:%s, 参数默认值:%s' % (param.kind, name, param.default))
# 返回
参数类型:POSITIONAL_OR_KEYWORD, 参数名:a, 参数默认值:<class 'inspect._empty'>
参数类型:VAR_POSITIONAL, 参数名:b, 参数默认值:<class 'inspect._empty'>
参数类型:KEYWORD_ONLY, 参数名:c, 参数默认值:<class 'inspect._empty'>
参数类型:KEYWORD_ONLY, 参数名:d, 参数默认值:None
参数类型:VAR_KEYWORD, 参数名:e, 参数默认值:<class 'inspect._empty'>

好了,上面一个简单的函数我们看到了有五种参数类型,分别是:

POSITIONAL_OR_KEYWORD : 可以通过定位参数和关键字参数传入的形参
VAR_POSITIONAL : 定位参数元组
VAR_KEYWORD : 关键字参数字典
KEYWORD_ONLY : 仅限关键字参数(Python 3 新增)

我们一个一个来解释:

5.1 位置参数POSITIONAL

这个最简单,就是按照位置顺序进行传参,大多数的传参都是基于此,使用的时候必须参数个数及位置是对应的,所以有人也叫它通过位置隐式填充。当然你可以使用关键字的方式传参以更改位置:

def func(a, b, c):
    print(a, b, c)
# 如果不指定关键字,形参a, b, c绑定的值, 完全取决于实参传入的顺序
func(1, 2, 3) 	    # 1 2 3
func(b=2, c=3, a=1) # 1 2 3

5.2 默认参数,关键字参数(具名参数)

关键字=值的方式传参,就是关键字参数啦,这种方式也被人叫做通过名字显式填充。默认参数是在函数定义时就给参数一个默认的参数值,如果函数调用时没有给这个参数传值,就使用默认值, 如果显式的传参了,就使用新传入的值代替默认值。

def func(a, b, c=3):
    print(a, b, c)
func(b=2, a=1) # 关键字传参
# 返回1 2 3

5.3 可变参数

可变参数就是一个形参可以接受多个实参,用∗ 标志这是一个可变参数,并将所有接受的值以元组的形式传入函数体内,如果可变参数没有接受到任何值,则传入一个空元组。

def func(a,b,*var):
    print('a=', a)
    print('b=', b)
    print('var=', var)
    
func(1,2,3,4,5)   # a,b填满后, 剩下的都传给可变参数
a= 1
b= 2
var= (3, 4, 5)

不过需要注意的是,函数调用的时候各种参数是有接受顺序的,例如下面的这个例子:

# --------------------------------A
def func(a, b, *var):
    print('a=', a)
    print('b=', b)
    print('var=', var)
func(1, 2, 3, a=4, b=5)
# 返回
# TypeError: func() got multiple values for argument 'a'

# --------------------------------B
def func(*var,a, b ):
    print('a=', a)
    print('b=', b)
    print('var=', var)
func(1, 2, 3, a=4, b=5)
# 返回
a= 4
b= 5
var= (1, 2, 3)

有没有感觉很怪异,为什么AB只是交换了参数顺序结果却不一致?这是因为函数的实际参数调用行为是:


  • 初始化时,所有的参数槽都被标记为空。

  • 位置参数第一个被赋值,接下来是关键字参数。

  • 对于每一个位置参数:

    • 先尝试将值绑定到第一个空的参数槽,如果这个参数槽不是一个可变变量的参数槽,将它标记为’filled
    • 否则,如果下一个空槽是可变参数槽,就将所有剩下的非关键字参数值放到这个可变参数槽里。
  • 对于每一个关键字参数:

    • 如果存在与关键字命名相同的参数,就把这个实参的值赋给这个关键字参数槽。如果所有的参数槽都已经被填满了,会引发错误异常
    • 否则,如果存在一个关键字 字典的关键字参数,这个参数会被添加到一个字典:关键字为此字典的键,如果这个键已经存在,会报错。
    • 否则, 如果不存在keyword dictionary, 而且没有匹配到参数名,报错。
  • 最后:

    • 如果可变变量的参数槽仍然没有填充,则赋一个空的元组作为它的值。

    • 对每一个剩余的空参数槽:如果这个参数槽有默认值,就把这个参数槽用默认值填充。如果没有默认值,报错。


现在我们根据以上行为确定这两段代码的执行过程:

代码A: 首先确定位置参数,a,b是,1,2分别赋值给a,b->下一个空槽是可变参数槽,所以var接收的是(3,)->又出现a=4这种关键字对已经赋完值的a赋值,程序报错。

代码B:首先确定位置参数,好吧,这个你可要注意了,没有位置参数->a,b是两个关键字参数,分别被赋值->剩余1, 2, 3赋值给可变参数槽var

所以尽管Python支持*varargs这样的语法实现可变参数的传递,但是实际传参过程中存在着位置参数、关键字参数、可变参数这样的顺序,所以在赋值的时候千万要注意。

5.4 可变关键字参数 var_keyword

可变关键字参数 用 双星号+参数名表示, 如:∗∗kwargs,可变关键字参数接收零个或多个关键字参数,并以字典的形式传入函数体,关键字为此字典的key,关键字绑定的值为value。如果可变关键字没有接收到任何参数, 则传入函数体一个空字典{}。

def func(a, b, *var, **kwargs):
    print('a=', a)
    print('b=', b)
    print('var=', var)
    print('kwargs=', kwargs)
func(1, 2, 3, c=4, d=5)
# 返回
a= 1
b= 2
var= (3,)
kwargs= {'c': 4, 'd': 5}

在实际使用时,还有一个有趣的用法**Adict:字典中的所有元素作为单个参数传入,同名键会绑定到对应的具名参数上,余下的则被**kwargs捕获。

def func(*var, a, b, **kwargs):
    print('a=', a)
    print('b=', b)
    print('var=', var)
    print('kwargs=', kwargs)
func(1, 2, **{'a': 4, 'c': 3, 'b': 2})
# 解包后,{'a': 4, 'c': 3, 'b': 2}中的a,b对应绑定到关键字参数中
# 返回
a= 4
b= 2
var= (1, 2)
kwargs= {'c': 3}

5.5 仅限关键字参数 KEYWORD_ONLY

仅限关键字参数就是只能传入关键字参数,不能通过其他方式传参,也一定不会捕获未命名的位置参数。也就是说仅限关键字参数不可缺省(除非有默认值),且只能强制性通过关键字传参。
可变参数后面的关键字参数都是仅限关键字参数,定义函数时若想指定仅限关键字参数,要把它们放到前面有*的参数后面。 若不想要可变参数,只要仅限关键字参数,可以省略可变参数名,只留一个 单星号*表示不接受任何可变参数,它可以作为普通参数的结束标志, 如下例:

def func(a,*,b):
    print('a=',a)
    print('b=',b)
## 必须必须强制通过关键字传参
func(1,2) # TypeError: func() takes 1 positional argument but 2 were given
func(b=1,a=2) # 正确

——未完待续——
欢迎关注我的微信公众号
扫码关注公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值