介绍函数式编程、闭包、装饰器和单例模式。(Python版本)

在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

在面向过程编程中,我们见到过函数(function);在面向对象编程中,我们见过对象(object)。函数和对象的根本目的是以某种逻辑方式组织代码,并提高代码的可重复使用性(reusability)。函数式编程也是一种编程范式,其中闭包(closure)是函数式编程的重要的语法结构。闭包是一种组织代码的结构,它提高了代码的可重复使用性,可以简单地把闭包理解成轻量级的接口封装。

(1)闭包

要形成闭包,首先得有一个嵌套的函数,即函数中定义了另一个函数,闭包则是一个集合,它包括了外部函数的局部变量,这些局部变量在外部函数返回后也继续存在,并能被内部函数引用。按变量的作用域进行分类,Python 中的变量可分为「全局变量」、「局部变量」以及「自由变量」。一般而言,Python 中使用变量前不需要声明变量,但假定在函数体中赋值的变量为局部变量,除非显式使用 global 将在函数中赋值的变量声明为全局变量!而自由变量则是存在于嵌套函数中的一个概念:定义在其他函数内部的函数被称之为嵌套函数 nested function ,嵌套函数可以访问封闭范围内(外部函数)的变量。嵌套函数不可以在函数外直接访问。

闭包的最大特点是可以将父函数的变量与内部函数绑定,并返回绑定变量后的函数(也即闭包),此时即便生成闭包的环境(父函数)已经释放,闭包仍然存在,这个过程很像类(父函数)生成实例(闭包),不同的是父函数只在调用时执行,执行完毕后其环境就会释放,而类则在文件执行时创建,一般程序执行完毕后作用域才释放,因此对一些需要重用的功能且不足以定义为类的行为,使用闭包会比使用类占用更少的资源,且更轻巧灵活。闭包在爬虫以及web应用中都有很广泛的应用,并且闭包也是装饰器的基础。

但是需要注意的是:闭包只能访问,无法修改外部函数的局部变量。比如:

1
2
3
4
5
6
7
def line_conf():
    b = 15
    def line(x):
        return 2*x-b
    return line       # return a function object
my_line = line_conf()
print(my_line(10))

在 Python 中,非本地变量默认仅可读取,在修改时必须显式指出其为非本地变量~自由变量 nonlocal,全局变量 global。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
something =0 # 这个是函数外部的变量
def get_avg():
    scores = 0  # 将外部临时变量由 list 改为一个 整型数值
    count = 0   # 同时新增一个变量,记录个数
    def inner_count_avg(val):  # 内部函数,用于计算平均值
        nonlocal count, scores # 这个表示想要修改子函数外面的变量
        scores += val  # 使用外部函数的临时变量
        count += 1
        return scores / count  # 返回计算出的平均值
    return inner_count_avg  # 外部函数返回内部函数引用
avg = get_avg()
print(avg(10))  # 报错

(2)函数式编程

