2024 Python3.10 系统入门+进阶(十三):函数进阶

一、匿名函数(lambda)

1.1 定义匿名函数及其简单运用

匿名函数就是没有名字的函数,不使用 def 语句来定义,使用 lambda 运算符定义,也称为函数表达式。在 Python 中,使用 lambda 表达式创建匿名函数,其语法格式如下:

fn = lambda [arg1 [,arg2,……,argn]]:expression

参数说明:

  1. [arg1 [,arg2,……,argn]] 可选参数,表示匿名函数的参数,参数个数不限,参数之间通过 , 分隔。
  2. expression:必选参数,用于指定一个实现具体功能的表达式。如果有参数,那么在该表达式中可以访问这些参数,即访问冒号左侧的参数
  3. fn: 表示一个变量,用来接收 lambda 表达式的返回值,返回值为一个匿名函数对象,通过 fn 变量可以调用该匿名函数

lambda 是一个表达式,而不是一个语句块,它具有如下几个特点:

  1. 与 def 语句的语法相比较,lambda 表达式不需要小括号,冒号(:) 左侧的值列表表示函数的参数,函数不需要 return 语句,冒号右侧表达式的运算结果就是返回值
  2. 与 def 语句的功能相比较,lambda 的结构单一,功能有限。lambda 的主体是一个表达式,而不是一个代码块,因此不能包含各种命令,如 for、while 等结构化语句,仅能够在 lambda 表达式中封装有限的运算逻辑
  3. lambda 表达式也会产生一个新的局部作用域,拥有独立的命名空间。在 def 定义的函数中嵌套 lambda,lambda 表达式可以访问外层 def 定义的函数中可用的变量。
  4. 在为高阶函数传参时,使用 lambda 表达式,往往能简化代码

示例:

# 定义一个无参匿名函数,直接返回一个固定值
print((lambda: 'amo')())
# 定义一个带参数的匿名函数,用来求两个数字的和
"""
def add(x, y):
    return x + y
"""
add = lambda x, y: x + y
print(add(2, 3))  # 输出: 5
# 判断一个数是奇数还是偶数
is_even = lambda x: "Even" if x % 2 == 0 else "Odd"
print(is_even(4))  # 输出: Even
print(is_even(5))  # 输出: Odd
# 在匿名函数中设置默认值
c = lambda x, y=2: x + y
print(c(10))
print(c(10, 20))
# 快速转换为字典对象
c = lambda **kwargs: kwargs
d = c(a=1, b=2, c=3)
print(d)


# 嵌套lambda函数
# 创建一个函数,返回某个数的n次方
def make_power(n):
    return lambda x: x ** n


square = make_power(2)
cube = make_power(3)

print(square(5))  # 输出: 25
print(cube(2))  # 输出: 8
# keyword-only参数
print((lambda x, *, y=30: x + y)(5))
print((lambda x, *, y=30: x + y)(5, y=10))
# 可变参数
print((lambda *args: (x for x in args))(*range(5)))
print((lambda *args: [x + 1 for x in args])(*range(5)))
print((lambda *args: {x % 2 for x in args})(*range(5)))

from collections import defaultdict

# 需求,构建一个字典,所有key对应的值是一个列表,创建新的kv对的值也是空列表
d = {c: [] for c in 'abcde'}
d['a'].append(10)  # {'a': [10], 'b': [], 'c': [], 'd': [], 'e': []}
print(d)
d['f'] = []  # 新增key,value是空列表
d['f'].append(20)
print(d)  # {'a': [10], 'b': [], 'c': [], 'd': [], 'e': [], 'f': [20]}

# defaultdict
d = defaultdict(list)  # lambda : list()
d['a'].append('10')  # d['a'] = list()
d['f'].append('20')
print(d)
# sorted
x = ['a', 1, 'b', 20, 'c', 32]
print(sorted(x, key=str))
# 如果按照数字排序怎么做?
x = ['a', 1, 'b', 20, 'c', 32]
print(sorted(x, key=lambda x: x if isinstance(x, int) else int(x, 16)))

1.2 collections模块之defaultdict详解(拓展)

