博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Python3学习笔记08-闭包、装饰器、内存管理
阅读量:2384 次
发布时间:2019-05-10

本文共 10447 字,大约阅读时间需要 34 分钟。

##一、闭包 **闭包(closure)**是函数式编程的重要的语法结构。函数式编程是一种编程范式 (而面向过程编程和面向对象编程也都是编程范式)。在面向过程编程中,我们见到过函数(function);在面向对象编程中,我们见过对象(object)。函数和对象的根本目的是以某种逻辑方式组织代码,并提高代码的可重复使用性(reusability)。闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性。

不同的语言实现闭包的方式不同。Python以函数对象为基础,为闭包这一语法结构提供支持的 (我们在特殊方法与多范式中,已经多次看到Python使用对象来实现一些特殊的语法)。Python一切皆对象,函数这一语法结构也是一个对象。在函数对象中,我们像使用一个普通对象一样使用函数对象,比如更改函数对象的名字,或者将函数对象作为参数进行传递。

###1、函数对象的作用域 和其他对象一样,函数对象也有其存活的范围,也就是函数对象的作用域。函数对象是使用def语句定义的,函数对象的作用域与def所在的层级相同。比如下面代码,我们在 line_conf 函数的隶属范围内定义的函数line,就只能在 line_conf 的隶属范围内调用。

def line_conf():    def line(x):        return 2*x+1    print(line(5))   # within the scopeline_conf()print(line(5))       # out of the scope

输入图片说明 line函数定义了一条直线(y = 2x + 1)。可以看到,在 line_conf() 中可以调用line函数,而在作用域之外调用line将会有下面的错误:

NameError: name 'line' is not defined

说明这时已经在作用域之外。

同样,如果使用lambda定义函数,那么函数对象的作用域与lambda所在的层级相同。

###2、闭包 函数是一个对象,所以可以作为某个函数的返回结果

def line_conf():    def line(x):        return 2*x+1    return line       # return a function objectmy_line = line_conf()print(my_line(5))

上面的代码可以成功运行。line_conf 的返回结果被赋给line对象。上面的代码将打印11。

如果line()的定义中引用了外部的变量,会发生什么呢?

def line_conf():    b = 15    def line(x):        return 2*x+b    return line       # return a function objectb = 5my_line = line_conf()print(my_line(5))

我们可以看到,line定义的隶属程序块中引用了高层级的变量b,但b信息存在于line的定义之外 (b的定义并不在line的隶属程序块中)。我们称b为line的环境变量。事实上,line作为line_conf 的返回值时,line中已经包括b的取值(尽管b并不隶属于line)。

上面的代码将打印25,也就是说,line所参照的b值是函数对象定义时可供参考的b值,而不是使用时的b值。

一个函数和它的环境变量合在一起,就构成了一个闭包(closure)。在Python中,所谓的闭包是一个包含有环境变量取值的函数对象。环境变量取值被保存在函数对象的**__closure__属性**中。比如下面的代码:

def line_conf():    b = 15    def line(x):        return 2*x+b    return line       # return a function objectb = 5my_line = line_conf()print(my_line.__closure__)print(my_line.__closure__[0].cell_contents)

__closure__里包含了一个元组(tuple)。这个元组中的每个元素是cell类型的对象。我们看到第一个cell包含的就是整数15,也就是我们创建闭包时的环境变量b的取值。

下面看一个闭包的实际例子:

def line_conf(a, b):    def line(x):        return a*x + b    return lineline1 = line_conf(1, 1)line2 = line_conf(4, 5)print(line1(5), line2(5))

这个例子中,函数line与环境变量a,b构成闭包。在创建闭包的时候,我们通过line_conf 的参数a,b说明了这两个环境变量的取值,这样,我们就确定了函数的最终形式(y = x + 1和y = 4x + 5)。我们只需要变换参数a,b,就可以获得不同的直线表达函数。由此,我们可以看到,闭包也具有提高代码可复用性的作用。

