这次的python回顾来到函数的知识,首先来探讨一下作为一等兑现的函数。
相关的jupyte notebook文件在这里

函数作为对象

python中,函数是一等对象(first-class object),一等对象的定义为满足以下条件的程序实体:

  • 在运行时创建
  • 能够赋值给其他元素
  • 能作为参数传递给函数,且能作为函数的返回结果

下面以一个阶乘的例子来说明函数如何作为对象:

1
2
3
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

1
2
3
4
def factorial(n):
'''定义一个阶乘函数'''
return 1 if n < 2 else n * factorial(n -1 )

1
2
fact = factorial #将函数赋值给其他元素  
fact
<function __main__.factorial>
1
2
map(factorial, range(11)) #将函数作为参数 
print(list(map(factorial, range(11))))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

高阶函数

在上面的代码中,我们将factorial函数作为参数传给了map,后者将它的第一个参数(factorial)应用在第二个参数上(range(11))。像map这样能够接受函数作为参数,或者能够将函数作为结果返回的函数称作高阶函数

例如sorted函数就是这样一个高阶函数,它支持接受函数作为参数:它的key参数可以是一个函数,返回待排序元素的序列值,例如我们将文本的长度作为序列值(默认对文本排序是按照字典序),既可以将key设为长度函数len

1
2
fruits = ['fig', 'respberry', 'strawberry', 'apple', 'watermelon', 'cherry', 'banana']
print(sorted(fruits)) #默认字典序排序
['apple', 'banana', 'cherry', 'fig', 'respberry', 'strawberry', 'watermelon']
1
print(sorted(fruits, key=len)) #接受len函数作为key参数,按长短排序  
['fig', 'apple', 'cherry', 'banana', 'respberry', 'strawberry', 'watermelon']

函数式编程范式中,常用的高阶函数有map, filter, reduce, apply,在python3中移除了apply,其他几个高阶函数也都有了现代替代品。
例如,对于map, filter,可以用列表推导和生成式表达式来替代,可读性更强:

1
2
3
4
5
6
# 比较以下两组代码的可读性  
list(map(fact, range(6)))
[fact(n) for n in range(6)]

list(map(factorial, filter(lambda n: n % 2, range(6))))
[factorial(n) for n in range(6) if n % 2]
[1, 1, 4, 18, 96, 600]






[1, 1, 4, 18, 96, 600]






[1, 6, 120]






[1, 6, 120]

在python3中,reduce函数从内置韩式被移到了functools模块中,该函数常被用来求和。除了reduceany, all也是常见的规约函数。

上面的代码中,用到了lambda表达式,它常用来创建匿名函数。匿名函数是一个一次性的函数,在参数列表中比较适合使用。

可调用对象

python中的可调用对象(即可使用调用运算符()的对象)有:

  • 用户自定义函数 包括def创建的具名函数和lambda表达式。
  • 内置函数
  • 内置方法
  • 类 调用类时运行__new__创建实例然后运行__init__初始化
  • 方法 在类的定义体中定义的函数
  • 类的实例 如果类定义了__call__函数,则它的实例可以当作函数调用
  • 生成器函数 使用yield关键字的函数或方法

上面可以看到,正如一个类的实例,任何python对象如果定义了__call__函数,则其变得可调用。

1
2
3
4
5
6
7
8
class Bird():

def sing(self):
print('Balabalabala...')

def __call__(self):
return self.sing()

上面我们定义另一个鸟类,它有一个sing函数,并且我们定义了它的__call__函数,使其返回sing函数。下面我们将创建一个Bird的实例:

1
2
3
4
5
bird = Bird()
print('调用sing')
bird.sing()
print('调用Bird类的实例bird')
bird()
调用sing
Balabalabala...
调用Bird类的实例bird
Balabalabala...

这样我们就方便地创建了一个函数类对象。

函数内省

下面探讨几个将函数作为对象的相关属性。
首先__dict__属性存储了函数的用户属性,利用它我们可以知道任何对象的属性,由此,我们来关注一下函数专有而用户定义的一般对象没有的属性。
下面列出几个重要的属性:

属性 类型 说明
__annotations__ dict 参数和返回值的注解
__closure__ tuple 函数闭包
__code__ code 编译成字节码的函数元数据和函数定义体
__defaults__ tuple 形式参数的默认值
__name__ str 函数名称

为了深入了解它们我们先讨论一下python的参数机制。
python的参数处理非常灵活,调用函数时还可以传入可迭代对象*和**,前者为列表对象,后者为字典对象,举例如下:

1
2
3
4
5
6
7
8
def lunch(*food_name, cost_time=10, **lunch_attrs):
if food_name:
for i in food_name:
print(i)
if lunch_attrs:
for k, v in lunch_attrs.items():
print(k, v)
print('cost time: ', cost_time)
1
lunch('apple', 'rice', 'meat')
apple
rice
meat
cost time:  10
1
2
food_num = {'apple':1, 'rice':2, 'meat':2}
lunch(**food_num)
apple 1
rice 2
meat 2
cost time:  10
1
lunch('apple', 'rice', 'meat', **food_num)
apple
rice
meat
apple 1
rice 2
meat 2
cost time:  10

在传入的参数前加入**,则该参数的每个元素都会被作为单个参数传入,同名键会绑定到对应的具名参数上,其他的会被**attrs捕获。例如:

1
2
food_num = {'apple':1, 'rice':2, 'meat':2, 'cost_time':20}
lunch(**food_num)
apple 1
rice 2
meat 2
cost time:  20

那么在函数内省时,如何知道函数需要哪些参数呢?其中哪些参数又有默认值呢?
函数的__defaults__属性里存了定位参数和关键词参数的默认值,__kwdefaults__存储了关键词参数,参数名称在__code__属性中。

