流畅的 Python笔记.
本篇主要讲述 Python 中函数的进阶内容. 包括函数和对象的关系, 函数内省, Python 中的函数式编程.
1. 前言
本片首先介绍函数和对象的关系; 随后介绍函数和可调用对象的关系, 以及函数内省. 函数内省这部分会涉及很多与 IDE 和框架相关的东西, 如果平时并不写框架, 可以略过此部分. 最后介绍函数式编程的相关概念, 以及与之相关的两个重要模块: operator 模块和 functools 模块.
首先补充 "一等对象" 的概念."一等对象" 一般定义如下:
在运行时创建;
能赋值给变量或数据结构中的元素;
能作为参数传给函数;
能作为函数的返回结果.
从上述定义可以看出, Python 中的函数符合上述四点, 所以在 Python 中函数也被视作一等对象.
"把函数视作一等对象" 简称为 "一等函数", 但这并不是指有一类函数是 "一等函数", 在 Python 中所有函数都是一等函数!
2. 函数
2.1 函数是对象
为了表明 Python 中函数就是对象, 我们可以使用 type()函数来判断函数的类型, 并且访问函数的__doc__属性, 同时我们还将函数赋值给一个变量, 并且将函数作为参数传入另一个函数:
def factorial(n):
"""return n!"""
return 1 if n <2 else n * factorial(n - 1)
# 在 Python 控制台中, help(factorial)也会访问函数的__doc__属性.
print(factorial.__doc__)
print(type(factorial))
# 把函数赋值给一个变量
fact = factorial
print(fact)
fact(5)
# 把函数传递给另一个函数
print(list(map(fact, range(11))))
# 结果:
return n!
<class 'function'>
<function factorial at 0x000002421033C2F0>
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
从上述结果可以看出,__doc__属性保存了函数的文档字符串, 而 type()的结果说明函数其实是 function 类的一个实例. 将函数赋值给一个变量和将函数作为参数传递给另一个函数则体现了 "一等对象" 的特性.
2.2 高阶函数
接收函数作为参数, 或者把函数作为结果返回的函数叫做高阶函数(higher-order function), 上述的 map 函数就是高阶函数, 还有我们常用的 sorted 函数也是.
大家或多或少见过 map,filter 和 reduce 三个函数, 这三个就是高阶函数, 在过去很常用, 但现在它们都有了替代品:
Python3 中, map 和 filter 依然是内置函数, 但由于有了列表推导和生成器表达式, 这两个函数已不常用;
Python3 中, reduce 已不是内置函数, 它被放到了 functools 模块中. 它常被用于求和, 但现在求和最好用内置的 sum 函数.
sum 和 reduce 这样的函数叫做归约函数, 它们的思想是将某个操作连续应用到一系列数据上, 累计之前的结果, 最后得到一个值, 即将一系列元素归约成一个值.
内置的归约函数还有 all 和 any:
all(iterable): 如果 iterable 中每个值都为真, 则返回 True;all([])返回 True;
any(iterable): 如果 iterable 中有至少一个元素为真, 则返回 True;any([])返回 False.
2.3 匿名函数
lambda 关键字在 Python 表达式内创建匿名函数, 但在 Python 中, 匿名函数内不能赋值, 也不能使用 while,try 等语句. 但它和 def 语句一样, 实际创建了函数对象.
如果使用 lambda 表达式导致一段代码难以理解, 最好还是将其转换成用 def 语句定义的函数.
3. 可调用对象
函数其实一个可调用对象, 它实现了__call__方法. Python 数据模型文档列出了 7 种可调用对象:
用于定义的函数: 使用 def 语句或 lambda 表达式创建;
内置函数: 使用 C 语言 (CPython) 实现的函数, 如 len 或 time.strftime;
内置方法: 使用 C 语言实现的方法, 如 dict.get;
方法: 在类的定义体中定义的函数;
类: 调用类时 (也就是实例化一个类时) 会运行类的__new__方法创建一个实例, 然后运行__init__方法初始化实例, 最后把实例返回给调用方. 因为 Python 没有 new 运算符, 所以调用类相当于调用函数;
类的实例: 如果类实现了__call__方法, 那么它的实例可以作为函数调用;
生成器函数: 使用 yield 关键字的函数或方法. 调用生成器函数返回的是生成器对象.
3.1 用户定义的可调用类型
任何 Python 对象都可以表现得像函数, 只要实现__call__方法.
class SayHello:
def sayhello(self):
print("Hello!")
def __call__(self):
self.sayhello()
say = SayHello()
say.sayhello()
say()
print(callable(say))
# 结果:
Hello!
Hello!
True
实现__call__方法的类是创建函数类对象的简便方式. 有时这些类必须在内部维护一些状态, 让它在调用之间可用, 比如装饰器. 装饰器必须是函数, 而且有时还要在多次调用之间保存一些数据.
3.2 函数内省
以下内容在编写框架和 IDE 时用的比较多.
笔者之前偶有见到 "内省", 但一直不明白 "内省" 这个词究竟是什么意思."自我反省"? 其实在编程中, 这个词的意思就是: 让代码自动确定某一段代码能干什么. 如果以函数举例, 就是函数 A 自动确定函数 B 是什么, 包含哪些信息, 能干什么. 不过在讲 Python 函数的内省之前, 先来看看函数都有哪些属性和方法.
3.2.1 函数的属性和方法
dir 函数可以检测一个参数所含有的属性和方法. 我们可以用该函数查看一个函数所包含的属性和方法:
>>> dir(factorial)
['__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 对象共有的. 函数独有的属性如下:
>>> class C:pass
>>> obj = C()
>>> def func():pass
>>> sorted(set(dir(func)) - set(dir(obj)))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__',
'__globals__', '__kwdefaults__', '__name__', '__qualname__']
3.2.2 __dict__属性
与用户定义的常规类一样, 函数使用__dict__属性存储用户赋予它的属性. 这相当于一种基本形式的注解.
这里可能有人觉得别扭: 之前都是给变量或者对象赋予属性, 现在是给函数或者方法赋予属性. 不过正如前面说的, Python 中函数就是对象.
一般来说, 为函数赋予属性不是个常见的做法, 但 Django 框架就有这样的行为:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = "Customer name" # 给方法赋予了一个属性
3.2.3 获取关于参数的信息
从这里开始就是函数内省的内容. 在 HTTP 为框架 Bobo 中有个使用函数内省的例子, 它以装饰器的形式展示:
import bobo
@bobo.query("/")
def hello(person):
return "Hello %s!" % person
通过装饰器 bobo.query,Bobo 会内省 hello 函数: Bobo 会发现这个 hello 函数需要一个名为 person 的参数, 然后它就会从请求中获取这个参数, 并将这个参数传给 hello 函数.
有了这个装饰器, 我们就不用自己处理请求对象来获取 person 参数, Bobo 框架帮我们自动完成了.
那这究竟是怎么实现的呢? Bobo 怎么知道我们写的函数需要哪些参数? 它又是怎么知道参数有没有默认值呢?
这里用到了函数对象特有的一些属性(如果不了解参数类型, 可以阅读笔者的 "Python 学习之路 7" 中的相关内容):
__defaults__的值是一个元组, 存储着关键字参数的默认值和位置参数;
__kwdefaults__存储着命名关键字参数的默认值;
__code__属性存储参数的名称, 它的值是一个 code 对象引用, 自身也有很多属性.
下面通过一个例子说明这些属性的用途:
def func(a, b=10):
"""This is just a test"""
- c = 20
- if a> 10:
- d = 30
- else:
- e = 30
- print(func.__defaults__)
- print(func.__code__)
- print(func.__code__.co_varnames)
- print(func.__code__.co_argcount)
- # 结果:
- (10,)
- <code object func at 0x0000021651851DB0, file "mytest.py", line 1>
- ('a', 'b', 'c', 'd', 'e')
- 2
可以看出, 这种信息的组织方式并不方便:
参数名在
__code__.co_varnames
中, 它同时还存储了函数定义体中的局部变量, 因此, 只有前
__code__.co_argcount
个元素是参数名(不包含前缀为 * 何 ** 的的变长参数);
如果想将参数名和默认值对应上, 只能从后向前扫描__default__属性, 比如上例中关键字参数 b 的默认值
10
.
不过, 我们并不是第一个发现这种方式很不方便. 已经有人为我们造好了轮子.
使用 inspect 模块简化上述操作
- >>> from mytest import func
- >>> from inspect import signature
- >>> sig = signature(func) # 返回一个 inspect.Signature 对象(签名对象)
- >>> sig
- <Signature (a, b=10)>
- >>> str(sig)
- '(a, b=10)'
- >>> for name, param in sig.parameters.items():
- ... print(param.kind, ":", name, "=",param.default)
- ...
- POSITIONAL_OR_KEYWORD : a = <class 'inspect._empty'> # 表示没有默认值
- POSITIONAL_OR_KEYWORD : b = 10
inspect.Signature 对象有一个属性 parameters, 该属性是个有序映射, 把参数名和 inspect.Parameter 对象对应起来. inspect.Parameter 也有自己的属性, 如:
name: 参数的名称;
default: 参数的默认值;
kind: 参数的类型, 有 5 种,
POSITIONAL_OR_KEYWORD
,VAR_POSITIONAL(任意数量参数, 以一个 * 号开头的那种参数),VAR_KEYWORD(任意数量的关键字参数, 以 ** 开头的那种参数),KEYWORD_ONLY(命名关键字参数)和 POSITIONAL_ONLY(Python 句法不支持该类型)
annotation 和 return_annotation: 参数和返回值的注解, 后面会讲到.
inspect.Signature 对象有个 bind 方法, 它可把任意个参数绑定到 Singature 中的形参上, 框架可使用这个方法在真正调用函数前验证参数是否正确. 比如你自己写的框架中的某函数 A 自动获取用户输入的参数, 并根据这些参数调用函数 B, 但在调用 B 之前, 你想检测下这些参数是否符合函数 B 对形参的要求, 此时你就有可能用到这个 bind 方法, 看能不能将这些参数绑定到函数 B 上, 如果能, 则可认为能够根据这些参数调用函数 B:
- >>> from mytest import func
- >>> from inspect import signature
- >>> sig = signature(func)
- >>> my_tag = {"a":10, "b":20}
- >>> bound_args = sig.bind(**my_tag)
- >>> bound_args
- <BoundArguments (a=10, b=20)>
- >>> for name, value in bound_args.arguments.items():
- ... print(name, "=", value)
- a = 10
- b = 20
- >>> del my_tag["a"]
- >>> bound_args = sig.bind(**my_tag)
- Traceback (most recent call last):
- TypeError: missing a required argument: 'a'
3.2.4 函数注解
Python3 提供了一种句法, 用于为函数声明中的参数和返回值附加元数据. 如下:
- # 未加注解
- def func(a, b=10):
- return a + b
- # 添加注解
- def func(a: int, b: 'int> 0' = 10) -> int:
- return a + b
各个参数可以在冒号后面增加注解表达式, 如果有默认值, 注解放在冒号和等号之间. 上述 -> int 是对返回值添加注解的形式.
这些注解都存放在函数的__annotations__属性中, 它是一个字典:
- print(func.__annotations__)
- # 结果
- # 'return'表示返回值
- {'a': <class 'int'>, 'b': 'int> 0', 'return': <class 'int'>}
Python 只是将注解存储在函数的__annotations__属性中, 除此之外, 再无任何操作. 换句话说, 这些注解对 Python 解释器来说没有意义. 而这些注解的真正用途是提供给 IDE, 框架和装饰器等工具使用, 比如 Mypy 静态类型检测工具, 它就会根据你写的这些注解来检测传入的参数的类型是否符合要求.
inspect 模块可以获取这些注解. inspect.Signature 有个一个 return_annotation 属性, 它保存返回值的注解; inspect.Parameter 对象中的 annotation 属性保存了参数的注解.
函数内省的内容到此结束. 后面将介绍标准库中为函数式编程提供支持的常用包.
4. 函数式编程
Python 并不是一个函数式编程语言, 但通过 operator 和 functools 等包的支持, 也可以写出函数式风格的代码.
4.1 operator 模块
在函数式编程中, 经常需要把算术运算符当做函数使用, 比如非递归求阶乘, 实现如下:
- from functools import reduce
- def fact(n):
- return reduce(lambda a, b: a * b, range(1, n + 1))
operator 模块为多个算术运算符提供了对应的函数. 使用算术运算符函数可将上述代码改写如下:
- from functools import reduce
- from operator import mul
- def fact(n):
- return reduce(mul, range(1, n + 1))
operator 模块中还有一类函数, 能替代从序列中取出元素或读取对象属性的 lambda 表达式: itemgetter 和 attrgetter. 这两个函数其实会自行构建函数.
4.1.1 itemgetter()
以下代码展示了 itemgetter 的常见用途:
- from operator import itemgetter
- test_data = [
- ("A", 1, "Alpha"),
- ("B", 3, "Beta"),
- ("C", 2, "Coco"),
- ]
- # 相当于 lambda fields: fields[1]
- for temp in sorted(test_data, key=itemgetter(1)):
- print(temp)
- # 传入多个参数时, 它构建的函数返回下标对应的值构成的元组
- part_tuple = itemgetter(1, 0)
- for temp in test_data:
- print(part_tuple(temp))
- # 结果:
- ('A', 1, 'Alpha')
- ('C', 2, 'Coco')
- ('B', 3, 'Beta')
- (1, 'A')
- (3, 'B')
- (2, 'C')
itemgetter 内部使用 [] 运算符, 因此它不仅支持序列, 还支持映射和任何实现了__getitem__方法的类.
4.1.2 attrgetter()
attrgetter 和 itemgetter 作用类似, 它创建的函数根据名称提取对象的属性. 如果传入多个属性名, 它也会返回属性名对应的值构成的元组. 这里要展示的是, 如果参数名中包含句点.,attrgetter 会深入嵌套对象, 获取指定的属性:
- from collections import namedtuple
- from operator import attrgetter
- metro_data = [
- ("Tokyo", "JP", 36.933, (35.689722, 139.691667)),
- ("Delhi NCR", "IN", 21.935, (28.613889, 77.208889)),
- ("Mexico City", "MX", 20.142, (19.433333, -99.133333)),
- ]
- LatLong = namedtuple("LatLong", "lat long")
- Metropolis = namedtuple("Metropolis", "name, cc, pop, coord")
- metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for
name, cc, pop, (lat, long) in metro_data]
- # 返回新的元组, 获取 name 属性和嵌套的 coord.lat 属性
- name_lat = attrgetter("name", "coord.lat")
- for city in sorted(metro_areas, key=attrgetter("coord.lat")): # 嵌套
- print(name_lat(city))
- # 结果:
- ('Mexico City', 19.433333)
- ('Delhi NCR', 28.613889)
- ('Tokyo', 35.689722)
- 4.1.3 methodcaller()
从名字也可看出, 它创建的函数会在对象上调用参数指定的方法(注意是方法, 而不是函数).
- >>> from operator import methodcaller
- >>> s = "The time has come"
- >>> upcase = methodcaller("upper")
- >>> upcase(s) # 相当于 s.upper()
- 'THE TIME HAS COME'
- >>> hiphenate = methodcaller("replace","","-")
- >>> hiphenate(s) # 相当于 s.replace("","-")'The-time-has-come'
从 hiphenate 这个例子可以看出, methodcaller 还可以冻结某些参数, 即部分应用(partial application), 这与 functools.partial 函数的作用类似.
4.2 使用 functools.partial 冻结参数
functool 模块提供了一系列高阶函数, reduce 函数相信大家已经很熟悉了, 本节主要介绍其中两个很有用的函数 partial 和它的变体 partialmethod.
functools.partial 用到了一个 "闭包" 的概念, 这个概念的详细内容下一篇再介绍. 使用这个函数可以把接收一个或多个参数的函数改编成需要回调的 API, 这样参数更少.
- >>> from operator import mul
- >>> from functools import partial
- >>> triple = partial(mul, 3)
- >>> triple(7)
- 21
- >>> list(map(triple, range(1,10))) # 这里无法直接使用 mul 函数
- [3, 6, 9, 12, 15, 18, 21, 24, 27]
>>> triple.func # 访问原函数
- <built-in function mul>
- >>> triple.args # 访问固定参数
- (3,)
- >>> triple.keywords # 访问关键字参数
- {}
- functools.partialmethod
函数的作用于 partial 一样, 只不过 partialmethod 用于方法, partial 用于函数.
补充: 回调函数 (callback function) 可以简单理解为, 当一个函数 X 被传递给函数 A 时, 函数 X 就被称为回调函数, 函数 A 调用函数 X 的过程叫做回调.
5. 总结
本篇首先介绍了函数, 包括函数与对象的关系, 高阶函数和匿名函数, 重点是函数就是对象; 随后介绍了函数和可调用对象的关系, 以及函数的内省; 最后, 我们介绍了关于函数式编程的概念以及与之相关的两个重要模块.
来源: https://juejin.im/post/5b0d80d16fb9a00a220b15c4