函数式编程的特点:

  • 函数式编程无副作用。所谓”副作用”(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
  • 引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫”引用不透明”,很不利于观察和理解程序的行为。

非函数式的例子:

1
2
3
4
int cnt;
void increment(){
    cnt++;
}

函数式的例子:

1
2
3
int increment(int cnt){
    return cnt+1;
}

函数式编程的技术:

  • map & reduce :这个技术不用多说了,函数式编程最常见的技术就是对一个集合做Map和Reduce操作,重点在于描述问题。
  • pipeline:这个技术的意思是,把函数实例成一个一个的action,然后,把一组action放到一个数组或是列表中,然后把数据传给这个action list,数据就像一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。

把函数当成变量来用,关注于描述问题而不是怎么实现,这样可以让代码更易读。

1
2
3
name_len = map(len, ["hao", "chen", "coolshell"])
print name_len
# 输出 [3, 4, 9]

Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。尽管 Python 算不上是一门纯函数式编程语言,但它本身提供了很多函数式编程的特性,像 map、reduce、filter、sorted 这些函数都支持函数作为参数,lambda 函数就可以应用在函数式编程中。

1
2
3
list1 =[3, 4, -4, -1, 0, -2, -6]
list1.sort(key =lambda x: abs(x))
print(list1)

当使用传统的方式书写的时候,就不够pythonic

1
2
3
4
5
6
def my_add(n):
    return lambda x: x-n
add_3 =my_add(3) # add_3 是一种 lambda 函数
print(add_3) # 定义的时候把 n初始化为了3
print("*****")
print(add_3(4)) # 4对应着x 输出是1,

(当处理简单逻辑时候,可以使用lambda 函数,当处理复杂逻辑时候,不建议使用)

(3)Decorator装饰器

python 中函数的”人设“, function 也是一种对象,内部函数可以访问外部的 function的变量,但是权限是”只读“。一个函数可以输入一个函数也可以返回一个函数。装饰器使得代码可以复用。调用的时候,先调用装饰器,然后调用函数,执行的时候,先执行内层函数,最后是装饰器,类似递归的过程,深度优先,最后返回的是一个函数。总的来说,Decorator在你希望在不修改函数本身代码的前提下扩展函数的功能时非常有用。

例子说明。下面代码和上面是相同的功能,p_decorate 就像是 function get_text() 的一个外套, 其作为一种输入到 p_decorate() 中。装饰器首先执行的是装饰器部分,本函数作为一个变量使用,最后返回的是结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def p_decorate(func):
   def func_wrapper(name):
       return "<p>{0}</p>".format(func(name))
   return func_wrapper

@p_decorate
def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

print (get_text("John"))

输出结果:

1
<p>lorem ipsum, John dolor sit amet</p>

在给个例子,理解调用过程。

1
2
3
4
5
6
7
8
9
def hello_decorator(original_fn):
    def decorator_fn():
        print("Hello from new")
        original_fn()   # original function must be invoked
    return decorator_fn

@hello_decorator
def hello():
   print("Hello from original")

输出结果:

1
2
Hello from new
Hello from original

另外,一个函数是可以添加多个 修饰器的,并且修饰器的顺序也是有关系的。一个函数还可以同时定义多个装饰器,比如:

1
2
3
4
5
@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

1
f = a(b(c(f)))

执行顺序是先执行里面的,但是里面的func 作为一种输入到下一个函数中,所以最后先执行的还是“外面”函数。类似不断的递归进去的感觉。

感觉装饰器很难的原因在于没有理清它的逻辑关系,本质上装饰器也是函数,但它是对核心程序的闭包封装,在原有的基础上增加更多的功能。细细回顾几遍上面的例子能够加深对装饰器的理解。

(4)使用修饰器实现单例模式

设计模式分成单例模式和多例模式,对于单例模式对于一个类只能实现一个实例;多例模式可以实现多个实例。单例是一种设计模式,应用该模式的类只会生成一个实例。这种方式是可以代替全局变量的。比如一些配置、日志等只需要初始化一次的文件,就可以使用这种方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 函数修饰器实现单例
def singleton(cls):
    _instance ={}
    def inner():
        if cls not in _instance:
            _instance[cls] =cls
        return _instance[cls]
    # 函数调用
    return inner

@singleton
class Cls(object):
    def __init__(self):
        pass
cls1 =Cls()
cls2 =Cls()
print(id(cls1) ==id(cls2))

常见的自带的修饰器。

Python中 的 @classmethod 和 @staticmethod

在对象的实例方法中,self 参数是类实例对象本身,我们可以用它来对实例数据进行一些操作。@classmethod 方法也有一个强制性的第一个参数,它表示的是未实例化的类本身,而非类的实例。

1
2
3
4
5
6
7
class Student(object):
    @classmethod
    def from_string(cls, name_str):
        first_name, last_name = map(str, name_str.split(' '))
        student = cls(first_name, last_name)
        return student
scott = Student.from_string('Scott Robinson')  

@staticmethod 装饰器

@staticmethod 装饰类似于@classmethod ,它能够从一个非实例类对象被调用,但是没有传递 cls 参数。

1
2
3
4
5
6
7
class Student(object):
    @staticmethod
    def is_full_name(name_str):
        names = name_str.split(' ')
        return len(names) > 1
Student.is_full_name('Scott Robinson')   # True  
Student.is_full_name('Scott')            # False  

由于没有 self 传递任何对象,这意味着此装饰器方法无法访问任何实例数据,并且也无法在实例化对象上调用此方法。这些类型的方法通常不是为了创建/实例化对象,它们是为了处理一些与类本身有关的逻辑。