1
2
3
4
5
6
7
8
9
# 为了方便说明,重新定义lunch
def lunch(start_time=20, cost_time=10, **lunch_attrs):
for i in range(10):
print('cost time: ', cost_time)

'参数默认值', lunch.__defaults__
'参数名称', lunch.__code__
'code.co_varnames', lunch.__code__.co_varnames
'code.co_argcount', lunch.__code__.co_argcount
('参数默认值', (20, 10))






('参数名称',
 <code object lunch at 0x000001F48F958810, file "<ipython-input-57-ee16f5a3102b>", line 2>)






('code.co_varnames', ('start_time', 'cost_time', 'lunch_attrs', 'i'))






('code.co_argcount', 2)

上面可以看到函数的相关信息,其中co_varnames除了函数的参数外还包含了函数的局部变量,其他信息查看起来又不是很方便。因此我们通常用inspect模块来提取函数相关信息,这样做更加高效。详细做法如下:

1
2
3
4
5
6
from inspect import signature
sig = signature(lunch)
sig
str(sig)
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)
<Signature (start_time=20, cost_time=10, **lunch_attrs)>






'(start_time=20, cost_time=10, **lunch_attrs)'



POSITIONAL_OR_KEYWORD : start_time = 20
POSITIONAL_OR_KEYWORD : cost_time = 10
VAR_KEYWORD : lunch_attrs = <class 'inspect._empty'>

python3提供了为函数声明中的参数和返回值附件元数据的句法。例如下面的函数声明加入了声明,其不同仅在于第一行给各个参数和返回值都加了注解,注解可以是任何类型,常见的是类和字符串。
对于参数,只要在参数后面加:然后加上注解,返回值则在函数声明末尾和冒号之间加入->和注解。注解不会对函数造成任何功能影响,只是存储在函数的__annotations__属性中,我们可以通过inspect来提取。

1
2
3
4
5
6
7
8
9
10
def lunch(start_time:int=20, cost_time:'int>0'=10, **lunch_attrs) -> None:
for i in range(2):
print('cost time: ', cost_time)
lunch()
lunch.__annotations__
sig = signature(lunch)
sig.return_annotation
for param in sig.parameters.values():
note = repr(param.annotation).ljust(13)
print(note, ':', param.name, '=', para.default)
cost time:  10
cost time:  10





{'cost_time': 'int>0', 'return': None, 'start_time': int}



<class 'int'> : start_time = <class 'inspect._empty'>
'int>0'       : cost_time = <class 'inspect._empty'>
<class 'inspect._empty'> : lunch_attrs = <class 'inspect._empty'>

函数式编程

虽然python的目标并不是编程函数式编程语言,但是其中的operatorfunctools等包可以支持函数式编程范式。

下面列举一些常用的函数和例子。
reduce规约,下面展示如何用它来做阶乘:

1
2
3
from functools import reduce 
def fact(n):
return reduce(lambda a, b: a*b, range(1,n+1))

operator包

mul乘法,同样是阶乘的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
```
**itemgetter**从对象中取出元素,支持任何实现了`__getitem__`的类。例如`itemgetter(1)`和`lambda fields: fields[1]`都是返回序列索引位1上的元素。下面是排序的例子:
```py
from operator import itemgetter
sorted(data, key=itemgetter(1))
```
**attrgertter**作用和`itemgetter`类似,不过他不是通过索引位而是通过名称提取
**methodcaller**会自动创建一个函数,并在对象上调用参数指定的方法。如下例:


```python
from operator import methodcaller
s = 'Wakaka'
# 以下两种写法等价
#1
upcase = methodcaller('upper')
upcase(s)

#2
s.upper()

'#1'






'WAKAKA'






'#2'






'WAKAKA'

functools包

除了上述的reduce外,functools还有一个常用的partial函数。
该函数可以基于一个函数(我们记为A)创建一个新的可调用对象,并将A函数的某些参数固定住,例如:

1
2
3
4
5
6
7
8
9
10
11
12
def lunch(*food_name, cost_time=10, **lunch_attrs):
if food_name:
for i in food_name:
print(i)
if lunch_attrs:
for k, v in lunch_attrs.items():
print(k, v)
from functools import partial
# 固定住food_name和cost_time参数
apple_lunch = partial(lunch, 'apple', cost_time=20)
apple_lunch.args
apple_lunch.keywords
('apple',)






{'cost_time': 20}



apple
{'water': 1}

stackoverflow上有位答主给出了关于partial必要性的有趣解释

本章结尾有一则杂谈颇为有趣,摘录如下:

Python 是函数式语言吗 2000 年左右,我在美国做培训,Guido van Rossum 到访了教室(他不是讲师)。在课后的问答环节,有人问他 Python 的哪些特性是从其他语言借鉴而来的。他答 道:“Python 中一切好的特性都是从其他语言中借鉴来的。” 布朗大学的计算机科学教授 Shriram Krishnamurthi 在其论文“Teaching Programming Languages in a Post-Linnaean Age”(http://cs.brown.edu/~sk/Publications/Papers/Published/sk-teach-pl-post-linnaean/) 的开头这样写道:
编程语言“范式”已近末日,它们是旧时代的遗留物,令人厌烦。既然现代语言的 设计者对范式不屑一顾,那么我们的课程为什么要像奴隶一样对其言听计从?
在那篇论文中,下面这一段点名提到了 Python: 对 Python、Ruby 或 Perl 这些语言还要了解什么呢?它们的设计者没有耐心去精 确实现林奈层次结构;设计者按照自己的意愿从别处借鉴特性,创建出完全无视 过往概念的大杂烩。
Krishnamurthi 指出,不要试图把语言归为某一类;相反,把它们视作特性的聚合更有用。