Python进阶 - 闭包与装饰器

一、闭包

1. 什么是闭包?

在一个函数,如下面的函数所示:

def outside_function():
	temp = "Hello"
	print(temp)

我们的程序在运行过这个函数之后,因为temp变量只是一个中间过程的变量,不再被程序需要了,因此其占用的内存会被释放。也就是说,在一个函数执行完成后,函数作用域中的变量会被释放内存。

然而,如果我们在一个函数中再定义另一个函数:

def outside_function():
	temp = "Hello"
	print("This is outside function")
	def inner_function():
		print("This is inner function")
		print(temp)
	return inner_function

由于在Python中一切皆为对象,函数也能够作为对象被return关键字返回,此时在外层函数中定义的inner_function()函数叫做一个闭包。

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

也就是说,在上面的代码中,在正常运行中,运行完outside_function函数后,temp变量的内存会被释放。但因为inner_function函数使用了temp变量,且inner_functionoutside_function函数的返回值。也就意味着,如果用户调用outside_function函数,就可以获得inner_function函数的对象,并以此来调用inner_function函数。可是此时如果temp变量随着outside_function函数的执行完成而释放内存,那么当inner_function函数执行时,就无法再执行和temp有关的指令了。因此,temp变量和inner_function函数被绑定在一起,不会在内存中被释放了。

一个函数要成为闭包,必须满足三个条件:

  1. 要成为闭包的函数嵌套在另外一个函数中
  2. 闭包函数使用了外层函数的变量,该变量叫做自由变量(free variable)
  3. 闭包函数是外层函数的返回值
2. nonlocal关键字

需要注意的是,在内层函数中,并不能改变自由变量的值,且每个闭包并不是对应着同一个自由变量。

def outside_function():
	str = "Hello"
	def inner_function():
		str = "Hi"
	print("Before inner function str = %s" % str)
	inner_function()
	print("After inner function str = %s" % str)
	return inner_function

上面的代码中,虽然在inner_function函数中改变了str变量的值,但前后打印的结果仍然是Hello,而不会变成Hi。如果要在inner_function()中修改str的值,需要添加nonlocal关键字,如下所示:

def outside_function():
	str = "Hello"
	def inner_function():
		nonlocal str
		str = "Hi"
	print("Before inner function str = %s" % str)
	inner_function()
	print("After inner function str = %s" % str)
	return inner_function

这次运行的结果就会变成

# 运行结果
Hello
Hi

我们还需要注意的是,每一个闭包的对象对应的是不同的自由变量,例如下面的例子中,我们首先定义一个闭包函数:

def outside_function():
	test_list = []
	def inner_function(name):
		test_list.append(len(test_list) + 1)
		print("%s %s" % (name, test_list))
	return inner_function

上面的嵌套函数中,外层函数定义了一个列表test_list,内层函数每次会将其添加一个新的数字(它的长度+1)。根据上面闭包的定义,函数嵌套关系存在,内层函数调用了外层函数作用域内的对象,外层函数的返回值是内层函数的对象,因此该内层函数是一个闭包。当上面的函数运行下面的程序片段时:

inner_function1 = outside_fuction()
inner_function2 = outside_fuction()
inner_function1("inner_function1")
inner_function1("inner_function1")
inner_function1("inner_function1")
inner_function1("inner_function1")
inner_function2("inner_function2")
inner_function2("inner_function2")
inner_function2("inner_function2")

我们可以获得的结果为:

# 运行结果
inner_function1 [1]
inner_function1 [1,2]
inner_function1 [1,2,3]
inner_function1 [1,2,3,4]
inner_function2 [1]
inner_function2 [1,2]
inner_function2 [1,2,3]

可以看到,inner_function1inner_function2并不是向同一个test_list中添加整形数值。也就是说,每个闭包对象对应的是不同的自由变量。

二、自由变量

自由变量并不是只在闭包中出现,在普通的函数中也很常见

i = 3
def f(j):
	return i*j

自由变量指的是函数在函数内部的命名域(namespace)没有找到声明的变量,此时Python编译器会在函数外,也就是全局的变量找到该变量的声明。该变量叫做自由变量,也就是上端代码中的i

i = 3
print(f(2))
i = 2
print(f(2))

# 运行结果
6
4

可以看到,非闭包的自由变量,我们对其修改数值,可以改变函数的运行结果

然而,如果是闭包的自由变量,我们修改自由变量的值,却无法修改函数运行的结果

def outside_function():
	i = 3
	def inside_function(j):
		return i*j
	return inside_function

以上的函数来运行下面的代码段:

inside_function = outside_function()
print(inside_function(2))
i = 100
print(inside_function(2))

两次获得的结果都是6,可见改变i的值并不会影响inside_function的运行;原因很明显,因为我们在全局作用域中的i变量和outside_function作用域中的i变量并不是一个变量,因此我们虽然可以修改全局作用域中的i变量,却无法修改函数内部的i变量

三、装饰器