defaultdict 的工作原理: defaultdict 是 dict 的一个子类,区别在于它会自动为不存在的键提供一个默认值。这个默认值是通过传递给 defaultdict 的工厂函数生成的。当访问字典中不存在的键时,defaultdict 会调用工厂函数来创建一个默认值,并将其插入到字典中。defaultdict 的语法:

from collections import defaultdict

defaultdict(default_factory)
default_factory: 是一个返回默认值的函数。如果访问字典时该键不存在,defaultdict会使用default_factory生成的默认值。
常见的 default_factory 函数包括: 
int: 用于初始化计数任务(默认值为 0)。
list: 用于存储列表(默认值为空列表 [])。
set: 用于存储集合(默认值为空集合 set())。

【示例1】使用int作为default_factory。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-06 15:42
# @Author  : AmoXiang
# @File: defaultdict_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

from collections import defaultdict

# 初始化一个默认值为 0 的 defaultdict
count_dict = defaultdict(int)

# 假设我们在统计字符的出现次数
string = "hello world"
for char in string:
    count_dict[char] += 1

print(count_dict)
print(type(defaultdict))
for k, v in count_dict.items():
    print(k, v)

【示例2】使用list作为default_factory。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-06 15:42
# @Author  : AmoXiang
# @File: defaultdict_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

from collections import defaultdict

# 初始化一个默认值为空列表的 defaultdict
list_dict = defaultdict(list)

# 假设我们要根据名字的首字母将名字分类
names = ["Alice", "Aria", "Bob", "Barbara", "Charlie"]
for name in names:
    list_dict[name[0]].append(name)

print(list_dict)

【示例3】使用set作为default_factory。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-06 15:42
# @Author  : AmoXiang
# @File: defaultdict_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

from collections import defaultdict

# 初始化一个默认值为空集合的 defaultdict
set_dict = defaultdict(set)

# 假设我们要根据课程分类学生的选课情况
courses = [("Math", "Alice"), ("Math", "Bob"), ("English", "Alice"),
           ("English", "Charlie"), ("Math", "Charlie")]
for course, student in courses:
    set_dict[course].add(student)

print(set_dict)

【示例4】使用 lambda 创建复杂的默认值。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-06 15:42
# @Author  : AmoXiang
# @File: defaultdict_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

from collections import defaultdict

# 使用 lambda 表达式设置默认值为嵌套字典
nested_dict = defaultdict(lambda: defaultdict(int))
print(nested_dict)

# 假设我们要统计每个学生在每门课中的分数
grades = [("Alice", "Math", 95), ("Alice", "English", 88),
          ("Bob", "Math", 75), ("Bob", "English", 80),
          ("Charlie", "Math", 85)]

for student, subject, score in grades:
    nested_dict[student][subject] = score

print(nested_dict)

defaultdict 与普通字典的区别:

  1. 普通字典:如果你尝试访问一个不存在的键,会引发 KeyError 错误。
  2. defaultdict:如果你尝试访问一个不存在的键,会自动为该键创建一个默认值。

示例:

from collections import defaultdict

# 普通字典的行为
regular_dict = {}
# print(regular_dict['key'])  # 会抛出 KeyError: 'key'

# defaultdict 的行为
default_dict = defaultdict(int)
print(default_dict['key'])  # 输出: 0

如果在创建 defaultdict 后需要修改它的 default_factory,你可以直接通过赋值的方式来改变其默认行为。

from collections import defaultdict

# 创建一个默认值为 int 的 defaultdict
mod_dict = defaultdict(int)
print(mod_dict['a'])  # 输出: 0

# 修改 default_factory 为 list
mod_dict.default_factory = list
print(mod_dict['b'])  # 输出: []

# 修改 default_factory 为 None,阻止自动创建默认值
mod_dict.default_factory = None
print(mod_dict['c'])  # 会抛出 KeyError: 'c'

在修改 default_factory 为 None 后,defaultdict 不会再自动创建新的默认值,并会像普通字典一样抛出 KeyError。

二、生成器函数

2.1 生成器函数

Python中有 2 种方式构造生成器对象:

  1. 生成器表达式。 生成器表达式与列表推导式类似,但它们使用小括号而不是方括号,并且返回的是生成器对象而不是整个列表。
  2. 生成器函数。 函数体代码中包含 yield 语句的函数。与普通函数调用不同,生成器函数调用返回的是生成器对象。

