1. 函数是一等对象
python从来不是一门函数式编程语言,但函数确实是一等对象,准确的说函数和其他对象一样,都是平等的.
编程语言理论家把'一等对象'定义为满足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
Python函数是对象.这里我们创建了一个函数,然后调用它,读取它的__doc__
属性,并且确定函数对象本身是function
类的实例
def factorial(n): """return n!""" return 1 if n<2 else n*factorial(n-1)
factorial(42)
1405006117752879898543142606244511569936384000000000
factorial.__doc__
'return n!'
type(factorial)
function
可以看到它和其他对象形式上是保持一致的.
有了一等函数,就可以使用函数式风格编程.函数式编程的特点之一是使用高阶函数
1.1. 高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function).内置的map,reduce,filter,sorted等都是高阶函数的代表,这边不再复述.
最为人熟知的高阶函数有map
、filter
、reduce
和 apply
.
apply
函数在Python 3
中已经移除了,因为不再需要它了.如果想使用不定量的参数调用函数,可以编写fn(*args, **keywords)
,不用再编写apply(fn, args, kwargs)
map
,filter
和reduce
这三个高阶函数还能见到,不过多数使用场景下都有更好的替代品.
1.1.1. map、filter和reduce的现代替代品
函数式语言通常会提供map
、filter
和reduce
三个高阶函数(有时使用不同的名称).在Python 3
中,map
和 filter
还是内置函数,但是由于引入了列表推导和生成器表达式,它们变得没那么重要了.列表,字典,集合推导或生成器表达式具有map
和filter
两个函数的功能,而且更易于阅读,具体的可以看数据结构预算法中相关的内容.
内置的高阶函数与可迭代对象有着很强的关联.这是python3的一项优化,通过延迟计算可以节省内存.
1.2. 匿名函数
lambda
关键字在Python表达式内创建匿名函数.
然而,Python 简单的句法限制了lambda
函数的定义体只能使用纯表达式.换句话说,lambda
函数的定义体中不能赋值,也不能使用while
和try
等 Python 语句.
在参数列表中最适合使用匿名函数.比如sorted
中就可以用lambda
表达式
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana'] sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
除了作为参数传给高阶函数之外,Python 很少使用匿名函数.由于句法上的限制,非平凡的lambda
表达式要么难以阅读,要么无法写出
如果使用lambda
表达式导致一段代码难以理解,PIL的作者"Fredrik Lundh"建议像下面这样重构
- 编写注释,说明
lambda
表达式的作用。 - 研究一会儿注释,并找出一个名称来概括注释.
- 把
lambda
表达式转换成def
语句,使用那个名称来定义函数. - 删除注释.
lambda
句法只是语法糖--与def
语句一样,lambda
表达式会创建函数对象.这是Python中几种可调用对象的一种.下一节会说明所有可调用对象
1.3. 可调用对象
除了用户定义的函数,调用运算符(即 ()
)还可以应用到其他对象上.如果想判断对象能否调用,可以使用内置的callable()
函数.Python数据模型文档列出了8
种可调用对象.
用户定义的函数
使用
def
语句或lambda
表达式创建内置函数
使用C语言(CPython)实现的函数,如
len
或time.strftime
内置方法使用 C 语言实现的方法,如dict.get
方法
在类的定义体中定义的函数
类
调用类既是创建实例
类的实例
如果类定义了
__call__
方法,那么它的实例可以作为函数调用生成器函数
使用
yield
关键字的函数或方法.调用生成器函数返回的是生成器对象协程
使用
async def
创建
callable(...)
函数可以用于判定对象是否是可调用的对象
1.3.1. 用户定义的可调用类型
不仅Python函数是真正的对象,任何Python对象都可以表现得像函数.为此只需实现实例方法__call__
.
例:BingoCage类.这个类的实例使用任何可迭代对象构建,而且会在内部存储一个随机顺序排列的列表.调用实例会取出一个元素
import random class BingoCage: def __init__(self, items): self._items = list(items) random.shuffle(self._items) def pick(self): try: return self._items.pop() except IndexError: raise LookupError('pick from empty BingoCage') def __call__(self): return self.pick()
factorial(42)
0
factorial(42)
1
factorial(42)
2
factorial(42)
3
factorial(42)
4
factorial(42)
5
1.4. 函数内省
函数对象还有很多属性.使用dir
函数可以探知函数具有的属性.这其中大多数属性是Python对象共有的.与函数对象相关的几个属性有:
__annotations__
参数和返回值的注解__call__
实现()
运算符;即可调用对象协议__closure__
函数闭包,即自由变量的绑定(通常是 None)__code__
编译成字节码的函数元数据和函数定义体__defaults__
形式参数的默认值__get__
实现只读描述符协议__globals__
函数所在模块中的全局变量__kwdefaults__
仅限关键字形式参数的默认值__name__
函数名称__qualname__
函数的限定名称,如 Random.choice
1.5. 从定位参数到仅限关键字参数
Python最好的特性之一是提供了极为灵活的参数处理机制,而且Python 3进一步提供了仅 限关键字参数(keyword-only argument
).与之密切相关的是,调用函数时使用*
和**
"展开"可迭代对象,映射到单个参数.
仅限关键字参数是Python 3新增的特性.用于指定参数只能通过关键字参数指定,而一定不会捕获未命名的定位参数. 定义函数时若想指定仅限关键字参数,要把它们放到前面有*
的参数后面.如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个*
.
factorial(42)
6
factorial(42)
7
factorial(42)
8
factorial(42)
9
1405006117752879898543142606244511569936384000000000
0
仅限关键字参数不一定要有默认值,可以像上例中 b 那样,强制必须传入实参.
要获取一个函数的参数签名,可以使用inspect
模块
inspect.signature
函数返回一个inspect.Signature
对象,它有一个parameters
属性,这是一个有序映射,把参数名和inspect.Parameter
对象对应起来.各个Parameter
属性也有自己的属性,例如name
、default
和 kind
.特殊的inspect._empty
值表示没有默认值.考虑到None
是有效的默认值(也经常这么做),而且这么做是合理的.kind
属性的值是_ParameterKind
类中的5个值之一,列举如下:
POSITIONAL_OR_KEYWORD
可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)
VAR_POSITIONAL
定位参数元组
VAR_KEYWORD
关键字参数字典
KEYWORD_ONLY
仅限关键字参数
POSITIONAL_ONLY
1405006117752879898543142606244511569936384000000000
1
除了name
、default
和 kind
,inspect.Parameter
对象还有一个annotation
属性,它的值通常是inspect._empty
,这部分与类型注释和检验有关
1405006117752879898543142606244511569936384000000000
2
1405006117752879898543142606244511569936384000000000
3
1405006117752879898543142606244511569936384000000000
4
1405006117752879898543142606244511569936384000000000
5
inspect.Signature
对象有个bind
方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样.框架可以使用这个方法在真正调用函数前验证参数
1405006117752879898543142606244511569936384000000000
6
1405006117752879898543142606244511569936384000000000
7
1405006117752879898543142606244511569936384000000000
8
1405006117752879898543142606244511569936384000000000
9
factorial.__doc__
0
factorial.__doc__
1
1.6. 支持函数式编程的包
虽然python之父 "Guido" 明确表明,Python的目标不是变成函数式编程语言,但是得益于operator
和functools
等包的支持,函数式编程风格也可以信手拈来.
1.6.1. operator模块
在函数式编程中,经常需要把算术运算符当作函数使用.例如,不使用递归计算阶乘.求和可以使用sum
函数,但是求积则没有这样的函数.我们可以使用reduce
函数,但是需要一个函数计算序列中两个元素之积.
operator
模块为多个算术运算符提供了对应的函数,从而避免编写lambda a, b: a*b
这种平凡的匿名函数.
factorial.__doc__
2
operator
模块中还有一类函数,能替代从序列中取出元素或读取对象属性的lambda
表达式,因此itemgetter
和 attrgetter
其实会自行构建函数
factorial.__doc__
3
factorial.__doc__
4
如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组:
factorial.__doc__
5
factorial.__doc__
6
itemgetter
使用[]
运算符,因此它不仅支持序列,还支持映射和任何实现__getitem__
方 法的类attrgetter
与 itemgetter
作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给attrgetter
,它也会返回提取的值构成的元组。此外,如果参数名中包含.
(点号),attrgetter
会深入嵌套对象,获取指定的属性。
我们构建一个嵌套结构,这样才能展示attrgetter
如何处理包含点号的属性名.
定义一个namedtuple
,名为metro_data
,演示使用attrgetter
处理它
factorial.__doc__
7
factorial.__doc__
8
factorial.__doc__
9
'return n!'
0
'return n!'
1
'return n!'
2
'return n!'
3
'return n!'
4
具体符号如下:
Operation | Syntax | Function |
---|---|---|
Addition | a + b | add(a, b) |
Concatenation | seq1 + seq2 | concat(seq1, seq2) |
Containment Test | obj in seq | contains(seq, obj) |
Division | a / b | truediv(a, b) |
Division | a // b | floordiv(a, b) |
Bitwise And | a & b | and_(a, b) |
Bitwise Exclusive Or | a ^ b | xor(a, b) |
Bitwise Inversion | ~ a | invert(a) |
Bitwise Or | a l b | or_(a, b) |
Exponentiation | a ** b | pow(a, b) |
Identity | a is b | is_(a, b) |
Identity | a is not b | is_not(a, b) |
Indexed Assignment | obj[k] = v | setitem(obj, k, v) |
Indexed Deletion | del obj[k] | delitem(obj, k) |
Indexing | obj[k] | getitem(obj, k) |
Left Shift | a << b | lshift(a, b) |
Modulo | a % b | mod(a, b) |
Multiplication | a * b | mul(a, b) |
Matrix Multiplication | a @ b | matmul(a, b) |
Right Shift | a >> b | rshift(a, b) |
Slice Assignment | seq[i:j] = values | setitem(seq, slice(i, j), values) |
Slice Deletion | del seq[i:j] | delitem(seq, slice(i, j)) |
Slicing | seq[i:j] | getitem(seq, slice(i, j)) |
String Formatting | s % obj | mod(s, obj) |
Subtraction | a - b | sub(a, b) |
Truth Test | obj | truth(obj) |
Ordering | a < b | lt(a, b) |
Ordering | a <= b | le(a, b) |
Equality | a == b | eq(a, b) |
Difference | a != b | ne(a, b) |
Ordering | a >= b | ge(a, b) |
Ordering | a > b | gt(a, b) |
Matrix Multiplication | a @ b | matmul(a, b) |
Negation (Arithmetic) | a | neg(a) |
Negation (Logical) | not a | not_(a) |
Positive | a | pos(a) |
1.7. 使用functools.partial冻结参数
functools.partial
这个高阶函数用于部分应用一个函数.部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定.使用这个函数可以把接受一个或多个参数的函数改编成需要回调的API
,这样参数更少. functools.partialmethod
函数的作用与partial
一样,不过是用于处理方法的.我们以partial
来作为例子
'return n!'
5
'return n!'
6
'return n!'
7
'return n!'
8
使用unicode.normalize
函数再举个例子,这个示例更有实际意义.如果处理多国语言编写的文本,在比较或排序之前可能会想使用unicode.normalize('NFC', s)
处理所有字符串s
如果经常这么做,可以定义一个nfc
函数.
'return n!'
9
type(factorial)
0
type(factorial)
1
factorial(42)
5
type(factorial)
3
factorial(42)
5
还没有评论,来说两句吧...