Skip to content

函数式编程

Python的高级特性2

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

函数的本质

在学C语言指针的时候,会接触到一个概念:函数名是函数所在内存的地址,类似-数组名是数组的首元素的地址。

即使Python中没有指针一说,但是本质还是一样的。 变量名是一个变量的地址 函数名是一个函数的地址 调用函数的时候其实是把参数传到函数本身所在的那块内存中去,函数名不过是一个媒介而已。 在C中可以说,函数名就是个指针,指向一个函数 在Python中可以说,函数名就是个变量,指向一个函数

既然函数名只是指向函数本身所在地址,那我换个名来指不也可以。

def sum(a, b):
    return a + b

f = sum
f(1, 2)

---------------------------------

# Output:
3
如果放到交互模式中,可以更明显的看出来

# 定义一个函数
>>> def sum(a, b):
...     return a + b
...
>>> sum
<function sum at 0x000001F25AF6B280>

>>> f=sum
>>> f
<function sum at 0x000001F25AF6B280>
>>> f(3, 4)
7

大概就这么回事儿

1

高阶函数

高阶函数:接受函数作为参数的函数 def heigh_fn(a, b, fn)

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

一个最简单的高阶函数:

def abs(num):
    return (num) if num >= 0 else (-num)

def abs_add(a, b, fn):
    return fn(a) + fn(b)

print(abs_add(8, -4, abs))

---------------------------------

# Output:
12

这个高阶函数的过程大概是这样的 a = 8 b = -4 fn = abs fn(a) + fn(b) ==> abs(8) + abs(-4) ==> 12

返回函数

返回函数:返回值为函数的函数

通常函数都是返回int、str、list 这些变量,但是变量可以指向函数, 那函数可以返回一个变量,不就也可以返回一个函数。

举个栗子 实现一个求和函数。参数可变,可以是1个也可以是多个。

def cal_sum(*args):
    sum = 0
    for n in args:
        sum += n
    return sum


result = cal_sum(1, 2, 3, 4, 5)
print(result)

---------------------------------

# Output:
15
好,现在再写一个函数lazy_sum(),把这个函数cal_sum()作为返回值返回
1
2
3
4
5
6
7
def lazy_sum(*args):
    def cal_sum():
        sum = 0
        for n in args:
            sum += n
        return sum
    return cal_sum
可以看到最后一句return cal_sum就把函数返回出来了。 那么函数lazy_sum()就是一个返回函数

现在调用一下这个返回函数

def lazy_sum(*args):
    print('I am the return function.')
    def cal_sum():
        sum = 0
        for n in args:
            sum += n
        return sum
    return cal_sum


result = lazy_sum(1, 2, 3, 4, 5)
print(result)

---------------------------------

# Output:
I am the return function.
<function lazy_sum.<locals>.cal_sum at 0x000001C8765CB310>
哦嚯,lazy_sum()没有进行计算,但也看出来,lazy_sum()返回的是一个函数 result此时是一个函数,这时候我们要去调用一次返回值result,才会进行计算
def lazy_sum(*args):
    print('I am the return function.')
    def cal_sum():
        sum = 0
        for n in args:
            sum += n
        return sum
    return cal_sum


result = lazy_sum(1, 2, 3, 4, 5)
print(result)

res = result()
print(res)

---------------------------------

# Output:
I am the return function.
<function lazy_sum.<locals>.cal_sum at 0x000001E02D07B310>
15
这就是为什么说 返回函数是惰性的。就是这么来的

为什么要返回一个函数

lazy_sum()中,有一句print语句。 从输出中可以看出,返回函数本身是立即执行的,但是它返回出来的函数要再进行一次调用才会执行。

单独一个返回函数并没有太大作用,但是返回函数和高阶函数结合起来,是不是就有意思了。 接收一个函数,然后把这个函数返回出去。在返回之前是不是可以进行一些操作?

假如现在有一个需求:求和;实现完了之后,产品经理又加需求:除了求和还要打印一下求和用了多长时间,也就是计时功能。 那么现在你有两个办法 - 一是在求和函数里实现这个计时功能,但是这样就在一个函数里做了两件事情。即不优雅,也不好维护。这里举例是简单的求和,实际开发中可能是跟复杂的功能。 - 二是把计时函数写出来,然后把求和函数传给计时函数,在计时函数中给求和函数加上计时功能,之后再把这个有计时功能的求和函数返回出去。