示例:

m = (i for i in range(5))
print(type(m))  # <class 'generator'>
print(next(m))  # 0
print(next(m))  # 1


def inc():
    for i in range(5):
        yield i


print(type(inc))  # <class 'function'>
print(type(inc()))  # 生成器函数一定要调用,返回生成器对象 <class 'generator'>
g = inc()  # 返回新的生成器对象
print(next(g))  # 0
print('-------------------')
for x in g:
    print(x)
print('-------------------')
for x in g:  # 还能迭代出元素吗?不能
    print(x)

普通函数调用,函数会立即执行直到执行完毕。生成器函数调用,并不会立即执行函数体,而是返回一个生成器对象,需要使用 next 函数来驱动这个生成器对象,或者使用循环来驱动。生成器表达式和生成器函数都可以得到生成器对象,只不过生成器函数可以写更加复杂的逻辑。执行过程:

def gen():
    print(1)
    yield 2
    print(3)
    yield 4
    print(5)
    return 6
    yield 7


print(next(gen()))  # 看到什么
print('--------------------------')
print(next(gen()))  # 看到什么
g = gen()
print(next(g))
print('--------------------------')
print(next(g))
print('--------------------------')
# print(next(g))  # return的值可以拿到吗?StopIteration: 6
print('--------------------------')
print(next(g, 'End'))  # 没有元素不想抛异常,给个缺省值

在生成器函数中,可以多次 yield,每执行一次 yield 后会暂停执行,把 yield 表达式的值返回,再次执行会执行到下一个 yield 语句又会暂停执行,return 语句依然可以终止函数运行,但 return 语句的返回值不能被获取到,return 会导致当前函数返回,无法继续执行,也无法继续获取下一个值,抛出 StopIteration 异常,如果函数没有显式的 return 语句且生成器函数执行到结尾(相当于执行了 return None),一样会抛出 StopIteration 异常。生成器函数小结:

  1. 包含 yield 语句的生成器函数调用后,生成生成器对象的时候,生成器函数的函数体不会立即执行
  2. next(generator) 会从函数的当前位置向后执行到之后碰到的第一个 yield 语句,会弹出值,并暂停函数执行
  3. 再次调用 next 函数,和上一条一样的处理过程
  4. 继续调用 next 函数,生成器函数如果结束执行了(显式或隐式调用了 return 语句),会抛出 StopIteration 异常

【示例1】无限循环。

def counter():
    i = 0
    while True:
        i += 1
        yield i


c = counter()
print(next(c))  # 打印什么 1
print(next(c))  # 打印什么 2
print(next(c))  # 打印什么 3

【示例2】计数器。

def inc():
    def counter():
        i = 0
        while True:
            i += 1
            yield i

    c = counter()
    return next(c)


print(inc())  # 打印什么? 1
print(inc())  # 打印什么? 1
print(inc())  # 打印什么?为什么? 1 如何修改成为1,2,3的效果


# 修改代码
def inc():
    def counter():
        i = 0
        while True:
            i += 1
            yield i

    c = counter()

    def inner():
        return next(c)

    return inner  # return lambda : next(c)


foo = inc()
print(foo())  # 打印什么? 1
print(foo())  # 打印什么? 2 
print(foo())  # 打印什么?为什么? 3

【示例3】斐波那契数列。

def fib():
    a = 0
    b = 1
    while True:
        yield b
        a, b = b, a + b


f = fib()
for i in range(1, 102):
    print(i, next(f))

2.2 生成器结合关键字in使用时注意事项

