1.函数式编程

虽然面向对象编程属于主流操作,但是不可否认的是包括面向过程、函数式编程、面向切面编程等都有着非常优秀的应用。

函数式编程是一种编程的范式,指导我们应该如何去编写符合函数式编程规范的代码,函数式编程具有非常明显的五个特点:

1.1 函数是“一等公民”

所谓的一等公民就是将函数与其他的数据类型一样处于一个平等的地位,可以将函数赋值给变量,可以作为参数传递给其他函数,也能够作为返回值返回。

1.2 只用“表达式”,不用“语句”

“表达式”是一个单纯的运算过程,总是有返回值的;“语句”是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

原因是函数式编程的开发动机,一开始就是为了处理运算,不考虑系统的读写。“语句“属于对系统的读写操作,所以就排斥在外。

当然,实际开发中,不做I/O是不可能的,因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

1.3 没有”副作用“

所谓的”副作用“,指的是函数内部与外部的互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

函数式编程强调没有”副作用“,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

1.4. 不修改状态

上一点已经提到了,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。在python中,装饰器最 常被提到的有点就是在不修改原有代码的基础上为其添加新的功能。

在其他类型的语言中,变量往往用来保存状态。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。

由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。

1.5引用透明

引用透明,指的是函数的运行不依赖于外部变量或者”状态“,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫”引用不透明“,很不利于观察和理解程序的行为。

2.闭包

都已经提到函数式编程了,那么闭包也是一个必不可少的需要了解的概念。

2.1 什么是闭包

python中的闭包可以理解为:如果在一个内部函数里,对其外部作用域(但不是全局作用域)的变量进行了引用,那么内部函数就被认为是闭包。

2.2作用域

作用域指的就是程序运行时变量可以被访问的范围,但是闭包函数的作用域和我们常编写代码的作用域就有所不同,闭包就是一种能够在脱离了函数本身的作用范围,依旧能够访问函数里的局部变量的方式。

def func():
    msg = 'test'
    def printer():
        print(msg)
    return printer

func()()

在上述代码中,我们将内部函数返回之后,就可以视作msg变量已经脱离了其作用域,但是我们执行func函数的返回结果时,依旧能够正常打印出变量的信息,这就是闭包。

2.3 为什么要使用闭包

因为闭包避免了必须要使用全局变量的情况,在实际开发的时候,我们应该尽量避免使用全局变量,而闭包就解决了这个问题,将变量和环境关联了起来,在这一点上闭包和面向对象是非常相似的(我看到过的一个对面向对象编程一个印象非常深刻的解释就是将变量和方法绑定)。

def adder(x):
    def wrapper(y):
        return x + y
    return wrapper

adder5 = adder(5)
# 输出 15
adder5(10)
# 输出 11
adder5(6)

2.4闭包中使用的变量都存在哪里了?

看完上述部分,可能会产生疑惑,既然变量已经脱离了其作用域,那么是如何访问到内部函数的变量的呢?

这是因为所有的函数都有一个__closure__ 属性,如果这个函数是一个闭包的话,那么它返回的是一个由cell对象组成的元组对象。cell对象中的cell_contents属性就是闭包中的自由变量。

print(adder5.__closure__)
# 输出 (<cell at 0x0000023315310108: int object at 0x00007FFCF37BD4A0>,)
print(adder5.__closure__[0].cell_contents)
# 输出 5

2.5 使用闭包时的注意事项

使用闭包常遇到的问题有两个,第一个就是在内部函数中不能够修改外部变量,另一个就是在使用高阶函数时经常会遇到的问题闭包延时绑定

def func():
    x = 1
    def wrapper():
        x += 1
    return wrapper
func()()
# 直接报错 UnboundLocalError: local variable 'x' referenced before assignment

解决这个问题的办法也很简单,只需在wrapper函数内部用nonlocal修饰引用的外部变量即可。

l = []
for i in range(3):
    def wrapper():
        print(i)
    l.append(wrapper)

for each in l:
    each()
    # 输出 2 2 2

这个实例是一个比较直观的例子,在python的几个高阶函数中都有对参数的迭代,如果使用不慎也会轻易就引起上述问题。原因在于,将内部函数添加到列表中的时候,并未对变量的值进行绑定,当执行内部函数的时候才回去寻找变量的值,而变量 i 还没有被销毁,其值是迭代的最后一个值,因此结果就和预期要求不一致。解决办法同样很简单:

l = []
for i in range(3):
    def wrapper(x=i):
        print(x)
    l.append(wrapper)

