31天Python入门——第11天:挑战一口气把闭包·装饰器讲明白

在这里插入图片描述

你好,我是安然无虞。

文章目录

    • 1. 闭包
      • 扩展知识: 闭包的自由变量是如何存储的
    • 2. 装饰器
      • 装饰器的应用场景
    • 3. 补充练习

在这里插入图片描述

1. 闭包

闭包是指在一个函数内部定义的函数,并且这个内部函数可以访问外部函数的变量、参数.换句话说,闭包是一个包含了函数及其相关引用环境的组合体.

在Python中,当一个函数返回了内部函数的引用时,这个内部函数可以访问并操作外部函数的局部变量,它就创建了一个闭包, 即使外部函数已经执行完毕,它的局部变量仍然可以被内部函数所使用.

def outer(out_num):

    def inner(in_num):
        print('out', out_num)
        print('in', in_num)
        print("他们的和是:", out_num + in_num)

    return inner

# 此时f已经持有了inner函数的引用, 形成了闭包.
f = outer(10)
f(5) # 依然可以使用外部函数的局部变量
# 10
# 5
# 15

闭包的主要特点是可以将数据和行为捆绑在一起,形成一个独立的环境,而不会与其他部分的代码发生冲突。通过闭包,我们可以实现一些高级的编程技巧,如在函数内部创建私有变量、实现函数工厂或装饰器等.

def outer(out_num):

    def inner(in_num):
        print("他们的和是:", out_num + in_num)

    return inner


# 此时f已经持有了inner函数的引用, 形成了闭包.
f = outer(10)
f(5) # 15
f(15) # 25
print('-' * 20)

f1 = outer(100)
f1(5) # 105
f1(15) # 115

# 每一个闭包实例都持有自己独立的外部函数参数(会消耗一定的内存)
# 并且在内部函数调用的时候都可以准确地计算出结果, 说明闭包在保持状态和数据封装的强大能力.

总结:

  1. 闭包可以保证数据安全.
  2. 内层函数对外层函数非全局变量的引用(只能是局部变量的引用), 就会形成闭包.
  3. 内部函数可以访问外部函数的变量和参数,即使外部函数已经执行完毕.
  4. 外部函数的局部变量在闭包中被保存,形成了一个私有的环境.
  5. 闭包可以捕获和保留外部函数的状态信息,使得函数的状态得以保持和更新.
  6. 被引用到的非全局变量也称自由变量, 这个自由变量会与内层函数产生一个绑定关系.

闭包是一种强大的编程技术,它可以使代码更加模块化、可维护和可重用.通过使用闭包,我们可以在函数内部创建和维护状态,而不必依赖全局变量或其他复杂的机制. 这使得我们的代码更加清晰、可靠,并且更容易扩展和修改.

案例1: 幂的计算

 def power(exponent):
 	 """进行幂的计算"""
     def calculate_power(base):
         return base ** exponent

     return calculate_power
   
 # 求平方
 # square = power(2)
 # num = square(5)
 # num1 = square(8)
 # num2 = square(11)
 # print(num)
 # print(num1)
 # print(num2)

 # 求立方
 # cube = power(3)
 # num3 = cube(5)
 # num4 = cube(6)
 # print(num3)
 # print(num4)

案例2: 计算每天的平均开销.

def avg():
    num_list = []

    def inner(num):
        num_list.append(num)
        print(num_list)
        print(sum(num_list) / len(num_list))
    return inner

# avg1 = avg()
# avg1(100) # 100
# avg1(200) # 150
# avg1(300) # 200

案例3: 模拟购物车.

def shopping_cart():
    # 购物车的清单列表.
    items = []

    def add_item(item):
        items.append(item)
        print(f'已添加到购物车: {item}')

    def get_items():
        return items

    return add_item, get_items


usr1_cart, usr1_get = shopping_cart()
usr2_cart, usr2_get = shopping_cart()

# usr1_cart('手机')
# usr1_cart('耳机')
# usr1_cart('相机')
#
# usr2_cart('苹果')
# usr2_cart('橘子')
# usr2_cart('香蕉')

# usr1_items = usr1_get()
# print(usr1_items)
# usr2_items = usr2_get()
# print(usr2_items)

第一个案例和后面两个案例的区别是什么:

区别在于外层函数是否接收参数.

第一个案例中外部函数接受参数, 所以它既可以计算平方也可以计算立方, 甚至可以计算任意次幂.

外部函数接收参数所形成的闭包. 更加的灵活, 可以创建具有不同状态的闭包.

但是外层函数不接收参数的闭包, 它的状态是固定的.

总结: 外部函数是否接收参数决定了闭包的灵活性和定制性.