在 Python 中,生成器表达式可以结合 in 关键字用于查找元素。需要注意的是,生成器表达式是惰性求值的,这意味着它不会一次性计算所有元素,而是根据需要逐个生成值。因此,使用 in 关键字时需要注意以下几点:

  1. 一次性消耗生成器:生成器表达式只能被遍历一次。生成器是一次性对象。一旦遍历(或部分遍历),其中的元素就被消耗掉,无法再被重新访问。结合 in 关键字时,如果某个元素被检查过,该生成器的迭代就已经开始了。
    gen = (x for x in range(5))
    lis = [x for x in range(5)]
    
    # 第一次检查 2 是否在生成器中
    print(2 in gen)  # 输出: True
    print(2 in lis)  # 输出: True
    
    # 再次检查 2,会得到 False 因为生成器已经被部分消耗了
    print(2 in gen)  # 输出: False
    print(2 in lis)  # 输出: True
    
  2. 查找元素时会遍历到找到的元素为止。当使用 in 检查生成器表达式中是否包含某个值时,生成器会从头开始遍历,直到找到该值或到达生成器的末尾。
    gen = (x for x in range(10))
    
    # 查找 5,生成器会逐个产生值,直到找到 5
    print(5 in gen)  # 输出: True
    
    # 查找 7,生成器从 6 开始继续生成,因为 5 之前的值已经被消耗
    print(7 in gen)  # 输出: True
    
  3. 提前终止生成器的遍历。in 操作会在找到目标值时立即终止生成器的遍历,这对于长序列来说是非常高效的。与完整迭代所有元素的列表相比,生成器只会生成它需要的值。
    gen = (x for x in range(1000000))
    
    # 只需检查到 5,生成器不需要遍历全部 100 万个元素
    print(5 in gen)  # 输出: True
    
  4. 避免多次查找同一元素。因为生成器只能遍历一次,所以多次使用 in 查找同一个元素时需要注意,之前的部分已经被消耗。因此,如果需要多次查找某个元素,应该提前将生成器的值存储为可重复使用的数据结构(如列表或集合)。
    gen = (x for x in range(10))
    gen_list = list(gen)  # 将生成器的结果保存到列表
    
    # 可以多次使用 'in' 进行查找
    print(5 in gen_list)  # 输出: True
    print(5 in gen_list)  # 输出: True
    

总结:

  1. 生成器是一次性对象:结合 in 使用时,生成器会从头开始逐个生成值,一旦某个值被生成,该部分生成器内容就无法再次访问。
  2. in 操作是惰性的:生成器不会一次性生成所有值,而是逐个生成,直到找到目标值。
  3. in 操作可以提前终止遍历:查找到目标值时,生成器会停止生成,节省资源。
  4. 生成器遍历后不可复用:如果需要多次查找,考虑将生成器转换为列表或其他可重复访问的数据结构。

2.3 yield from语法

yield from 是 Python 3.3 中引入的一种语法,用于简化在生成器函数中迭代其他生成器或可迭代对象的过程。它可以将子生成器(或其他可迭代对象)中的所有值一个接一个地 "委托" 给父生成器。这使得代码更加简洁,而不需要显式地在循环中手动 yield 子生成器中的每个值。基本语法:

yield from <iterable>
# ① <iterable> 可以是任何可迭代对象,例如列表、元组、生成器等。
# ② yield from 会自动迭代 <iterable>,将其中的每一个值依次生成,并传递给外部的调用者。

使用 yield from 的优点:

  1. 简化嵌套生成器的代码逻辑。
  2. 避免手动使用循环 for x in iterable: yield x 的重复代码。
  3. 更好地处理生成器的返回值,尤其是协程中。

【示例1】简化嵌套生成器的代码。

def generator1():
    yield 1
    yield 2


def generator2():
    for value in generator1():
        yield value
    yield 3


# 使用生成器
gen = generator2()
print(list(gen))  # 输出: [1, 2, 3]


# 使用 yield from 改造 generator2
def generator2():
    yield from generator1()
    yield 3


# 使用生成器
gen = generator2()
print(list(gen))  # 输出: [1, 2, 3]

【示例2】将多个可迭代对象合并为一个生成器。

def generator():
    yield from [1, 2, 3]
    yield from (x ** 2 for x in range(3))  # 生成器表达式
    yield from "abc"


# 使用生成器
gen = generator()
print(list(gen))  # 输出: [1, 2, 3, 0, 1, 4, 'a', 'b', 'c']

yield from 与生成器的返回值:在 Python 3.3 中,生成器被允许通过 return 返回一个值。yield from 允许外层生成器不仅能获取内层生成器的值,还能接收内层生成器的返回值。示例:

def sub_generator():
    yield 1
    yield 2
    return "Subgenerator done!"