如果没有闭包,我们需要每次创建直线函数的时候同时说明a,b,x。这样,我们就需要更多的参数传递,也减少了代码的可移植性。利用闭包,我们实际上创建了泛函。line函数定义一种广泛意义的函数。这个函数的一些方面已经确定(必须是直线),但另一些方面(比如a和b参数待定)。随后,我们根据line_conf传递来的参数,通过闭包的形式,将最终函数确定下来。

###3、闭包与并行运算 闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。

并行运算正称为一个热点。这也是函数式编程又热起来的一个重要原因。函数式编程早在1950年代就已经存在,但应用并不广泛。然而,我们上面描述的流水线式的工作并行集群过程,正适合函数式编程。由于函数式编程这一天然优势,越来越多的语言也开始加入对函数式编程范式的支持。

##二、装饰器 装饰器(decorator)是一种高级Python语法。装饰器可以对一个函数、方法或者类进行加工。在Python中,我们有多种方法对函数和类进行加工,比如在Python闭包中,我们见到函数对象作为某一个函数的返回结果。相对于其它方式,装饰器语法简单,代码可读性高。因此,装饰器在Python项目中有广泛的应用。

装饰器最早在Python 2.5中出现,它最初被用于加工函数和方法这样的可调用对象(callable object,这样的对象定义有__call__方法)。在Python 2.6以及之后的Python版本中,装饰器被进一步用于加工类。

###1、装饰函数和方法 我们先定义两个简单的数学函数,一个用来计算平方和,一个用来计算平方差:

# get square sumdef square_sum(a, b):    return a**2 + b**2# get square diffdef square_diff(a, b):    return a**2 - b**2print(square_sum(3, 4))print(square_diff(3, 4))

在拥有了基本的数学功能之后,我们可能想为函数增加其它的功能,比如打印输入。我们可以改写函数来实现这一点:

# modify: print input# get square sumdef square_sum(a, b):    print("intput:", a, b)    return a**2 + b**2# get square diffdef square_diff(a, b):    print("input", a, b)    return a**2 - b**2print(square_sum(3, 4))print(square_diff(3, 4))

我们修改了函数的定义,为函数增加了功能。

现在,我们使用装饰器来实现上述修改:

def decorator(F):    def new_F(a, b):        print("input", a, b)        return F(a, b)    return new_F# get square sum@decoratordef square_sum(a, b):    return a**2 + b**2# get square diff@decoratordef square_diff(a, b):    return a**2 - b**2print(square_sum(3, 4))print(square_diff(3, 4))

装饰器可以用def的形式定义,如上面代码中的decorator。装饰器接收一个可调用对象作为输入参数,并返回一个新的可调用对象。装饰器新建了一个可调用对象,也就是上面的new_F。new_F中,我们增加了打印的功能,并通过调用F(a, b)来实现原有函数的功能。

定义好装饰器后,我们就可以通过@语法使用了。在函数square_sum和square_diff定义之前调用@decorator,我们实际上将square_sum或square_diff传递给decorator,并将decorator返回的新的可调用对象赋给原来的函数名(square_sum或square_diff)。 所以,当我们调用square_sum(3, 4)的时候,就相当于:

square_sum = decorator(square_sum)square_sum(3, 4)

我们知道,Python中的变量名和对象是分离的。变量名可以指向任意一个对象。从本质上,装饰器起到的就是这样一个重新指向变量名的作用(name binding),让同一个变量名指向一个新返回的可调用对象,从而达到修改可调用对象的目的。

与加工函数类似,我们可以使用装饰器加工类的方法。

如果我们有其他的类似函数,我们可以继续调用decorator来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

###2、含参的装饰器 在上面的装饰器调用中,比如@decorator,该装饰器默认它后面的函数是唯一的参数。装饰器的语法允许我们调用decorator时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

# a new wrapper layerdef pre_str(pre=''):    # old decorator    def decorator(F):        def new_F(a, b):            print(pre + "input", a, b)            return F(a, b)        return new_F    return decorator# get square sum@pre_str('^_^')def square_sum(a, b):    return a**2 + b**2# get square diff@pre_str('T_T')def square_diff(a, b):    return a**2 - b**2print(square_sum(3, 4))print(square_diff(3, 4))