Python的装饰器本质上是一个函数或一个类,功能是在不需要修改原代码的基础上添加新的功能。装饰器应用的功能有很多,一般有插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们可以将与函数本身无关的代码抽离出来,并把这部分代码也实现重用。

我们首先定义一个函数,这个函数是判断一个给定的数字是否是素数:

def isPrime(num):
	if num == 2:
		return True
	else:
		for i in range(2,num):
			if num % i == 0:
				return False
		return True

现在我们想看看这个函数执行的时间是多少,该如何实现呢?一种方法是直接在原函数上进行修改:

import time

def isPrime(num):
	tic = time.clock()
	if num == 2:
		return True
	else:
		for i in range(2,num):
			if num % i == 0:
				toc = time.clock()
				print("运行时间为%.2f" % (toc-tic))
				return False
		toc = time.clock()
		print("运行时间为%.2f" % (toc-tic))
		return True

但是这样的缺点很明显,我们的函数isPrime本来是判断给定数字是否是素数的,现在混入了大量无关的time模块的代码。在这里使用嵌套的函数,即可把time模块与函数本身的功能分离开:

def count_time(num):
	tic = time.clock()
	flag = isPrime(num)
	toc = time.clock()
	print("运行时间为%.2f" % (toc-tic))
	return flag

def isPrime(num):
	if num == 2:
		return True
	else:
		for i in range(2,num):
			if num % i == 0:
				return False
		return True

if __name__ == "__main__":
	wrapper = count_time(10)

但是这样,我们的count_time函数就和isPrime函数绑定了,但也许我们的其他函数也有这样的需求,这样设计的结果使得代码中产生大量的count_time函数,依旧造成了大量重复的代码。如果我们把需要记录时间的函数作为一个对象传入到count_time函数中,那么我们就可以实现对任何函数实现记录运行时间的功能了。

def count_time(func,*args,**kwargs):
	tic = time.clock()
	result = func(*args, **kwargs)
	toc = time.clock()
	print("运行时间为%.2f" % (toc-tic))
	return result
	# 如果没有返回值会返回None

在这里,我们可以应用到第一部分所讲的闭包,来把功能封装到一个内层函数中,并把func的参数传到内层函数中。当我们需要调用内层函数时,就调用count_time函数返回内层函数对象。这样,count_time函数只需要传递一个需要调用的函数对象func,便可以返回内层函数了,这也就是装饰器的思想:

def count_time(func):
	# 内层函数
	def wrapper(*args, **kwargs):
		tic = time.clock()
		result = func(*args,**kwargs)
		toc = time.clock()
		print("运行时间为%.2f" % (toc-tic))
		return result
	return wrapper

从代码来看,首先函数形成嵌套,且内层函数wrapper调用了外层函数count_time作用域内的对象func,且外层函数的返回值是内层函数,因此根据前文我们对闭包的判断条件,该函数形成了闭包。

那么在调用的时候,我们该如何使用呢?按照正常的闭包用法,我们应该:

num = 10
wrapper = count_time(isPrime)
result = wrapper(num)

也就是说,我们每次调用函数,都需要调用一次count_time函数来获取内层函数,并将func对象与wrapper函数进行绑定;并且,我们需要对返回的内部函数进行命名,如果将其命名为wrapper的话,不同的函数之间会造成混淆,如果将其命名为isPrime的话,又会与原函数造成混淆。如果我们能直接调用原函数就好了!

在Python的装饰器中,就提供了这样的语法来帮助我们。当我们定义isPrime的时候,如果我们需要对其进行记录时间的操作,在定义完count_timewrapper闭包函数后,我们只需要对isPrime函数添加@count_time注解即可

@count_time
def isPrime(num):
	if num == 2:
		return True
	else:
		for i in range(2,num):
			if num % i == 0:
				return False
		return True

这样,在调用的时候,只需要直接调用isPrime函数即可,即

num = 10
result = isPrime(num)

并且依旧可以实现记录时间的功能

四、Python的内置装饰器

Python提供了三个内置的装饰器,分别为@property@classmethod@staticmethod

1. @property

@Property装饰器可以把一个类中的方法变为同名的属性,主要应用在类的定义中。

例如,我们定义一个Student类,其中有一个分数属性score,那么从外部可以直接访问并修改,导致了学生的分数可以任意的修改,十分不安全。在Java或其他面向对象语言中,我们的解决方法是把score设置为私有属性,并设置setter来更新数据,设置getter来访问数据,在Python中,我们也可以做相同的操作,且在更新成绩时判断,输入的值是否为0-100之间的数值

class Student:
	def __init__(self,score)
		self._score = score

	def getScore(self):
		return self._score

	def setScore(self,score):
		if 0 <= score <= 100:
			self._score = score
		else:
			raise ValueException("成绩必须在0到100之间!")

在Python中,通过使用提供的@property装饰器,我们可以实现通过直接访问来修改数值,并仍然让数据可控