for each in l:
    each()
    # 输出0 1 2

3.装饰器

其实在前文中已经完成了一个装饰器,只是这和我们常用的装饰器有所区别的是,python中的@语法糖可以简化装饰器的调用。

3.1多个装饰器的执行顺序

def dec1(func):
    print('这是装饰器1的外部函数')
    def wrapper(*args, **kwargs):
        print('这是装饰器1的内部函数')
        return func(*args, **kwargs)
    return wrapper

def dec2(func):
    print('这是装饰器2的外部函数')
    def wrapper(*args, **kwargs):
        print('这是装饰器2的内部函数')
        return func(*args, **kwargs)
    return wrapper

@dec1
@dec2
def test():
    print('这里是test函数')

test()
# 执行结果:
# 这是装饰器2的外部函数
# 这是装饰器1的外部函数
# 这是装饰器1的内部函数
# 这是装饰器2的内部函数
# 这里是test函数

可以从结果上看出,多个装饰器装饰时,包装是自内向外,执行是自外向内。

我们将上述代码稍作修改就能够很清晰的明白为什么它的执行结果会是这个样子

def dec1(func):
    print('这是装饰器1的外部函数')
    def wrapper(*args, **kwargs):
        print('这是装饰器1的内部函数')
        return func(*args, **kwargs)
    return wrapper

def dec2(func):
    print('这是装饰器2的外部函数')
    def wrapper(*args, **kwargs):
        print('这是装饰器2的内部函数')
        return func(*args, **kwargs)
    return wrapper

# @dec1
# @dec2
def test():
    print('这里是test函数')

# test()


test1 = dec2(test)
test2 = dec1(test1)
print('装饰部分')
test2()
# 执行结果
# 这是装饰器2的外部函数
# 这是装饰器1的外部函数
# 装饰部分
# 这是装饰器1的内部函数
# 这是装饰器2的内部函数
# 这里是test函数

这里就与本文的函数式编程对应上了,函数也是一等公民,可以作为参数传递,可以作为返回值返回,上述代码中我们只需要明确一点函数与函数执行是不一样的就能够清晰的看出多个装饰器执行时结果。

可以根据前文中的闭包得出,最后的包装之后的结果应该是这样的。

def last(func):
    print('这是装饰器1的内部函数')
    def wrapper(func):
        print('这是装饰器2的内部函数')
        return func
    return wrapper(func)

last(test)()
# 运行结果
# 这是装饰器1的内部函数
# 这是装饰器2的内部函数
# 这里是test函数

3.2 装饰器的执行时机

装饰器的执行时机可以从前文中得出一个结论就是,装饰器在装饰函数时就已经被执行了,在执行被装饰的函数时并不是去执行装饰器,而是执行被装饰器装饰之后的函数。

还有另外一点就是,被装饰函数在被导入另外一个文件时,装饰器立刻执行。

这两点验证都很简单,因此不在此演示代码。

3.3带参数的装饰器

def dec_argument(msg):
    msg = 'test: '+ msg
    def dec1(func):
        def wrapper(*agrs, **kwargs):
            return func(msg, *agrs, **kwargs)
        return wrapper
    return dec1
@dec_argument('带参数的装饰器')
def test(msg):
    print(msg)

test()
# 输出结果
# test: 带参数的装饰器

如果看懂了前文的内容的话就会发现这个非常容易理解,就是多一层内部函数嵌套而已。

3.4装饰器带来的问题

如果阅读前文时细心一点就能够发现装饰器将给我们带来什么样的问题,在3.1多个装饰器的执行顺序最后得到的last函数那里就能够很清晰的看出,被装饰之后的函数和装饰之前的函数已经不是同一个函数了,因此原函数的一些信息也随之发生了改变:

def dec(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
@dec # 注释这一行比较装饰前后的结果
def test():
    pass
print(test.__name__)
# 装饰前:test
# 装饰后:wrapper

可以看到前后结果已经完全不一致了,在python的functools模块中,提供了一个wraps方法可以将原函数的这些信息复制给被装饰的函数:

from functools import wraps
def dec(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
@dec
def test():
    pass
print(test.__name__)
# 装饰前:test
# 装饰后:test

文中如有错误,请给我留言,我会及时更改,如果对您造成误导,非常抱歉。

refer:

函数式编程初探:https://www.ruanyifeng.com/blog/2012/04/functional_programming.html

一步一步教你认识python的闭包:https://foofish.net/python-closure.html

python中的闭包:https://blog.csdn.net/marty_fu/article/details/7679297