上面的pre_str是允许参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有环境参量的闭包。当我们使用@pre_str('^_^')调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。该调用相当于:

square_sum = pre_str('^_^') (square_sum)

###3、装饰类 在上面的例子中,装饰器接收一个函数,并返回一个函数,从而起到加工函数的效果。在Python 2.6以后,装饰器被拓展到类。一个装饰器可以接收一个类,并返回一个类,从而起到加工类的效果

def decorator(aClass):    class newClass:        def __init__(self, age):            self.total_display   = 0            self.wrapped         = aClass(age)        def display(self):            self.total_display += 1            print("total display", self.total_display)            self.wrapped.display()    return newClass@decoratorclass Bird:    def __init__(self, age):        self.age = age    def display(self):        print("My age is",self.age)eagleLord = Bird(5)for i in range(3):    eagleLord.display()

在decorator中,我们返回了一个新类newClass。在新类中,我们记录了原来类生成的对象(self.wrapped),并附加了新的属性total_display,用于记录调用display的次数。我们也同时更改了display方法。

通过修改,我们的Bird类可以显示调用display的次数了。

装饰器的核心作用是name binding。这种语法是Python多编程范式的又一个体现。大部分Python用户都不怎么需要定义装饰器,但有可能会使用装饰器。鉴于装饰器在Python项目中的广泛使用,了解这一语法是非常有益的。

##三、内存管理 语言的内存管理是语言设计的一个重要方面。它是决定语言性能的重要因素。无论是C语言的手工管理,还是Java的垃圾回收,都成为语言最重要的特征。这里以Python语言为例子,说明一门动态类型的、面向对象的语言的内存管理方式。

###1、对象的内存使用 赋值语句是语言最常见的功能了。但即使是最简单的赋值语句,也可以很有内涵。Python的赋值语句就很值得研究。

a = 1

整数1为一个对象。而a是一个引用。利用赋值语句,引用a指向对象1。Python是动态类型的语言(参考动态类型),对象与引用分离。Python像使用“筷子”那样,通过引用来接触和翻动真正的食物——对象。

引用和对象:

输入图片说明

为了探索对象在内存的存储,我们可以求助于Python的内置函数id()。它用于返回对象的身份(identity)。其实,这里所谓的身份,就是该对象的内存地址

a = 1print(id(a))print(hex(id(a)))

在我的计算机上,它们返回的是:

11246696'0xab9c68'

分别为内存地址的十进制和十六进制表示。

在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。当我们创建多个等于1的引用时,实际上是让所有这些引用指向同一个对象

a = 1b = 1print(id(a))print(id(b))

上面程序返回:

1124669611246696

可见a和b实际上是指向同一个对象的两个引用

为了检验两个引用指向同一个对象,我们可以用is关键字is用于判断两个引用所指的对象是否相同

# Truea = 1b = 1print(a is b)# Truea = "good"b = "good"print(a is b)# Falsea = "very good morning"b = "very good morning"print(a is b)# Falsea = []b = []print(a is b)

上面的注释为相应的运行结果。可以看到,由于Python缓存了整数和短字符串,因此每个对象只存有一份。比如,所有整数1的引用都指向同一对象。即使使用赋值语句,也只是创造了新的引用,而不是对象本身。长的字符串和其它对象可以有多个相同的对象,可以使用赋值语句创建出新的对象。

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)

我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

from sys import getrefcounta = [1, 2, 3]print(getrefcount(a))b = aprint(getrefcount(b))

由于上述原因,两个getrefcount将返回2和3,而不是期望的1和2。

###2、对象引用对象 Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用

我们也可以自定义一个对象,并引用其它对象:

class from_obj(object):    def __init__(self, to_obj):        self.to_obj = to_objb = [1,2,3]a = from_obj(b)print(id(a.to_obj))print(id(b))

可以看到,a引用了对象b。

对象引用对象,是Python最基本的构成方式。即使是a = 1这一赋值方式,实际上是让词典的一个键值"a"的元素引用整数对象1。该词典对象用于记录所有的全局引用。该词典引用了整数对象1。我们可以通过内置函数**globals()**来查看该词典。