def main_generator():
    result = yield from sub_generator()  # 获取返回值
    print(result)
    yield 3


# 使用生成器
gen = main_generator()
print(list(gen))  # 输出: [1, 2, Subgenerator done!, 3]

三、函数执行原理

C 语言中,函数的活动和栈有关。栈是后进先出的数据结构,栈是由底端向顶端生长,栈顶加入数据称为压栈、入栈,栈顶弹出数据称为出栈。

def add(x, y):
    r = x + y
    print(r)
    return r


def main():
    a = 1
    b = add(a, 2)
    return b


main()

简单说明:

  1. main 调用,在栈顶创建栈帧
  2. a = 1,在 main 栈帧中增加 a,堆里增加 1,a 指向这个 1
  3. b = add(a, 2),等式右边先执行,add 函数调用
  4. add 调用,在栈顶创建栈帧,压在 main 栈帧上面
  5. add 栈帧中增加 2 个变量,x 变量指向 1,y 指向堆中新的对象 2
  6. 在堆中保存计算结果 3,并在 add 栈帧中增加 r 指向 3
  7. print 函数创建栈帧,实参 r 被压入 print 的栈帧中
  8. print 函数执行完毕,函数返回,移除栈帧
  9. add 函数返回,移除栈帧
  10. main 栈帧中增加 b 指向 add 函数的返回值对象
  11. main 函数返回,移除栈帧

问题:如果再次调用 main 函数,和刚才的 main 函数调用,有什么关系?每一次函数调用都会创建一个独立的栈帧入栈。因此,可以得到这样一句不准确的话:哪怕是同一个函数两次调用,每一次调用都是独立的,这两次调用没什么关系。

四、递归函数

在这里插入图片描述
函数直接或者间接调用自身就是 递归,递归需要有边界条件、递归前进段、递归返回段递归一定要有边界条件,当边界条件不满足的时候,递归前进,当边界条件满足的时候,递归返回。

斐波那契数列 Fibonacci number:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …,如果设 F(n)为该数列的第 n 项(n∈N*),那么这句话可以写成如下形式:F(n)=F(n-1)+F(n-2),有 F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2) ,循环实现:

# 循环实现
def fib_v1(n):  # n>=3
    a = b = 1
    for i in range(n - 2):
        a, b = b, a + b
    return b


print(fib_v1(101))
print(fib_v1(35))

使用递归实现,需要使用上面的递推公式:

# 递归
def fib_v2(n):
    if n < 3:
        return 1
    return fib_v2(n - 1) + fib_v2(n - 2)


# 递归
def fib_v2(n):
    return 1 if n < 3 else fib_v2(n - 1) + fib_v2(n - 2)


print(fib_v2(35))  # 9227465

递归实现很美,但是执行 fib(35) 就已经非常慢了,为什么?以 fib(5) 为例。看了下图后,fib(6) 是怎样计算的呢?
在这里插入图片描述
这个函数进行了大量的重复计算,所以慢。递归要求: 递归一定要有退出条件,递归调用一定要执行到这个退出条件。没有退出条件的递归调用,就是无限调用,递归调用的深度不宜过深,Python 对递归调用的深度做了限制,以保护解释器。超过递归深度限制,抛出 RecursionError: maximum recursion depth exceeded 超出最大深度

import sys

print(sys.getrecursionlimit())  # 1000

# ipython中为3000
In [5]: import sys

In [6]: sys.getrecursionlimit()
Out[6]: 3000

使用时间差或者 %%timeit 来测试一下这两个版本斐波那契数列的效率。很明显循环版效率高。难道递归函数实现,就意味着效率低吗?能否改进一下 fib_v2 函数?

# 递归
def fib_v3(n, a=1, b=1):
    if n < 3:
        return b
    a, b = b, a + b
    # print(n, a, b)
    return fib_v3(n - 1, a, b)  # 函数调用次数就成了循环次数,将上次的计算结果代入下次函数调用


fib_v3(101)  # fib_v3(35)
print(fib_v3(101))
print(fib_v3(35))
# 提示:用fib_v3(3)代入思考递归后计算了几次