计时函数这种接收函数 -> 增强函数 -> 返回函数的函数就叫闭包

不仅可以用在计时功能,还能用在日志功能,我只要把求和函数写好,传给日志函数就可以获得日志功能。 函数只做一件事情,其他事情由别的函数做,需要什么功能把函数往里面一丢,还你一个增强版的。Amazing!这就叫面向切面编程

闭包、装饰器、面向切面编程

闭包

闭包 = 高阶函数 + 返回函数 即,一个接收函数作为参数,又返回一个函数的函数,就叫做闭包 作用:增强函数功能、面向切面编程(AOP)

无参闭包

import time

def print_hello():
    print('hello!')

def cal_time(fn):
    def improve_fn():
        start_time = time.time()
        fn()
        end_time = time.time()
        print('耗时:{}'.format(end_time - start_time))

    return improve_fn

improve_print_hello = cal_time(print_hello)
improve_print_hello()

# 也可以写做 cal_time(print_hello)()

---------------------------------

# Output:
hello!
耗时0.0009620189666748047
栗子中,print_hello()只负责打印hello,cal_time()负责把传进来的函数增强然后返回出去 所以cal_time就是一个闭包

带参闭包

上面的栗子,print_hello()不需要参数,所以在无参闭包中可以顺利通过 但是如果一个函数有参,那无参闭包就会抛出TypeError错误。

想要带参很简单。

import time


def add(a, b):
    print(a + b)


def cal_time(fn):
    def improve_fn(*args, **kwargs):    # 在这里写下形参
        start_time = time.time()
        fn(*args, **kwargs)        # 调用的时候传进去
        end_time = time.time()
        print('耗时:{}'.format(end_time - start_time))

    return improve_fn


cal_time(add)(1, 2)

---------------------------------

# Output:
3
耗时0.00800633430480957

接收的函数有返回值

接收的函数有返回值的话,应该在闭包里对函数调用的时候把参数接收下来,然后进行返回。

import time


def add(a, b):
    return a + b


def cal_time(fn):
    def improve_fn(*args, **kwargs):
        start_time = time.time()
        count = fn(*args, **kwargs)     # 调用函数之后用一个变量接收返回值
        end_time = time.time()
        print('耗时:{}'.format(end_time - start_time))
        return count                    # 然后把返回值返回出去

    return improve_fn


result = cal_time(add)(5, 12)
print(result)

---------------------------------

# Output:
耗时0.0009620189666748047
17

装饰器

装饰器是闭包的语法糖——@闭包名@闭包名(参数) 语法糖只是一个更方便的写法,完全可以等价转换为非语法糖的代码

装饰器、闭包,本质上也是函数,说装饰器的同时也是在说闭包

保留原函数的元信息

装饰器(闭包)的原理就是把原函数当作参数传进装饰器中,经过增强后返回一个新的增强版原函数 这说明返回出来的增强版函数和原来的函数不是同一个了(在内存中创建了一个新的)

既然不是在原函数上进行改造,这就导致函数的元信息(文档字符串、注解、参数签名等)都丢失了 解决方法就是使用functools库中的@wraps装饰器来把原函数的元信息复制过来。

先看看保留元信息的样子

import time