当一个对象A被另一个对象B引用时,A的引用计数将增加1

from sys import getrefcounta = [1, 2, 3]print(getrefcount(a))b = [a, a]print(getrefcount(a))

由于对象b引用了两次a,a的引用计数增加了2。

容器对象的引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系,比如:

x = [1, 2, 3]y = [x, dict(key1=x)]z = [y, (x, y)]import objgraphobjgraph.show_refs([z], filename='ref_topo.png')

输入图片说明

objgraph是Python的一个第三方包。安装之前需要安装xdot。

sudo apt-get install xdotsudo pip install -i http://mirrors.aliyuncs.com/pypi/simple objgraphobjgraph官网

两个对象可能相互引用,从而构成所谓的引用环(reference cycle)。

a = []b = [a]a.append(b)

即使是一个对象,只需要自己引用自己,也能构成引用环。

a = []a.append(a)print(getrefcount(a))

引用环会给垃圾回收机制带来很大的麻烦,我将在后面详细叙述这一点。

###3、引用减少 某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:

from sys import getrefcounta = [1, 2, 3]b = aprint(getrefcount(b))del aprint(getrefcount(b))

del也可以用于删除容器元素中的元素,比如:

a = [1,2,3]del a[0]print(a)

如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少:

from sys import getrefcounta = [1, 2, 3]b = aprint(getrefcount(b))a = 1print(getrefcount(b))

###4、垃圾回收 吃太多,总会变胖,Python也是这样。当Python中的对象越来越多,它们将占据越来越大的内存。不过你不用太担心Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,比如Java和Ruby。尽管最终目的都是塑造苗条的提醒,但不同语言的减肥方案有很大的差异 (这一点可以对比本文和Java内存管理与垃圾回收)。

从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的表:

a = [1, 2, 3]del a

del a后,已经没有任何引用指向之前建立的[1, 2, 3]这个表。用户不可能通过任何方式接触或者动用这个对象。这个对象如果继续待在内存里,就成了不健康的脂肪。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。

然而,减肥是个昂贵而费力的事情。垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中**分配对象(object allocation)取消分配对象(object deallocation)**的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

我们可以通过gc模块的**get_threshold()**方法,查看该阈值:

import gcprint(gc.get_threshold())

返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。

我们也可以手动启动垃圾回收,即使用gc.collect()

###5、分代回收 Python同时采用了分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。 Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

这两个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。

同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。

import gcgc.set_threshold(700, 10, 5)

###6、孤立的引用环 引用环的存在会给上面的垃圾回收机制带来很大的困难。这些引用环可能构成无法使用,但引用计数不为0的一些对象。

a = []b = [a]a.append(b)del adel b

上面我们先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。

孤立的引用环:

输入图片说明

为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。

遍历后的结果:

tupian

在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。

注:本Python学习笔记是按照Vamei的博客教程来学习的,如有兴趣可以参考

转载于:https://my.oschina.net/corwien/blog/689302

你可能感兴趣的文章
yii框架的404、500等异常处理
查看>>
yii框架在layout模式下,模版和layout文件的渲染顺序
查看>>
php5对象复制、clone、浅复制与深复制
查看>>
php设计模式
查看>>
git与github在ubuntu下的使用
查看>>
css pie.htc使用总结
查看>>
python包含中文字符串长度
查看>>
sysbench 0.5 性能测试工具使用手册
查看>>
通过telnet连接查看memcache服务器
查看>>
django不用在数据库中创建新的user表而使用它的后台管理功能
查看>>
php array_unshift()修改数组key
查看>>
mysql性能优化-查询(Query)优化-2
查看>>
MySQL分区表的使用
查看>>
MongoDB 地理位置索引的实现原理
查看>>
MongoDB与MySQL的插入、查询性能测试
查看>>
深入理解OAuth2.0协议
查看>>
https原理:证书传递、验证和数据加密、解密过程解析
查看>>
MySQL在大型网站的应用架构演变
查看>>
sphinx教程1__mysql sphinx引擎插件式热安装
查看>>
sphinx教程2__安装、配置和使用
查看>>