思考时,也比较简单,思考 fib_v3(3) 来编写递归版本代码。经过比较,发现 fib_v3 性能不错,和 fib_v1 循环版接近。但是递归函数有深度限制,函数调用开销较大。间接递归:
在这里插入图片描述
示例代码:

def foo1():
    foo2()


def foo2():
    foo1()


foo1()

间接递归调用,是函数通过别的函数调用了自己,这一样是递归。只要是递归调用,不管是直接还是间接,都要注意边界返回问题。但是间接递归调用有时候是非常不明显,代码调用复杂时,很难发现出现了递归调用,这是非常危险的。所以,使用良好的代码规范来避免这种递归的发生。

总结:

  1. 递归是一种很自然的表达,符合逻辑思维
  2. 递归相对运行效率低,每一次调用函数都要开辟栈帧
  3. 递归有深度限制,如果递归层次太深,函数连续压栈,栈内存很快就溢出了
  4. 如果是有限次数的递归,可以使用递归调用,或者使用循环代替,循环代码稍微复杂一些,但是只要不是死循环,可以多次迭代直至算出结果,绝大多数递归,都可以使用循环实现
  5. 即使递归代码很简洁,但是能不用则不用递归

尾递归: 尾递归是递归的一种优化算法,它从最后开始计算,每递归一次就算出相应的结果,并把当前的运算结果(或路径)放在参数里传给下一层函数。在递归的尾部,立即返回最后的结果,不需要递归返回,因此不用缓存中间调用对象。下面是阶乘的一种普通线性递归运算:

def f(n):
    return 1 if (n == 1) else n * f(n - 1)


print(f(5))  # 120

使用尾递归算法后,则可以使用如下方法:

def f(n, a):
    return a if (n == 1) else f(n - 1, a * n)


print(f(5, 1))  # 120

当 n = 5 时,线性递归的递归过程如下:

f(5) = {5 * f(4)}
	 = {5 * {4 * f(3)}}
	 = {5 * {4 * {3 * f(2)}}}
	 = {5 * {4 * {3 * {2 * f(1)}}}}
	 = {5 * {4 * {3 * {2 * 1}}}}
	 = {5 * {4 * {3 * 2}}}
	 = {5 * {4 * 6}}
	 = {5 * 24}
	 = 120

而尾递归的递归过程如下:

f(5) = f(5,1)=f(4,5)=f(3,20)=f(2, 60)=f(1,120)=120

很容易看出,普通递归比尾递归更加消耗资源,每次重复的过程调用都使得调用链条不断加长,使系统不得不使用栈进行数据保存和恢复,而尾递归就不存在这样的问题,因为它的状态完全由变量 n 和 a 保存。

从理论上分析,尾递归也是递归的一种类型,不过它的算法具有迭代算法的特征,上面的阶乘尾递归可以改写为下面的迭代循环:

n = 5
w = 1
for i in range(1, n + 1):
    w = w * i
print(w)

五、高阶函数柯里化

5.1 函数合成

高阶函数是函数式编程最显著的特征,其形式应至少满足下列条件之一:

  1. 函数可以作为参数被传入(即回调函数),如函数合成运算
  2. 可以返回函数作为输出,如函数柯里化运算

compose(函数合成)和 curry(柯里化)是函数式编程两种最基本的运算,它们都利用了函数闭包的特性和思路来进行设计。在函数式编程中,经常见到如下表达式运算:

a(b(c(x)));

这是 包菜式 多层函数调用,不是很优雅。为了解决函数多层调用的嵌套问题,需要用到函数合成。合成语法形式如下:

f = compose(a, b, c)  # 合成函数
f(x)

示例:

def compose(f, g):  # 两个函数合成
    def sub(x):
        return f(g(x))

    return sub


def add(x): return x + 1  # 加法运算


def mul(x): return x * 5  # 乘法运算


f = compose(mul, add)  # 合并加法运算和乘法运算
print(f(2))  # 输出为 15

在上面的代码中,compose() 函数的作用就是组合函数,将函数串联起来执行。将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数,一旦第1个函数开始执行,就会像多米诺骨牌一样推导执行了。