扩展知识: 闭包的自由变量是如何存储的

外部函数中被引用到的非全局变量也称自由变量, 这个自由变量会与内层函数产生一个绑定关系.
自由变量指的是在外部函数中定义的局部变量 被 在内层函数中引用到, 此时这个内层函数中引用到的变量就是自由变量.

def outer(out_num, n):

    def inner(in_num):
        print("他们的和是:", out_num + in_num)
        print(n)

    return inner


# 此时f已经持有了inner函数的引用, 形成了闭包.
f = outer(10, 'str') # 创建闭包实例
closure = f.__closure__
print(closure) # 元组

for i in closure:
  print(i.cell_contents) 

# (, )
# str
# 10
  
  
# ---------------------------------------
def outer(out_num):

    def inner(in_num):
        pass

    return inner

# 此时f已经持有了inner函数的引用, 形成了闭包.
f = outer(10) # 创建闭包实例
closure = f.__closure__
print(closure) # None

2. 装饰器

Python装饰器是一种用于修改函数或类行为的特殊函数. 它们提供了一种简洁的方式来对现有函数或类进行扩展、修饰或包装,而无需修改它们的源代码. 装饰器通常用于在不影响(不改变)原始函数或类定义的情况下,添加额外的功能或行为.

装饰器本质上是一个函数,它接受一个函数作为输入,并返回一个新的函数. 装饰器函数通常在内部定义一个闭包函数,用于包装原始函数,添加额外的逻辑或功能. 装饰器函数可以访问原始函数的参数和返回值,并可以在调用前后执行自定义的操作.

装饰器提供了一种简洁而灵活的方式来修改函数的功能,使得我们可以在不改变原代码的情况下添加额外的功能. 用一句话来说就是: 装饰器就是创建一个闭包函数, 在闭包函数的内部调用目标函数, 然后添加相应的功能.

import time
# 计算calculate_sum运行所花费的时间

def cost_time(func):
  def inner():
    start = time.time()
    func()
    end_time = time.time()
    print(end_time - start_time)
  return inner

def calculate_sum():
  total = 0
  for i in range(1, 100000001):
    total += i
  print(total)
  
# 时间戳: 1970.1.1 00:00:00 UTC时间 开始 一直到现在的秒数
calculate_sum = cost_time(calculate_sum)
calculate_sum()

注意比较这行代码和下面使用@符号修饰的装饰器函数:
calculate_sum = cost_time(calculate_sum)

装饰器的语法:

在Python中,装饰器是通过使用@符号来应用的. 它可以直接放在函数的定义之前,用于修饰该函数.

以下是装饰器的基本语法:

 @decorator
 def func():
  # 函数定义和实现
  pass

用Python当中的装饰器改写上面的代码:

import time

def cost_time(func):
  def inner():
    start_time = time.time()
    func()
    end_time = time.time()
    print(end_time - start_time)
  return inner

@cost_time   # 等价于 calculate_sum = cost_time(calculate_sum)
def calculate_sum():
  total = 0
  for i in range(1, 10000001):
    total += i
  print(total)
  
calculate_sum()
# print(calculate_sum)发现这个函数指向的是闭包中的inner
@cost_time   # 等价于 calculate_sum = cost_time(calculate_sum)
def calculate_sum():
	...

上面的 calculate_sum()函数被装饰器函数cost_time修饰后, 此时calculate_sum函数其实就相当于闭包中的inner函数, 通过上面的print(calculate_sum)就可以看出来.

看下面的代码:

import time

def cost_time(func):
  def inner():
    start_time = time.time()
    func()
    end_time = time.time()
    print(end_time - start_time)
  return inner

@cost_time   # 等价于 calculate_sum = cost_time(calculate_time)
def calculate_sum():
  """求1-10000000的和"""
  total = 0
  for i in range(1, 10000001):
    total += i
  return total
  
total = calculate_sum() # inner函数没有返回值
print(total) # 这样写肯定是有问题的, 所以怎么改呢?

# -------------------------------------------------
# 很简单, 这样改动即可:
def cost_time(func):
  def inner():
    start_time = time.time()
    result = func()
    end_time = time.time()
    print(end_time - start_time)
    return result
  return inner

有一个问题是: 上面是计算固定数字的和, 那我们怎么计算指定数字的和呢?

import time

def cost_time(func):
  def inner(num):
    start_time = time.time()
    result = func(num)
    end_time = time.time()
    print(end_time - start_time)
    return result
  return inner

@cost_time   # 等价于 calculate_sum = cost_time(calculate_time)
def calculate_sum(num):
  """求1-10000000的和"""
  total = 0
  for i in range(1, num + 1):
    total += i
  return total
  