# 装饰器函数
def cal_time(fn):
    '''
    Decorator that report the execution time.
    '''
    def improve_fn(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        end = time.time()
        print(fn.__name__, end - start)
        return result

    return improve_fn


# 原函数
@cal_time
def countdown(n):
    '''
    Count down
    '''
    while n > 0:
        n -= 1


if __name__ == '__main__':
    countdown(10_000)
    print(countdown.__doc__)    # 没有使用wrap,所以元信息都丢失了

---------------------------------

# Output:
countdown 0.0010008811950683594
None
再看看使用了@wraps的样子
import time
from functools import wraps

# 装饰器函数
def cal_time(fn):
    '''
    Decorator that report the execution time.
    '''
    @wraps(fn)
    def improve_fn(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        end = time.time()
        print(fn.__name__, end - start)
        return result

    return improve_fn


# 原函数
@cal_time
def countdown(n):
    '''
    Count down
    '''
    while n > 0:
        n -= 1


if __name__ == '__main__':
    countdown(10000)
    print(countdown.__doc__)    # 使用了wrap,所以元信息没有丢失

---------------------------------

# Output:
countdown 0.0010025501251220703

    Count down

无参装饰器

import time
from functools import wraps


def cal_time(fn):
    '''
    Decorator that report the execution time.
    '''
    @wraps(fn)
    def improve_fn(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        end = time.time()
        print(fn.__name__, 'used time:', end - start)
        return result

    return improve_fn


# 装饰器里不需要携带参数
@cal_time
def add(a, b):
    return a + b

res = add(5, 6)
print(res)

# 等价于
#
# def add(a, b):
#     return a + b
#
# improve_add = cal_time(add)
# res = improve_add(5, 6)
# print(res)

---------------------------------

# Output:
add used time: 0.0
11

带参装饰器

import time
from functools import wraps

def cal_time(msg):    # 第一层,接收装饰器参数
    print('First floor:', msg)

    def decorator(fn):    # 第二层,接收函数
        print('Second floor:', msg)

        @wraps(fn)
        def improve_fn(*args, **kwargs):    # 第三层,增强函数
            print('Third floor:', msg)

            start = time.time()
            result = fn(*args, **kwargs)
            end = time.time()

            print('Used time:', end - start)
            return result

        return improve_fn

    return decorator


@cal_time('I am the message.')
def countdown(time=10000):
    while time > 0:
        time -= 1


if __name__ == '__main__':
    countdown()

---------------------------------

# Output:
First floor: I am the message.
Second floor: I am the message.
Third floor: I am the message.
Used time: 0.000997781753540039
有参装饰器和无参装饰器的区别仅仅只是多了一层函数而已 也就是在无参装饰器的基础上多一层函数来接收装饰器参数而已。注意带参装饰器即使不填参数也要写上括号

装饰器只在第一次被调用去装饰函数时对原函数进行增强 - 增强时机:第一调用之前 - 增强次数:只增强一次

面向切面编程

如果说类里面的继承是纵向继承,那面向切面编程可以理解为横向继承

纵向继承中,子类继承了父类非私有属性和方法;同属一个父类的子类都有相同的父类属性和方法 横向继承中,是给子类增加了一些功能;同属一个父类的子类不见得有相同的功能。

就好比,一个爹生了两个娃,两个娃都有手有脚的,这是继承自爹那里的(纵向继承) 但是一个娃点亮了游泳的技能,另一个娃点亮了跳舞的技能(横向继承) 这俩娃生了俩孙子,肯定也是有手有脚的,但不一定会游泳跳舞。

所以面向切面编程中,这种闭包、装饰器之类的通常是用在编写通用工具类上。

匿名函数

lambda [parameters]: expression 冒号前面的parameters表示匿名函数的形参列表,冒号后面的expression表示要return的语句 parameters可以有也可以没有。 匿名函数有个限制——只能有一个表达式,不用写return,返回值就是该表达式的结果。

匿名函数类似于 es6 中的简头函数

举个栗子:

def add(a, b):
    return a + b

写成匿名函数就是

lambda a, b: a + b
写成简头函数就是
(a, b) => a + b

匿名函数可以作为实参、返回值,在这些场景中应用的比较多。

匿名函数作为实参

# 普通写法
def abs(num):
    return (num) if num >= 0 else (-num)

def abs_add(a, b, fn):
    return fn(a) + fn(b)

print(abs_add(8, -4, abs))

---------------------------------
# 匿名函数写法
def abs_add(a, b, fn):
    return fn(a) + fn(b)

result = abs_add(8, -4, lambda x: x if x >= 0 else -x)    # 匿名函数作为实参
print(result)

---------------------------------

# Output:
12

匿名函数作为返回值

1
2
3
4
5
6
7
8
9
def build(x, y):
    return lambda: x * x + y * y        # 匿名函数作为返回值

build(2, 3)

---------------------------------

# Output:
13

总结

  1. 函数名的本质:函数起始地址
  2. 高阶函数:接收函数作为参数的函数
  3. 返回函数:返回一个函数(即返回一个函数地址)的函数,是惰性的
  4. 闭包:接收函数作为参数,并返回一个函数的函数(闭包=高阶函数+返回函数)
  5. 装饰器:闭包的语法糖,@闭包名字
  6. 匿名函数:lambda 参数: 返回值