注意:使用 compose() 函数里注意以下3点:

  1. compose() 函数的参数是函数,返回的也是一个函数。
  2. 除了初始函数(最右侧的一个)外,其他函数的接收参数都是上一个函数的返回值,即初始函数的参数可以是多余的,而其他函数的接收值是一元的。
  3. compose() 函数可以接收任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定放到参数的最右侧。

设计思路: 既然函数像多米诺骨牌执行,可以使用递归或选代,在函数体内不断地执行参数中的函数,将上个函数的执行结果作为下一个执行函数的输入参数。下面来完善 compose() 实现,实现无限函数合成。

# 函数合成,从右到左合成函数
def compose(*arguments):
    _arguments = arguments  # 缓存外层参数
    length = len(_arguments)  # 缓存长度
    index = length  # 定义游标变量
    # 检测参数,如果存在非函数参数,则抛出异常
    while (index):
        index = index - 1
        # if (not hasattr(_arguments[index], '__call__')):
        if not callable(_arguments[index]):
            raise NameError('参数必须为函数!')

    # 在返回的内层函数中执行运算
    def sub(*args, **kwargs):
        index = length - 1  # 定位到最后一个参数下标
        # 调用最后一个参数函数,并传入内层参数
        result = _arguments[index](*args, **kwargs)
        # 迭代参数函数
        while (index):
            index = index - 1
            # 把右侧函数的执行结果作为参数传给左侧参数函数,并调用
            result = _arguments[index](result)

        return result  # 返回最左侧参数函数的执行结果

    return sub


#  反向函数合成,即从左到右合成函数
def composeLeft(*arguments):
    list = []  # 定义临时列表
    for i in arguments:  # 遍历参数列表
        list.insert(0, i)  # 倒序排列,因为元素为函数无法调用内置函数
    return compose(*list)  # 调用compose()函数


add = lambda x: x + 5  # 加法运算
mul = lambda x: x * 5  # 乘法运算
sub = lambda x: x - 5  # 减法运算
div = lambda x: x / 5  # 除法运算

fn = compose(div, sub, mul, add);
print(fn(50));  # 输出为54
fn = composeLeft(mul, div, sub, add);
print(fn(50));  # 输出为50
fn = compose(add, mul, sub, div);
print(fn(50));  # 输出为30
fn = compose(add, compose(mul, sub, div));
print(fn(50));  # 输出为30
fn = compose(compose(add, mul), sub, div);
print(fn(50));  # 输出为30

# 初始函数可以接收任意个参数,并求和
from functools import reduce


def sum(*arg):
    return reduce(lambda x, y: x + y, arg)


fn = compose(div, sub, mul, add, sum)
print(fn(1, 2, 3))

在上面实现的代码中,compose() 实现从右到左进行合成,也提供了从左到右的合成,即composeLeft(),同时在 compose() 内添加了一层函数的校验,允许传递一个或多个参数。

5.2 函数柯里化

函数合成是把多个单一参数的函数合成为一个多参数的函数运算。例如, a(x) 和 b(x) 组合为 a(b(x)),则合成为 f(a, b, x)。这里的 a(x) 和 b(x) 都只能接收一个参数。如果接收多个参数,如 a(x,y) 和 b(a,b,c),那么函数合成就比较麻烦。

与函数合成相反的运算就是函数柯里化。所谓柯里化,就是把一个多参数的函数转化为一个单一参数的函数。有了柯里化运算之后,就能让所有函数只接收一个参数,实现分步运算。定义一个闭包体,先记录函数部分所需要的参数,然后异步接收剩余参数。也就是说,把多参数的函数分解为多步运算的函数,以实现每次调用函数时,仅需要传递更少或单个参数。例如,下面是一个简单的求和函数 add():

def add(x, y):
    return x + y

每次调用 add(),需要同时传入2个参数,如果希望每次仅传入1个参数,可以这样进行柯里化:

def add(x):  # 柯里化
    def sub(y):
        return x + y

    return sub


print(add(2)(6))  # 输出为 8,连续调用
add1 = add(200)
print(add1(2))  # 输出为 202,分步调用

