《Fluent Python》学习笔记:第 5 章 一等函数

在 Python 中函数(function)是一等对象(first-class objects)。编程语言理论学家把一等对象定义为满足下述条件的程序实体:

  • 运行时创建
  • 能赋值给变量或者数据结构中的元素
  • 能作为参数传递给函数
  • 能作为函数的返回结果

在 Python 中,整数、字符串和字典等都是一等对象。Python 中所有函数都是一等对象。

5.1 把函数视为对象

Python 函数是对象,可以通过以下示例证明(函数对象本身是 function 类的实例)。

def factorial(n):
    '''return n!'''
    return 1 if n < 2 else n * factorial(n-1)

print(factorial(42))
print(factorial.__doc__)  # __doc__ 用于生成对象的帮助文本。
print(type(factorial))   # factorial 是 function 类的一个实例
1405006117752879898543142606244511569936384000000000
return n!

  • 可以将函数赋值给变量,然后通过变量名调用
  • 函数可以为参数传递给高阶函数(如map函数)
fact = factorial
print(fact)
print(fact(5))
print(map(factorial, range(11))) 

"""
map(function, iterable, ...) 
以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。
"""

120

通过一等函数就可以进行函数式风格编程(programming in a functional style)。

函数式风格编程

假如要实现二叉树镜像反转
命令式编程:首先判断节点是否为空;然后翻转左树;然后翻转右树;最后左右互换。命令式编程的理论模型——图灵机的特点:一条写满数据的纸带,一条根据纸带内容运动的机器,机器每动一步都需要纸带上写着如何达到。
函数式风格编程:所谓“翻转二叉树”,可以看做是要得到一颗和原来二叉树对称的新二叉树。对一颗二叉树而言,它的镜像树就是左右节点递归镜像的树。通过描述一个 旧树->新树 的映射,而不是描述「从旧树得到新树应该怎样做」来达到目的。
函数式风格编程优点:减少代码量、增加复用(函数式的代码是“对映射的描述”,除了描述二叉树这样的数据结构,任何能在计算机中体现的东西之间的对应关系都可以描述——比如函数和函数之间的映射。

5.2 高阶函数

高阶函数(higher-order function):接受函数为参数,或者把函数作为结果返回的函数就是高阶函数。如:map、sorted 等内置函数。

函数式编程范式中,最为人熟知的是 map、filter、reduce 。map 和 filter 由于列表推导式和生成器表达式的出现变得没有那么重要了,通常列表推导式和生成器表达式更易读,也更推荐使用。

  • reduce 函数(Python 2 中是内置函数,Python 3 中被放入了 functools 模块,常用于求和)
  • sum函数,相比reduce函数,内置函数sum 在可读性和性能上做了改善
from functools import reduce
from operator import add

print(reduce(add, range(100)))
print(sum(range(100)))
4950
4950
  • all 函数(内置归约函数),用于判断给定的可迭代参数 iterable 中的所有元素是否都为 TRUE,如果是返回 True,否则返回 False。元素除了是 0、空、None、False 外都算 True。
  • any函数(内置归约函数),用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True。元素除了是 0、空、FALSE 外都算 TRUE。
def any(iterable):  # any函数等价于
    for element in iterable:
        if element:
            return True
    return False

5.3 匿名函数

Python 中匿名函数是通过关键字 lambda 创建。由于 Python 的简单句法限制了 lambda 函数的定义提只能使用纯表达式。换而言之,lambda 函数的定义体中不能赋值,也不能使用 while 和 try 等 Python 语句。

lambda 函数只是语法糖:与def语句一样,lambda 表达式会创建函数对象。

匿名函数最适合使用在参数列表中。它除了作为参数传给高阶函数之外,Python 很少使用匿名函数。匿名函数可读性差,且不好写。

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=lambda word: word[::-1]))  # 使用lambda表达式反转拼写,然后依此给单词列表排序
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

5.4 七大可调用对象

