Python 一等函数
在 Python 中,不仅整数、字符串、字典是一等对象,连函数也被当做一等公民。这说明了什么问题,先来看看一等对象的定义:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
那么,也就意味着 Python 函数是对象,是 function
类的实例。这篇文章从函数的属性、参数等几个方面拆解并分析 Python 函数。
Python 的可调用对象
首先看看 Python 中可直接调用的对象。我们知道,调用函数的方法是用调用运算符 ()
,那么判断一个对象是否可调用,就是判断 ()
能否应用到该对象上,可以使用内置的 callable()
函数。
Python 数据模型文档列出了 7 种可调用对象:用户定义的函数、内置函数、内置方法、方法、类、类的实例、生成器函数。
上述 7 种对象中,函数天生就是可以调用的,方法是类中定义的函数,而创建类的实例就是直接调用了类(其实是运行类的 __new__
方法,然后运行 __init__
方法初始化实例)。比较难理解的是类的实例,类的实例是通常意义上的对象,对象可调用的前提是类中定义了 __call__
方法。看个
吧:
代码语言:javascript复制class Father:
def __init__(self, age):
self.sex = "male"
self.age = age
self.money = 0
def own_money(self, money):
self.money = money
if __name__ == "__main__":
f = Father(30)
f.own_money(100)
print("Father's money: {}"
.format(f()))
运行结果:
代码语言:javascript复制Traceback (most recent call last):
File "/Users/zww/my_demo/test.py", line 15, in <module>
.format(f()))
TypeError: 'Father' object is not callable
提示 Father
object
不可被调用。在 Father
类中定义下面这个方法:
def __call__(self, *args, **kwargs):
return self.money
运行结果:
代码语言:javascript复制Father's money: 100
所以,只要定义了方法 __call__
,任何 Python 对象都可以表现得像函数。
函数的属性
我们可以通过 dir
函数来探知 Python 函数具有哪些属性:
In [1]: def fun():
...: pass
...:
In [2]: dir(fun)
Out[2]:
['__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__doc__',
'__format__',
'__get__',
'__getattribute__',
'__globals__',
'__hash__',
'__init__',
'__module__',
'__name__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'func_closure',
'func_code',
'func_defaults',
'func_dict',
'func_doc',
'func_globals',
'func_name']
其中,大部分属性是 Python 对象共有的,但这里我们主要讨论几种类的实例没有,但函数有的属性:
名称 | 类型 | 说明 |
---|---|---|
__annotations__ | dict | 参数和返回值的注解 |
__call__ | method-wrapper | 实现 () 运算符,即可调用对象协议 |
__closure__ | tuple | 函数闭包,即自由变量的绑定(通常是 None) |
__code__ | code | 编译成字节码的函数元数据和函数定义体 |
__defaults__ | tuple | 形式参数的默认值 |
__get__ | method-wrapper | 实现只读描述符协议 |
__globals__ | dict | 函数所在模块中的全局变量 |
__kwdefaults__ | dict | 仅限关键字形式参数的默认值 |
__name__ | str | 函数名称 |
__qualname__ | str | 函数的限定名称,如 Random.choice |
函数的参数
Python 比较强大的特性之一是提供了非常灵活的参数处理机制。而 Python3 进一步提供了仅限关键字参数。
仅限关键字参数
在 Python 中调用函数时使用 *
和 **
“展开” 可迭代对象,映射到单个参数。 *
表示将调用时的多个参数放入元组中,而 **
表示将关键字参数放入一个字典中。
用
说话:
代码语言:javascript复制def father_own_money(name, *args, **kwargs):
father_name = name
default_money = args
owned_money = kwargs["money"]
return father_name, default_money, owned_money
if __name__ == "__main__":
name = "MaYun"
result = father_own_money(name, 1, 2, money=100)
father_name = result[0]
default_money = result[1]
owned_money = result[2]
print("Father {} already has money {}.n"
"Then he owned money {}.".format
(father_name, default_money, owned_money))
运行结果:
代码语言:javascript复制Father MaYun already has money (1, 2).
Then he owned money 100.
上面
中的函数还可以这样调用:
代码语言:javascript复制name = "MaYun"
arrs = {"money": 100}
result = father_own_money(name, 1, 2, **arrs)
当想指定仅限关键字参数时,可以将指定的关键字参数放在 *
后面,比如:
def father_own_money(name, *, money):
father_name = name
owned_money = money
return father_name, owned_money
if __name__ == "__main__":
name = "MaYun"
result = father_own_money(name, money=100)
father_name = result[0]
owned_money = result[1]
print("Father {} owned money {}.".
format(father_name, owned_money))
运行结果:
代码语言:javascript复制Father MaYun owned money 100.
函数参数的信息
接下来我们看看函数参数的相关信息,来更好的理解函数。
结合函数的属性一节,我们知道函数的 __defaults__
属性存放着函数形参的默认值而 __kwdefaults__
属性则存放的是仅限关键字参数的默认值; __name__
属性存放函数的名称; __code__
属性存放的是编译成字节码的函数元数据和函数定义体,(好吧看不懂),没关系,只需要知道 __code__
是一个 code
对象引用,和自身的两个属性: co_varnames
和 co_argcount
就足够装 B 了。
结合
做简单说明:
代码语言:javascript复制def father_own_money(name="MaYun", *, money=100):
father_name = name
owned_money = money
return father_name, owned_money
if __name__ == "__main__":
print("Function's name: nt{}".
format(father_own_money.__name__))
print("Function keywords defaults: nt{}".
format(father_own_money.__kwdefaults__))
print("Function formal params defaults: nt{}".
format(father_own_money.__defaults__))
print("Function __code__ property: nt{}".
format(father_own_money.__code__))
print("Function all params name: nt{}".
format(father_own_money.__code__.co_varnames))
print("Function formal params num: nt{}".
format(father_own_money.__code__.co_argcount))
先看运行结果:
代码语言:javascript复制Function's name:
father_own_money
Function keywords defaults:
{'money': 100}
Function formal params defaults:
('MaYun',)
Function __code__ property:
<code object father_own_money at 0x101f618a0, file "xx.py", line 14>
Function all params name:
('name', 'money', 'father_name', 'owned_money')
Function formal params num:
1
结合运行结果可以看出 __code__.co_varnames
记录的是该函数的所有参数,包括传参和函数内定义的局部变量;而 __code__.co_argcount
记录的是形式参数的个数,可以看出不包含关键字参数。更多关于函数参数的分析可以引用 Python 的 inspect
模块来提取函数的前面,感兴趣可以研究研究,这里不做详解。
函数注解
Python3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据,就是函数注解,目的是更方便的进行文档编写、参数检查等。这是个可选项,入参的注解在参数后加个 :
即可,而返回值的注解需要在 )
和 :
直接添加 ->
和一个表达式。上面 father_own_money
添加注解如下:
def father_own_money(name: str ="MaYun", *, money: "default = 100"=100) -> ():
father_name = name
owned_money = money
return father_name, owned_money
需要说明的是,函数注解仅仅在函数的 __annotations__
属性中做了存储,除此之外 Python 不做检查、不做验证、不做强制,没有任何处理。换句话说,注解只是元数据,可以供 IDE 、框架和装饰器等工具使用。
高阶函数和匿名函数
了解了 Python 函数的以上特性之后,我们就可以利用一等函数的特性实现函数式风格编程了。
高阶函数
函数式风格编程的特点之一就是高阶函数,那么什么是高阶函数呢?既然 Python 函数是一等公民,那么 TA 既可以作为函数参数传入,也可以作为结果返回。而接受函数为参数,或者把函数作为结果返回的函数,我们称之为高阶函数。Python 中最为人熟知的高阶函数有: map()
、 reduce()
、 sorted()
、 filter()
。
map(func,
*iterables)
--> map object
map()
函数的传参:函数和一系列可迭代对象;返回一个 map
对象;函数的操作是将第二个参数(可迭代对象)的每个元素代入第一个参数(函数)中,得到的结果作为函数返回值。
如下:
代码语言:javascript复制def father_own_money(name: str = "MaYun", money=100):
father_name = name
owned_money = money
return father_name, owned_money
if __name__ == "__main__":
name = ["mayun"] * 10
money = range(0, 10)
results = map(father_own_money, name, money)
print(list(results))
运行结果:
代码语言:javascript复制[('mayun', 0), ('mayun', 1), ('mayun', 2), ('mayun', 3), ('mayun', 4), ('mayun', 5), ('mayun', 6), ('mayun', 7), ('mayun', 8), ('mayun', 9)]
需要说明的是, map()
传参中,可迭代对象有几个,取决于第一个函数有多少参数,用上面的
说就是 name
和 money
分别作为 father_own_money
的第一和第二个参数,可迭代数量按最少的算。由于返回值 results
是个 map
对象,需要 list
一下,才能得到所有的返回结果。
reduce(function, sequence)
-> value
和 map()
不同的是, reduce()
的传参 function 必须有两个参数;返回值就是 function 的返回值类型;函数的操作是:用 function 先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,直到计算完所有元素,最后得到一个结果。
def father_money_sum(default_money: int, owned_money: int):
return default_money owned_money
if __name__ == "__main__":
money = range(0, 101)
from functools import reduce
results = reduce(father_money_sum, money)
print("Father's all money: {}".format(results))
运行结果:
代码语言:javascript复制Father's all money: 5050
需要注意的是, reduce
在 Python3 中从全局名字空间中被移除了,需要先引入。
sorted(*args,
**kwargs)
sorted
函数本身只接受一个列表来排序,但因为其可选参数 key
接受函数来应用到列表的各个元素上进行排序,所以摇身一变成了高阶函数(2333333)。看个
:
代码语言:javascript复制def father_money_sum(default_money: int, owned_money: int = 90):
if default_money > 150:
return owned_money
return default_money owned_money
if __name__ == "__main__":
father_money = [100, 200, 201, 105, 189]
result = sorted(father_money, key=father_money_sum)
print(result)
运行结果:
代码语言:javascript复制[200, 201, 189, 100, 105]
由运行结果可以看出, sorted
函数仅仅将 father_money
这个列表中的每个值代入到 father_money_sum
函数中,针对结果对原列表进行排序,并没有改变列表中的值。
filter(function
or
None, iterable)
--> filter object
filter()
函数的操作是将 iterable
中的元素分别代入 function
中,将结果为 True 的返回。
def father_money_sum(default_money: int, owned_money: int = 90):
if default_money > 150:
return None
return default_money owned_money
if __name__ == "__main__":
father_money = [100, 200, 201, 105, 189]
result = filter(father_money_sum, father_money)
print(list(result))
匿名函数
为了更好的使用高阶函数,有时候创建一些小型的函数更为便利,这时候匿名函数的作用就体现出来了。
匿名函数主要是用关键字 lambda
,语法为:
lambda x: x 1
上面这个
用数学表达式可以写成: f(x)
= x
1
,这样理解就方便多了, lambda
相当于数学函数中的 f
, :
后的表达式的计算结果为匿名函数的返回值。这种简单的语法也意味着 lambda
函数只支持纯表达式,不能赋值,不能用 while
try
等 Python 语句。
当然, lambda
表达式也支持多参数,比如:
lambda x, y: x y
用数学表达式写成: f(x, y)
= x y
。
lambda
句法只是语法糖:与 def
语句一样, lambda
表达式会创建函数对象。