class Student:
	def __init__(self,score)
		self._score = score

	@property
	def score(self):
		return self._score

	@score.setter
	def score(self,value):
		if 0 <= score <= 100:
			self._score = value
		else:
			raise ValueException("成绩必须在0到100之间!")

我们对变量的getter添加@property装饰器,添加后可以继续对另一个同名函数添加@score.setter装饰器来定义修改数据的方式

我们通过直接访问参数即可,并且可以对输入的数据进行把控

s = Student(80)
s.score = 60
print(s.score)
s.score = 9999

运行的结果为

60
Traceback (most recent call last):
  ...
ValueError: 成绩必须在0到100之间!

当然,@score.setter装饰器不是必须添加的,如果不实现同名方法,并添加setter装饰器的注解的话,该属性就会变为只读属性,不能从外界被访问

例如,一个长方形的属性有长和宽,还有属性周长和面积。一个长方形在被创造的时候长和宽就被确定了,因此修改周长和面积是没有意义的,我们就可以把周长和面积不添加setter的装饰器,让其作为只读属性

class Rectangle:
	def __init__(self,length,width):
		self._length = length
		self._width = width

	@property
	def area(self):
		return self._length * self._width

	@property
	def perimeter(self):
		return 2 * (self._width + self._length)

在我们使用的时候,便可以直接通过方法名来访问参数了:

r1 = Rectangle(8,6)
print(r1.area)
print(r1.perimeter)
r1.area = 80

会有运行结果:

48
28
Traceback (most recent call last):
  File "test.py", line 17, in 
    r1.area = 80
AttributeError: can't set attribute
2. @classmethod

@classmethod装饰器会让一个方法返回其所在的类本身。由于Python不支持构造函数的重载,因此,@classmethod装饰器可以实现构造函数的重载;@classmethod还可以替代工厂方法,让普通类具有其它语言中工厂类的功能

@classmethod装饰器所修饰的方法不需要对象实例化,也不需要self参数,而需要代表自身类的cls参数,当然,cls只是一个约定俗成的名字,也可以叫this或任何其他名字。这类方法可以在实例化之前就被调用,因为有时在实例化之前可能需要调用该类的一些方法,该方法的返回值会影响类的实例化过程。

就像前面所说的,@classmethod可以实现构造方法的重构:

class Date:
	def __init__(self,year = 0,month = 0, day = 0):
		self.year = year
		self.month = month
		self.day = day

	@classmethod
	def construct_from_string(cls,date_string):
		year, month, day = date_string.split("-")
		return cls(year,date,string)

@classmethod还可以在类实例化之前对类做出一些判断,来影响之后的实例化过程。例如我们的模块在实例化之前,首先需要判断和其他模块的版本是否兼容:

class Module:
	def __init__(self,compatible_version):
		self.compatible_version = compatible_version

	@classmethod
	def isCompatible(cls,other_version):
		if other_version in compatible_version:
			return True
		else: 
			return False

@classmethod的第三个功能和类的继承有关。通常,我们在子类中可以重写一个方法,来添加基类中没有的功能。但是如果我们的基类想针对不同的子类在某些方法中做出不同的处理,则需要添加@classmethod装饰器:

class Person:
	def __init__(self,name):
		self.name = name

	def introduce(self):
		print("My name is %s" % self.name)

	@classmethod
	def introduce_job(cls):
		if cls.__name__ == "Police":
			print("I'm a police.")
		elif cls.__name__ == "Doctor":
			print("I'm a doctor")
	
class Police(Person):
	pass
class Doctor(Person):
	pass

下面我们分别实例化Police类和Doctor类,来调取introduce_job方法:

police = Police("Bob")
doctor = Doctor("Peter")
police.introduce()
police.introduce_job()
doctor.introduce()
doctor.introduce_job()

运行结果为:

My name is Bob
I'm a police.
My name is Peter
I'm a doctor
3. @staticmethod

@staticmethod装饰器会把一个方法标记为静态方法,即不需要实例化,直接调用类名来访问的方法;在Python中,实例化的对象也可以调用静态方法。由于在面向对象编程中比较常见,不再赘述了。

静态方法不需要传self参数或cls参数,只传入方法需要的参数即可

class Test:

	@staticmethod
	def average(arr):
		return sum(arr) / len(arr)

arr = [1,2,3,4,5,6,7,2,3,4,2,3]
print(Test.average(arr))

test = Test()
print(test.average(arr))

两种方式调用都会返回相同的结果3.5。

参考资料

[1] Python: 从闭包到装饰器
[2] 理解Python闭包概念
[3] Python系列之闭包
[4] Python 3函数自由变量的大坑
[5] Python小技巧:装饰器(Decorator)
[6] Python中内置装饰器的使用
[7] 使用@property
[8] Python @classmethod
[9] Python进阶(六):@classmethod和@staticmethod


我和几位大佬建立了一个微信公众号,欢迎关注后查看更多技术干货文章 Python进阶 - 闭包与装饰器_第1张图片

你可能感兴趣的:(Python进阶)