可调用对象是指该对象可以调用运算符(即())。

  • 用户自定义的函数:用 def 语句或 lambda 表达式创建。
  • 内置函数:用 C 语言实现的函数,如 len 或 time.strftime。
  • 内置方法:使用 C 语言实现的方法,如 dict.get。
  • 方法:在类的定义体中定义的函数。
  • 类:调用类时会运行类的 __new__ 方法创建一个实例,然后运行 __init__ 方法初始化实例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。
  • 类的实例:如果类定义了 __call__ 方法,那么它的实例可以当做函数调用。
  • 生成器函数:使用 yield 关键字的函数或方法,调用生成器函数返回的是生成器对象。
  • Python 中可以用内置函数 callable() 判断对象能否调用。

5.5 用户定义的可调用类型

任何 Python 对象都可以表现得像函数,只要实现实例方法 __call__。

# bingocall.py:调用 BingoCage实例,从打乱的列表中取出一个元素
import random

class BingoCage(object):

    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexErrror:
            raise LookupError('pick from empty BingoCage')

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


bingo = BingoCage(range(3))
print(bingo.pick())
print(bingo())
print(callable(bingo))
0
2
True

5.6 函数内省

可以用 dir()函数查看函数对象的属性。

# 列出常规对象没有而函数专有的属性
class C(object):
    pass


def func():
    pass

obj = C()
print(sorted(set(dir(func)) - set(dir(obj))))

5.7 从位置参数到仅限关键字参数

5.8 获取关于参数的信息

def clip(text, max_len=80):
    """在max_len前面或者后面的第一个空格处截断文本
    """
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
        if space_after >= 0:
            end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()

print(clip.__defaults__)
print(clip.__code__)
print(clip.__code__.co_varnames)
print(clip.__code__.co_argcount)
(80,)
", line 1>
('text', 'max_len', 'end', 'space_before', 'space_after')
2

 使用 inspect 模块更好的查看函数的信息

from inspect import signature

sig =signature(clip)
print(sig)
print(str(sig))
for name, param in sig.parameters.items():
    print(param.kind, ':', name, '=', param.default)
(text, max_len=80)
(text, max_len=80)
POSITIONAL_OR_KEYWORD : text = 
POSITIONAL_OR_KEYWORD : max_len = 80

5.9 函数注解

Python 3 中可以为函数声明中的参数和返回值附加元数据。函数声明中的各个参数可以在 : 之后增加注解表达式(annotation expression),如果参数有表达式,注解放在参数名和 = 之间,如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 ->和一个表达式。表达式可以是任何类型,注解中最常用的类型是类(如 str 或 int)和字符串(如 ‘int > 0’)。

注解不会做任何处理,只是存储在函数的 __annotations__ 属性(一个字典)中。Python 对注解所做的唯一事是把他们存储在函数的 __annotations__属性里。Python 不做检查、不做强制、不做验证,什么操作都不做。也就是说注解对于 Python 解释器没有任何意义。不过,注解可以供 IDE、框架和装饰器等工具使用,为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息。

# 有注解的 clip 函数
def clip(text:str, max_len:'int > 0'=80) -> str:
    """在max_len前面或者后面的第一个空格处截断文本
    """
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
        if space_after >= 0:
            end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()

print(clip.__annotations__)
{'text': , 'max_len': 'int > 0', 'return': }

5.10 支持函数式编程的包

operator 和 functools 等包的支持使得 Python 函数式编程风格也可以信手拈来。

operator 模块为多个算术运算提供了对应的函数,从而避免了编写 lambda a, b: a*b 这种匿名函数。其中的 itemgetter 和 attrgetter 函数会自行构建函数,从序列中取出元素或者读取对象属性。如:itemgetter(1) 的作用与 lambda fields: fields[1] 一样。

functools 模块提供了一系列高阶函数,如 reduce。此外还有一个非常有用的函数 partial 及其变体,partialmethod。

简单的说 functools.partial 是一个高阶函数,它的作用是把一个函数的一些参数固定,并返回一个新的函数对象。这样做的好处是,参数更少,调用更加方便。如下面这个例子:
 

# 使用 partial 把两个参数函数改编成需要单参数的函数并返回,原函数不改变。
from operator import mul
from functools import partial

triple = partial(mul, 3)  # 固定一个参数为3,并返回新的改编后的函数
print(triple(7))  # 即计算 3*7
print(list(map(triple, range(1, 10))))
print(mul(4, 5))  # 原函数不受影响
21
[3, 6, 9, 12, 15, 18, 21, 24, 27]
20

你可能感兴趣的:(python)