函数 add() 接收一个参数,并返回一个函数,这个返回的函数可以再接收一个参数,最后返回两个参数之和。从某种意义上讲,这是一种对参数的 缓存 是一种非常高效的函数式运算方法。设计 curry 可以接收一个函数,即原始函数,返回的也是一个函数,即柯里化函数。这个返回的柯里化函数在执行过程中会不断地返回一个存储了传入参数的函数,直到触发了原始函数执行的条件。例如,设计一个 add() 函数,计算两个参数之和。

def add(x, y):
    return x + y

柯里化函数:

curryAdd = curry(add)

这个 add() 需要两个参数,但是执行 curryAdd 时可以传入一个参数,当传入的参数少于 add() 需要的参数时,add() 函数并不会执行,curryAdd() 就会将这个参数记录下来,并且返回另外一个函数,这个函数可以继续接收传入参数。如果传入参数的总数等于 add() 需要参数的总数,就执行原始参数,返回想要的结果。或者没有参数限制,最后根据空的小括号调用作为执行原始参数的条件,返回运算结果。curry 实现的封装代码如下:

# 柯里化函数
def curry(fn, *args, **kwargs):
    # 把传入的第2个及以后参数进行缓存
    _args = list(args)
    _kwargs = dict(kwargs)

    # if (not hasattr(fn, '__call__')):   	# 检测第一个参数是否为函数,否则抛出异常
    if not callable(fn):
        raise NameError('第一个参数必须为函数!')
    _argLen = fn.__code__.co_argcount  # 记录原始函数的形参个数

    def wrap(*args, **kwargs):  # curry函数
        # 把当前参数与前面传递的参数进行合并
        _args.extend(list(args))
        _kwargs.update(dict(kwargs))
        _len = len(args) + len(kwargs)  # 记录当前调用传入的参数个数
        _len_all = len(_args) + len(_kwargs)  # 记录传入的参数总数

        def act(*args, **kwargs):  # 当重复调用时,对参数进行合并处理
            # 把当前参数与前面传递的参数进行合并
            _args.extend(list(args))
            _kwargs.update(dict(kwargs))
            _len = len(args) + len(kwargs)  # 记录当前调用传入的参数个数
            _len_all = len(_args) + len(_kwargs)  # 记录传入的参数总数

            # 如果参数等于原始函数的参数个数,或者调用时没有传递参数,即触发执行条件
            if ((_argLen == 0 and _len == 0) or
                    (_argLen > 0 and _len_all == _argLen)):
                #  执行原始函数,并把每次传入的参数传递给原始函数,停止curry
                return fn(*_args, **_kwargs)

            return act  # 返回处理函数

        # 如果参数等于原始函数的参数个数,或者调用时没有传递参数,即触发了执行条件
        if ((_argLen == 0 and _len == 0) or
                (_argLen > 0 and _len_all == _argLen)):
            #  执行原始函数,并把每次传入参数传入进去,返回执行结果,停止curry
            return fn(*_args, **_kwargs)
        return act  # 返回处理函数

    return wrap  # 返回curry函数


# 求和函数,参数不限
# 设计求和函数没有形参限制,柯里化函数将根据空小括号作为最后调用原始函数的条件
def add(*arguments):  #
    #  迭代所有参数值,返回最后汇总的值
    sum = 0
    for i in arguments:
        if isinstance(i, (int, float)):  # 检测参数类型是否为整数或浮点数
            sum = sum + i  # 求和
    return sum


# 柯里化函数
curried = curry(add)
print(curried(1)(2)(3)())  # 6
curried = curry(add)
print(curried(1, 2, 3)(4)())  # 10
curried = curry(add, 1)
print(curried(1, 2)(3)(3)())  # 10
curried = curry(add, 1, 5)
print(curried(1, 2, 3, 4)(5)())  # 21

# 应用函数有形参限制
def add(a, b, c):  # 求和函数,3个参数之和
    return a + b + c


# 柯里化函数 
curried = curry(add, 2)
print(curried(1)(2))  # 5

curried = curry(add, 2, 1)
print(curried(2))  # 5
curried = curry(add)
print(curried(1)(2)(6))  # 9
curried = curry(add)
print(curried(1, 2, 6))  # 9

curry() 函数的设计不是固定的,可以根据具体应用场景灵活定制。curry 主要有3个作用:缓存参数、暂缓函数执行、分解执行任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amo Xiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值