total = calculate_sum(10000001) # -> 现在这个函数其实就是inner函数

还有一个问题是, 我们想要传入2个参数呢, 或者3个参数呢?

聪明的你肯定能想到使用前面函数教程中所说的 不定长参数.

import time

def cost_time(func):
  def inner(*args, **kwargs):
    start_time = time.time()
    result = func(*args, **kwargs)
    end_time = time.time()
    print(end_time - start_time)
    return result
  return inner

@cost_time
def add(a, b):
  time.sleep(1)
  return a + b

total = add(100000,777)
print(total)

现在思考: 如何给装饰器传递参数呢?

比如上面的装饰器 cost_time 如果想传递参数该怎么做呢?

import time

# 计算 calculate_sum 运行所花费的时间.

def outer(is_need_ret_val):
    """
    :param is_need_ret_val: 是否需要返回值, true表示需要, false表示不需要.
    :return:
    """

    def cost_time(func):
        def inner(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            if not is_need_ret_val:
                # 如果不需要返回值, 那么就直接打印.
                print('函数运行的结果是:', result)
                print('函数运行所花费的时间:', end_time - start_time)
            else:
                return result, end_time - start_time

        return inner

    return cost_time


@outer(True) # outer()会返回cost_time, 不就和之前的 @cost_time 相同了吗
# @cost_time # 等价于calculate_sum = cost_time(calculate_sum)
def calculate_sum(num):
    """求1-指定数字的和"""
    total = 0
    for i in range(1, num + 1):
        total += i
    return total # 实际上面total返回的类型是元组


# 时间戳: 1970.1.1 00:00:00 utc时间 开始 一直到现在的秒数.
# calculate_sum = cost_time(calculate_sum)
total = calculate_sum(10000000)
print(total)

# ---------------------------------------------

# 打印出的结果:
(50000005000000, 0.556506872177124)

注意事项: 在使用装饰器时,需要注意以下几点:

  • 装饰器只会在被装饰函数定义时执行一次,而不会在每次函数调用时执行.
  • 装饰器的顺序很重要,多个装饰器的执行顺序是从上到下的.
import time

def cost_time(func):
    print("cost_time装饰器正在装饰")
    def inner(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print('函数运行所花费的时间:', end_time - start_time)
        return result
    return inner


@cost_time  # 等价于calculate_sum = cost_time(calculate_sum)
def calculate_sum(num):
    """求1-指定数字的和"""
    total = 0
    for i in range(1, num + 1):
        total += i
    return total


total = calculate_sum(10000000)
total1 = calculate_sum(5000)
total2 = calculate_sum(4000)
print(total)
print(total1)
print(total2)

# --------------------------------------------
# 执行结果:

cost_time装饰器正在装饰
函数运行所花费的时间: 0.5955467224121094
函数运行所花费的时间: 0.0002911090850830078
函数运行所花费的时间: 0.0002238750457763672
50000005000000
12502500
8002000

由此可见, 装饰器只会在被装饰函数定义时执行.

def decorator1(func):
    print("装饰器1正在装饰")
    def wrapper():
        print("Decorator 1 - Before function execution")
        func()
        print("Decorator 1 - After function execution")
    return wrapper


def decorator2(func):
    print("装饰器2正在装饰")
    def wrapper():
        print("Decorator 2 - Before function execution")
        func()
        print("Decorator 2 - After function execution")
    return wrapper


@decorator1  # my_function = decorator1(my_function) 这里的函数对象是刚才decorator2()返回的结果(先装饰decorator2)
@decorator2  # my_function = decorator2(my_function)
def my_function():
    print("Inside my_function")


my_function()
# print(my_function)

# -------------------------------------------------
# 执行结果是:

装饰器2正在装饰
装饰器1正在装饰
Decorator 1 - Before function execution
Decorator 2 - Before function execution
Inside my_function
Decorator 2 - After function execution
Decorator 1 - After function execution

# 注意看上面的执行结果

# 当有多个装饰器装饰的时候
# 装饰的顺序从下到上, 执行的顺序从上到下 - 先装饰, 再执行

装饰器的应用场景

装饰器在实际开发中有许多应用场景,以下是一些常见的例子:

  • 添加日志:可以使用装饰器在函数调用前后记录日志信息,方便调试和追踪问题
  • 计时器:可以使用装饰器来测量函数的执行时间,用于性能分析和优化
  • 缓存:可以使用装饰器来缓存函数的结果,提高重复调用时的性能
  • 权限验证:可以使用装饰器来验证用户的身份和权限,实现访问控制
  • 输入验证:可以使用装饰器来验证函数的输入参数,确保其符合要求

3. 补充练习

练习一: 编写一个装饰器函数, 用于记录函数的调用次数

# 编写一个装饰器函数,用于记录函数的调用次数。

def call_count(func):
    count = 0

    def wrapper(*args, **kwargs):
        nonlocal count
        result = func(*args, **kwargs)
        count += 1 # 修改了外层的局部变量
        print(f'{func.__name__}函数调用了{count}次')
        return result
    return wrapper


@call_count
def square(num):
    print(f"正在计算{num}的平方")
    return num ** 2


# square(1)
# square(2)
# square(3)
# square(4)
# square(5)

练习二: 实现一个装饰器函数, 用于缓存函数的执行结果.

# 实现一个装饰器函数, 用于缓存函数的执行结果.
from functools import lru_cache

def outer(func):
  cache = {}
  
  def wrapper(*args, **kwargs):
    # 定义元组不可变
    cache_key = (args, tuple(sorted(kwargs.items()))) # -> ( (), () )
    if cache_key in cache:
      return cache.get(cache_key)
    
    result = func(*args, **kwargs)
    cache[cache_key] = result
    return result
  return wrapper

@outer
def square(num):
  print(f"正在计算{num}的平方")
  return num ** 2

a = square(5)
b = square(5)

c = square(6)
d = square(6)
print(a)
print(b)
print(c)
print(d)

# 查看上面的打印结果就可以了

# 其实在Python中是已经实现了这样的装饰器的, 所以可以直接这样:
@call_count
def square(num):
  print(f"正在计算{num}的平方")
  return num ** 2

根据练习一和练习二有一个问题是, 为什么练习二中没有执行 nonlocal cache, 这是因为练习一中的count = 0是不可变的数据类型, 而cache = {} 是可变的数据类型.

练习三: 创建一个装饰器函数, 使被装饰的函数只能在特定的时间段内执行

比如在早上9点 -> 晚上6点之间, 可以调用, 其余时间不允许调用 -> 所以需要给装饰器传递时间参数 -> 所以需要定义为三层

import datetime


def time_limited(start, end):

    def outer(func):
        def inner(*args, **kwargs):
            now = datetime.datetime.now().time()
            if start < now < end:
                result = func(*args, **kwargs)
                return result
            else:
                print(f"对不起, 该时间段无法调用此接口.")
        return inner
    return outer


# @time_limited(start=datetime.time(9, 0), end=datetime.time(18, 0))
@time_limited(start=datetime.time(18, 0), end=datetime.time(23, 59))
def test():
    print('我只能在特定的时间段内执行.')


test()

练习四:
某公司要开发一个 系统, 这个系统运行的时候, 用户输入指令,比如,查询价格,就由相应的查询价格的业务代码去处理

假设你是系统架构师, 你确定了如下的代码文件结构

  • Python库模块文件名为 lib.py

里面包含一个装饰器函数 cmd_dispatch, 这个装饰器函数是给 开发业务工程师使用的。

  • 业务代码放在svc.py文件中

在这个代码文件中,开发业务代码的工程师,可以像下面这样使用你的装饰器 cmd_dispatch

# ** svc.py **
from lib import cmd_dispatch

@cmd_dispatch('查询价格')
def queryPrice():
    # 具体的查询价格处理代码
    print('执行查询价格的业务')


@cmd_dispatch('退货')
def refund():
    # 具体的退货处理代码
    print('执行退货的业务')

装饰器参数是 用户指令字符串

比如,上面的写法就表示,如果用户输入指令 查询价格 , 系统就会调用 queryPrice, 用户输入 退货 , 系统就会调用 refund。

启动代码文件是run.py。
我们运行这个run.py 就启动了整个系统。

代码如下

# ** run.py **

# cmd_route_table 是 lib模块里面的
# 存储了命令对应的业务处理函数
from  lib import  cmd_route_table
import svc

while True:
    op = input('请输入你的操作:')

    # 如果能找到该指令的处理函数
    if op in cmd_route_table:
        # 查找该指令对应的业务处理函数
        svcFunc = cmd_route_table[op]
        # 调用业务处理函数
        svcFunc()

    else:
        print('该指令没有对应业务处理')

请你写出 lib.py这个库, 完成上述的功能,包括装饰器函数 和 cmd_route_table

## lib.py

cmd_route_table = {}

def cmd_dispatch(cmd):
	def inner(func):
		cmd_route_table[cmd] = func
		return func
		
	return inner
遇见安然遇见你,不负代码不负卿。
谢谢老铁的时间,咱们下篇再见~

你可能感兴趣的:(Python手把手教程,python,开发语言,后端,pyqt)