python学习(6)之函数进阶(嵌套函数,高阶函数,递归函数,命名空间,闭包,装饰器,生成器,迭代器)

2.4 函数进阶

2.4.1 嵌套函数

函数的嵌套:在函数里面还有函数。其机构可以分为外函数和内函数。嵌套函数是为函数内部服务的,比如减少代码的重复,想要调用函数,要使用函数名,内函数也一样。使用方法和普通的函数调用类似,如果不用函数名调用内函数,内函数就永远不会执行。

如下例所示:

def func1():
    print("这是外部函数")
    def func2():
        print("这是内部函数")
    func2()  # 函数名调用内函数
func1()

也可以用return调用内函数。在使用return之前,需要说明一点:使用"函数名()“调用函数的方法,单独使用"函数名”,会得到函数的内存地址。如下:

def func1():
	print("这是外部函数")
	def func2():
		print("这是内部函数")
	return func2()
func1()

这里可以玩一个骚操作,但在现实中不建议这样编写代码,会引起不必要的误会

def func1():
    print("这是外部函数")
    def func2():
        print("这是内部函数")
    return func2
func1()()  # 执行func1()会返回func2内存地址然后再上()相当于再次使用func2()

外函数里面的变量和内函数里面的变量是有区别的,作用范围不一样。内函数也可以使用外函数的变量,但是如果想要在内部函数修改外部函数变量的值,就要使用关键字nonlocal

def func1():
    b = 2
    def func2():
        b = 3 # 相当于在内部函数中定义了一个局部变量 只是名字和外部函数中的一个变量名字相同 是一个全新的变量 结束内部函数后 该变量会被释放
    func2()
    print(b) # 输出2
func1()
def func1():
    b = 2
    def func2():
        nonlocal b
        b = 3 # 这里相当于继续使用外部函数中的变量 内部函数结束后 对于该变量进行的操作会得以保留
    func2()
    print(b) # 输出3
func1()
2.4.2 高阶函数

首先需要了解高阶函数的定义:一个函数允许函数作为参数传递给其他函数,或者作为返回值从函数中返回。有定义可以看出上述两个条件任意满足一个就可以称之为高阶函数。原理:在Python中,函数作为第一类对象的概念,这意味着函数可以像其他数据类型一样进行操作,可以被当作变量一样进行操作,包括作为参数传递给其他函数,或者作为返回值从函数中返回。

2.4.2.1 高阶函数之函数作为参数传递

函数作为参数传递在高阶函数中的使用是最为常用的,在python的开发中,经常使用的一些函数其实就是python中的高阶函数,如map(),filter()等。如下文举例所示:

def square(x):
    return x * x
numbers = [1, 2, 3, 4, 5]
squares = map(square, numbers)
print(list(squares))  # 输出:[1, 4, 9, 16, 25]

在例子中,map()函数就是python中官方自带的高阶函数,下文会写一个自己的高阶函数对这部分加深理解。

def square(x):
    return x * x
def square_add(x, y, f):
    return f(x) + f(y)
print(square_add(2, 3, square))

可以看到square_add()这个函数有3个形参,其中前两个为数据变量参数,最后一个参数为函数变量参数,在该函数调用的时候可以看出square_add(2, 3, square)中的square只是一个函数名称(函数在内存中的地址)并没有运行函数,函数真正的调用是 return f(x) + f(y),这句代码才是真正调用了square()函数,最后square_add()函数只是把两个调用square()函数的返回值相加起来。

2.4.2.2 高阶函数之函数作为返回值返回

高阶函数可以在函数内部定义并返回另一个函数。这种用法常常用于创建闭包,即一个带有捕获的外部变量的函数。

例如,可以定义一个函数make_adder(),用于生成一个可以实现加法的函数:

def make_adder(x):
    def add(y):
        return x + y
    return add

add_5 = make_adder(5)
print(add_5(3))  # 输出:8

在上面的例子中,make_adder()函数接受一个参数x(x=5),并返回一个新的函数add(),赋给了一个新的函数变量add_5,其中这个变量就是add(y)这个函数,但是与之不一样的一点是:add(y)函数是return x+y,而add_5这个函数是return 5+y也就是说,在原来函数中的x的值现在全部由刚才 make_adder(5)函数中传入的形参决定也就是5。

2.4.2.3 高阶函数的应用

1.函数式编程是一种编程范式,其中函数可以被传递和返回。高阶函数在函数式编程中被广泛使用,可以编写更加简洁、高效的代码。例如,可以使用内置函数filter()来筛选出符合某一条件的元素:

# is_even()函数作为参数传递给filter()函数,用于筛选出numbers列表中的偶数。
def is_even(x):
    return x % 2 == 0
numbers = [1, 2, 3, 4, 5]
evens = filter(is_even, numbers)
print(list(evens))  # 输出:[2, 4]

2…高阶函数与装饰器联合使用,后续章节会详细阐述,现在可以举例如下:

'''在下面的例子中,timer函数是一个高阶函数,它接受一个函数作为参数,并返回一个新的函数wrapper,这个新的函数在执行时会计算函数的执行时间并输出。

装饰器@timer 当调用slow_function()时,实际上是调用了经过装饰器timer修饰后的wrapper函数,从而实现了计算函数执行时间的功能。'''
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print("函数 {} 的执行时间为 {} 秒".format(func.__name__, end_time - start_time))
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(2)

slow_function()  # 输出:函数 slow_function 的执行时间为 2.0088157653808594 秒

3.回调函数

回调函数是一种将函数作为参数传递给其他函数,并在特定事件发生时由其他函数调用的技术。高阶函数可以用于实现回调函数。有点类似于C语言的函数指针的感觉,根据不同的条件,来选择不同的处理函数,对数据进行处理运算,举例如下:

例如,我们可以定义一个函数process_data(),它接受一个数据列表和一个处理函数作为参数,然后将列表中的每个元素传递给处理函数进行处理:

# process_data()函数接受一个数据列表和一个处理函数func作为参数,然后将列表中的每个元素传递给func进行处理,并将处理结果存入一个新的列表中
def process_data(data, func):
    result = []
    for item in data:
        processed_item = func(item)
        result.append(processed_item)
    return result
def square(x):
    return x * x
def cube(x):
    return x * x * x
data = [1, 2, 3, 4, 5]
squared_data = process_data(data, square)
print(squared_data)  # 输出:[1, 4, 9, 16, 25]
cubed_data = process_data(data, cube)
print(cubed_data)  # 输出:[1, 8, 27, 64, 125]
2.4.3 递归函数

如果一个函数在内部调用自身本身,这个函数就是递归函数。

递归函数具有以下特性:

  1. 必须有一个明确的结束条件;

  2. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少

  3. 相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入)

  4. 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出)

    所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。如下文举例所示:

    # 循环方式
    def sum_cycle(n):
        sum = 0
        for i in range(1, n + 1):
            sum += i
        print(sum)
    # 递归方式
    def sum_recu(n):
        if n > 0:
            return n + sum_recu(n - 1)
        else:
            return 0
    sum_cycle(100)
    sum = sum_recu(100)
    print(sum)
    

    使用递归函数需要注意防止栈溢出。函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。在python中默认栈调用的个数是1000个,如果使用的递归函数的递归次数太多那就会出现递归调用错误。

    递归其实在很多算法中都有应用比如推排序,快排等,一般需要较强的数学功底,最好把底层数学的递推式给出,我这里举一个小例子。

    # 二分查找
    def  Binary_Search(data_source,find_n):
        #判断列表长度是否大于1,小于1就是一个值
        if len(data_source) >= 1: 
            #获取列表中间索引;奇数长度列表长度除以2会得到小数,通过int将转换整型     
            mid = int(len(data_source)/2) 
            #判断查找值是否超出最大值   
            if find_n > data_source[-1]:                                    
                print('{}查找值不存在!'.format(find_n))
                exit()
            #判断查找值是否超出最小值
            elif find_n < data_source[0]:                                   
                print('{}查找值不存在!'.format(find_n))
                exit()
            #判断列表中间值是否大于查找值
            if data_source[mid]  > find_n:                                  
                print('查找值在 {} 左边'.format(data_source[mid]))
                #调用自己,并将中间值左边所有元素做参数
                Binary_Search(data_source[:mid],find_n)  
            #判断列表中间值是否小于查找值                   
            elif data_source[mid] < find_n:                                 
                #print('查找值在 {} 右边'.format(data_source[mid]))     
                #调用自己,并将中间值右边所有元素做参数      
                Binary_Search(data_source[mid:],find_n)
            else:
                #找到查找值
                print('找到查找值',data_source[mid])                          
        else:
            #特殊情况,返回查找不到
            print('{}查找值不存在!'.format(find_n))                          
     
    Data = [22,12,41,99,101,323,1009,232,887,97]
    #列表从小到大排序
    Data.sort()       
    #查找323                                                      
    Binary_Search(Data,323)   
    

    解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的

    尾递归:基于函数的尾调用。每一级调用直接返回函数的返回值更新调用栈,而不用创建新的调用栈, 类似迭代的实现, 时间和空间上均优化了一般递归。如举例所示:

    # 一般递归
    def normal_recursion(n):
        if n == 1:
            return 1
        else:
            return n + normal_recursion(n-1)
    
    # 尾递归
    def tail_recursion(n, total=0):
        if n == 0:
            return total
        else:
            return tail_recursion(n-1, total+n)
    '''
    tail_recursion(5)
    tail_recursion(4, 5)
    tail_recursion(3, 9)
    tail_recursion(2, 12)
    tail_recursion(1, 14)
    tail_recursion(0, 15)
    15
    '''
    

    如代码中所示normal_recursion()一般递归函数的调用是,没有退栈的,只有压栈,最后是退出该递归函数后,才进行压栈。但是tail_recursion()如尾递归函数所示每一级调用直接返回函数的返回值,从而更新调用栈,而不用创建新的调用栈,类似于线性的调用的形式。

    遗憾的是:虽然尾递归优化很好, 但python 不支持尾递归,递归深度超过1000时还是会报错。

    但是有位大哥写出了以下代码,就可以在python中实现尾递归的方法,如下:

    import sys
    
    class TailRecurseException(BaseException):
        def __init__(self, args, kwargs):
            self.args = args
            self.kwargs = kwargs
    
    
    def tail_call_optimized(g):
        """
        This function decorates a function with tail call
        optimization. It does this by throwing an exception
        if it is it's own grandparent, and catching such
        exceptions to fake the tail call optimization.
    
        This function fails if the decorated5
        function recurses in a non-tail context.
        """
        def func(*args, **kwargs):
            f = sys._getframe()
            if f.f_back and f.f_back.f_back and f.f_back.f_back.f_code == f.f_code:
                raise TailRecurseException(args, kwargs) # 抛出异常
            else:
                while 1:
                    try:
                        return g(*args, **kwargs)
                    except TailRecurseException as e:
                        args = e.args
                        kwargs = e.kwargs
    
        func.__doc__ = g.__doc__
        return func
    
    
    @tail_call_optimized
    def factorial(n, acc=1): # 阶乘的实现
        if n == 0:
            return acc
        return factorial(n - 1, n * acc)
    
    
    print(factorial(10000))
    

    tail_call_optimized()其实就是一个装饰器,该装饰器装饰了factorial()让其实现了尾递归函数的功能,接下来对尾递归优化原理进行分析:

    进行分析前,先明白几个操作:

    f = sys._getframe()实际上返回一个frameobject(也就是python所维护的帧栈),简称f,详情可以阅读6. Frames Objects - 深入理解 Python 虚拟机 (gitbook.io)该网址。

    f中的 f.f_back实际上会返回调用栈下一个元素的对象,也就是目前元素的上一个进栈对象

    f.f_back.f_back顾名思义就是上上一个进栈对象

    f_code指的是目前代码路径

    进行分析:

    1. factorial(10000)在执行前首先进入装饰器函数,f.f_back为main()函数也就是调用factorial(10000)的函数
    2. factorial(10000)下的f.f_back.f_backNone,所以进入else逻辑(调用栈深度为1)
    3. 执行factorial(10000)
    4. factorial(10000)调用factorial(9999,10000)
    5. 这时函数factorial(9999,10000)进入装饰器函数,符合if条件(调用栈深度为2),执行raise TailRecurseException,factorial(9999,10000)参数传入TailRecurseException
    6. 该参数被factorial(10000)except捕捉(调用栈深度为1),通过TailRecurseException将参数(9999,10000)传递给factorial,从而进行下一次调用
    7. 因为由于还处于factorial(10000)的装饰器的(调用栈深度为1)的装饰器的else分支中,继续走while循环,继续走4-6的流程。
    8. 因为最后factorial()函数最后会return acc,相当处于factorial(10000)的装饰器的(调用栈深度为1)的装饰器的else分支中,由于factorial()函数的return,跳出该装饰器的while循环,最后结束递归。
2.4.4 命名空间

命名空间(namespace)是从名称(names)到对象(objects)的映射。说的更加明白一点就是在Python中,变量只是一个引用,指向的是这个被赋值的对象。由于变量是一个引用关系,也就存在一对一的映射,因此需要有一个地方保存这个对应关系。命名空间就是用来保存这种对应关系的。一句话说明就是:命名空间是一个特殊对象,它保存的是变量名称(标识符)到对象(本质上是内存区域)的映射。

在python中里面有许多的名字空间,每个地方都有自己的名字空间,互不干扰,不同空间中的两个相同名字的变量之间没有任何关系。在python中有4种命名空间:LEGB

  • locals:函数被调用时创建的命名空间,记录函数的参数、函数体中定义的所有变量。注意:一个类或对象的属性集合,也构成了一个局部命名空间。但通常使用[对象名].[属性名]的间接方式访问属性,而不是直接通过属性名的方式访问,是比较特殊的一种局部命名空间,因此不将其列入命名空间讨论。生命周期:函数的局部命名空间,在函数调用时创建,函数调用结束或抛出异常时销毁(每一个递归函数都拥有自己的命名空间)
  • encoding function:在嵌套函数种外部函数的名字空间。若fun2嵌套在fun1中,对于fun2来说,fun1的名字空间就是encoding。**生命周期:**只有在嵌套函数被调用时,才会被创建,在函数调用结束后就会被摧毁。
  • globals:模块被加载时创建的命名空间,记录模块中导入的其它模块,定义的全局变量、函数和类。生命周期:在模块被解释器加载时创建,解释器退出或模块被移除(例如del 模块)时销毁;
  • built-in:内置命名空间,python语言内置的命名空间,在写代码时,任何地方都可以访问。例如,Python内置的数据类型(如bool,float,int,list,dict,set,str等类型)、内置函数(如sum,min,max,print等函数)和标准异常(如Exception、IOError、IndexError、NameError等异常)等等。生命周期:在Python解释器启动时创建,解释器退出时销毁;

作用域:不同命名空间的作用域也是不同的,上面有四个命名空间那么也就是说也有4个各自相对应的作用域。一句话来概括就是不同变量的作用域不同就是由这个变量所在的命名空间所决定的。

作用域即范围:

  • 全局范围:全局存活,全局有效
  • 局部范围:临时存活,局部有效

作用域是有查询顺序的。变量的查找顺序为:LEGB,也就是locals -> enclosing funcation - >globals ->built-in这个顺序。python解释器就会根据这个顺序进行一层层的查找,找到之后就会停止搜索,如果最后没有找到,就会抛出NameError的异常。这个顺序是非常重要的,因为可以让我们明白如果在函数调用的时候出现相同的变量名称的时候,可以帮我们快速的明确该变量的映射关系。

根据以上的查找规则,我们可以很明确和轻易的得出以下两条结论:

  • 内层能访问该层及外层的所有作用域中的变量;
  • 外层不能访问该层以内的所有作用域中的变量,也不能访问另一个同层次级别的作用域中的变量

Python 中只有模块(module),类(class)、函数(def、lambda)以及列表推导式才会创建新的命名空间并形成新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会创建新的命名空间以及形成新的作用域,也就是说这些语句内定义的变量,在该代码行所在的作用域的其它位置也是可以访问。

这也就很好的解释了python在函数中,为啥函数不可以直接修改相同名称变量全局变量的值,嵌套函数不可以直接改变相同名称变量的调用该函数的函数的局部函数的变量值。主要原因就是,这些被调函数只是创建了相同变量的局部变量,当被调函数结束后,就这些局部变量生命周期结束,这些变量就会摧毁,和调用该函数的函数变量完全就是不一样的东西。

所以需要:

  • 要想在局部作用域中修改全局变量的值,必须使用global关键字对变量进行声明
  • 在嵌套函数中修改嵌套作用域中的变量(也就是修改外层函数的局部作用域中的变量),必须使用 nonlocal 关键字对变量进行声明
2.4.5 闭包

闭包:在一个内部函数中,对外部作用域的变量(encoding function)进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。说的更加清楚一些,闭包就是一个函数的定义和函数都位于另一个函数体内,而且这些内部函数可以访问其所在的外部函数中所声明的局部变量和参数,并且内部函数往往是作为外部函数的返回值。而当这个内部函数执行时,它仍然必需访问其外部函数所声明的局部变量,参数等。这些局部变量,参数的值是当时外部函数所返回时的值。

闭包的意义:返回的函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得该函数无论在何处调用的时候,都优先使用自己的外层包括的作用域。

举例如下:

def add_num(x):
    def add_contant(y):
        return x + y
    return add_contant
add_8 = add_num(8)
print(add_8) # 输出函数的内存地址
print(add_8(2)) # 输出10

可以很轻易的看出在add_num函数中定义add_contant这个内部函数,返回的也是这个内部函数。add_8这个变量等于add_num(8)这个函数的返回值,也就是说是add_contant这个内部函数的内存地址,同时也返回了外部函数的作用域局部参数x=8,调用的add_8这个变量时也可以看出。

闭包的妙用:还会有一个常见的错误就是python循环中不包含域的概念,举例如下

list_1=[]
for i in range(3):
    def func(x):
        return x*i
    list_1.append(func)
for f in list_1:
    print(f(2)) # 输出4 4 4

这个输出的结果并不是我们想要的0,2,4;原因就是没有使用闭包,因为在列表中添加元素的时候,只是把函数的内存地址添加到列表中,也就是说list_1这个列表只是存了三个函数内存地址而已,当函数运算的时候i=2,所以出现3个4也不为奇怪了,为了解决这个问题,可以使用闭包的方法,举例如下:

list_1=[]
for i in range(3):
    def fun_out(i):
        def func(x):
            return x*i
        return func
    list_1.append(fun_out(i))
for f in list_1:
    print(f(2)) # 输出0 2 4

这个改动说白了就是使用了闭包的方法,把函数的内存地址和外部函数调用的作用域一起保存下来了。所以当函数运行的时候,就可以调用当时外部函数所保留的局部变量和参数了。

2.4.6 装饰器

在项目工程中,我们常常需要对已经开发的功能或代码进行功能的拓展,需要基于原来的代码基础上对程序进行二次开发,在这个开发的过程中,我们需要遵循一个非常重要的原则——开发封闭原则。

开放封闭原则主要体现在两个方面:

  1. 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  2. 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。

简单来说:就是我们不应该改变原来的代码功能逻辑,但是可以拓展功能,并且也不应该改变原来功能的调用方式。

所以说基于这种思想的影响下,就需要使用装饰器这个方法了。

装饰器的概念:装饰器就是一个闭包,装饰器是闭包的一种应用。简言之,python装饰器就是用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,好处就是在不用更改原函数的代码前提下给函数增加新的功能。使用时,再需要的函数前加上@demo即可。

2.4.6.1 装饰器的实现

经过上述文字的描述,可能理解起来还是有点抽象,为了更好的理解装饰器的含义,可以通过一个小例子来更加直观的理解它。

def print_self():
    print("I am liverpool")
print_self()

这个是一个介绍自己名字的函数,但是现在需要在介绍名字前,介绍自己的国家,要保证不改变自己自己的调用方式和源码的情况下实现。可以由闭包实现:

def print_self():
    print("I am liverpool")

def print_selfPlus(fun):
    def inner():
        print("I from China")
        fun()
    return inner

print_self = print_selfPlus(print_self)
print_self()

对上述的代码进行分析:首先print_selfPlus()这个函数,是一个高阶函数(传入的参数是函数,返回的也是内部函数的内存地址),同时这也是一个闭包的结构(返回还包括encoding function的工作域),print_self = print_selfPlus(print_self)这句话其实前面的print_self 这个代表的是函数变量保存的是print_selfPlus()这个函数返回的内部函数内存地址,只是这个函数变量的命名和原来函数的命名一样而已,其实没有什么关联了。print_self()这句话才是真正运行函数。其实这就是装饰器。

为了代码的简练,还可以使用@符号,这个@符号就是装饰器的语法糖。

def print_selfPlus(fun):
    def inner():
        print("I from China")
        fun()

    return inner

@print_selfPlus
def print_self():
    print("I am liverpool")
print_self()

@print_selfPlus这行代码的意思就是print_self = print_selfPlus(print_self),接下来就直接可以使用print_self,可以达到和上文一样的效果,换句话说,其实使用装饰器的是,默认传入的参数就是被装饰的函数。

2.4.6.2 装饰器的传参

由于装饰器需要是用于各种函数,由于上文的分析,装饰器只是返回的是闭包的内置函数的内存地址,也就是说要给原来的执行的函数传参,只需要给内置函数传参就可以了。又已知python函数参数为*args**kwargs,表示可以接受任意参数。所以只需要内置函数的形参是(*args, **kwargs),就可以代表随意传入参数,同时注意在在内置函数调用本来的执行函数的时候也要把参数传递过去。举例如下:

def print_selfPlus(fun):
    def inner(*args, **kwargs):
        print("I from China")
        fun(*args, **kwargs)
    return inner

@print_selfPlus
def print_self():
    print("I am liverpool")

@print_selfPlus
def print_other(name):
    print(number"I am {name}")

print_self()
print_other("Unity")

经过上文的例子后,已经知道了如果给装饰器的内部函数传参,但又出现了一个问题,如何给装饰器传递参数。由2.4.6.1可知,装饰器本质上还是一个函数,那么问题就变成了,如何给内置函数传参并且需要和调用函数的时候的传参进行分开处理。可以很容易的想到,那就是在装饰器外部在搞一个闭包结构就行了.如下例所示:

def print_selfUtral(msg="china"):
    def print_selfPlus(fun):
        def inner(*args, **kwargs):
            print(number"I from {msg}")
            fun(*args, **kwargs)
        return inner
    return print_selfPlus

@print_selfUtral()
def print_self():
    print("I am liverpool")

@print_selfUtral("English")
def print_other(name):
    print(number"I am {name}")

print_self()
print_other("Unity")
'''输出:
I from china
I am liverpool
I from English
I am Unity
'''

其实这里的@print_selfUtral(),这行代码和上文的代码运行的不一样了,这里其实是运行了如下的两行代码

a = print_selfUtral()
print_self = a(print_self)

首先a = print_selfUtral()这个代码是让函数变量a等于print_selfPlus()这个内部函数,并且还同时返回了外部函数的作用域,也就是msg的值,print_self = a(print_self)这行代码再次返回需要真正调用的内部函数的内存地址.简单来说这里的语法糖@print_selfUtral()实际上是做了两次函数变量的解析取值,这也和我们的朴素认知相对应起来,我们做了两次函数闭包,那调用的时候也必须要解析两次函数才行.总之一句话,装饰器的传参可以在套一层函数闭包实现,同时直接使用语法糖@就可以解析到真正需要调用的函数.

2.4.6.3 类装饰器

在python中,其实也可以用类来实现装饰器的功能,称之为类装饰器。类装饰器的实现是调用了类里面的__call__函数。类装饰器的写法比我们装饰器函数的写法更加简单.

当使用类装饰器时,工作流程:

  • 通过__init__()方法初始化类
  • 通过__call__()方法调用真正的装饰方法

举例如下:

class Decorator:
    def __init__(self, func):
        self.func = func
        print("执行类的__init__方法")

    def __call__(self, *args, **kwargs):
        print('进入__call__函数')
        self.func(*args, **kwargs)
@Decorator
def print_self():
    print("I am liverpool")

print_self()

透过问题看本质,其实就是运行了以下代码,只是被@这个语法糖包装了一下

print_self = Decorator(print_self)
print_self()

其实就是基于Decorator这个类,创立一个实例对象print_self,并且通过__init__这个构造函数,把print_self()这个函数,作为print_self这个实例对象的属性,通过print_self()这行代码运行的时候,其实不是运行原来的调用函数,而是运行的是print_self这个实例对象的__call__()函数,一定要明确这点.

那么如何构建带参数的类装饰器,这与之前的类装饰器就完全不同了.

可以通过一下修改:当装饰器有参数的时候,__init__() 函数就不能传入fun(fun代表要装饰的函数)了,而fun是在__call__函数调用的时候传入的.举例如下:

# 带参数的类装饰器
class Decorator:
    def __init__(self, msg):   # init()方法里面的参数都是装饰器的参数
        self.msg = msg
        print("执行类的__init__方法")

    def __call__(self, func): # 因为装饰器带了参数,所以接收传入函数变量的位置是这里
        print('进入__call__函数')
        def inner(*args,**kwargs):
            print(number"I am from {self.msg}")
            func(*args,**kwargs)
        return inner

@Decorator("china")
def print_other(name):
    print(number"I am {name}")

print_other("liverpool")

我们还是要透过现象看本质,这个带参数的类装饰器为啥可以实现原函数的调用?其实本质上是@Decorator("china")@Decorator这两个的区别,之前的不带参数的类装饰器没有带 (),其返回的是一个对象实例,调用的时候本质上是调用了这个对象实例__call__函数,但是现在有参数的类装饰器,在语法糖的时候使用了(),那么就完全 不一样了,也就是说在语法糖的时候就直接使用了__call__函数,又因为现在的__call__函数是一个闭包结构,所以返回的就是一个函数变量,也就是一个函数的内存地址和其返回的一个作用域而已,和前面的用普通函数作为修饰器,本质上是一样的.带参数的类装饰器的运行逻辑和以下代码一样.

a = Decorator("china") # 得到装饰器的实例对象
print_other = a(print_other) #__call__函数所内置函数的内存地址
print_other("Liverpool") # 内置函数调用
2.4.6.4 多装饰器使用

一个函数可以被多个装饰器进行装饰,装饰器修饰完的函数,装饰器的执行顺序,一定要从源码上分析,一般来说都是先执行,最外面的装饰器,一步步执行到最里面的装饰器,最后执行原函数功能.

# 多装饰器的使用
def print_age(fun):
    def inner(*args,**kwargs):
        print("I am 23")
        fun(*args,**kwargs)
    return inner

def print_nation(fun):
    def inner(*args,**kwargs):
        print("I am from china")
        fun(*args,**kwargs)
    return inner

@ print_age
@ print_nation
def print_other(name):
    print(number"I am {name}")

print_other("Liverpool")
'''
I am 23
I am from china
I am Liverpool
'''

我们可以对上述例子进行分析,通过现象看本质,忽视语法糖,找到本质关系:

a = print_nation(print_other)
b = print_age(a)
b("liverpool")

可以看出:@ print_nation这个语法糖表示就是a = print_nation(print_other)这行代码的意思,@ print_ageb = print_age(a)也是相对应的,所以说@ print_age修饰的并不是原函数,而是修饰的是已经被@ print_nation修饰过的函数.最后函数变量等于的是经过两次内置修饰函数返回的原函数.所以运行的时候,先运行print_age()里面的内置函数,然后在运行里面的fun函数,也就是print_nation()的内置函数,然后运行里面的fun函数,也就是原函数.

2.4.7 生成器
2.4.7.1 列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。简单来说就是一种可以让代码更加简单的语法规则。

生成的列表    =    [ 相对元素进行的操作   for    要提取的元素    in    原来的列表 (可以加一些if语句对元素进行删选)]

但是这里需要注意的一个点,这里的if只是作为删选元素的条件,所以后面不能加else。举例如下:

list1 = [i * 2 for i in range(0,5)]
print(list1)

以上代码就是一个标准的列表生成式。,以上代码的作用就是把0,1,2,3,4这几个数都乘2,并且都生成一个列表,用以保存这些数据,和以下代码达到的效果是一样的:

list1 = []
for i in range(0,5):
    list1.append(i * 2)
print(list1)

那么现在需要将0~5直接的偶数去除,只让计数部分进行平方,也就是如何在列表生成式中加入if语句

list2 = [i ** 2 for i in range(0, 5) if i % 2 == 1]
print(list2)
# 传统方法实现
# list2 =[]
# for i in range(0,5):
#     if i % 2 == 1:
#         list2.append(i ** 2)
# print(list2)

列表生成式中,双重循环的实现:

list4 = [item2 for item1 in list3 for item2 in item1 if item2 % 2 == 0]
print(list4)
# 传统方法的实现
# list4 = []
# for item1 in list3:
#     for item2 in item1:
#         if item2 % 2 == 0:
#             list4.append(item2)
# print(list4)

结合以上所学,进行一道综合能力的提升题目:

给定一个正整数,计算有多少对质数之和等于输入的这个正整数,输入的值在3~1000之间。

举例:输入为10,应该输出为2.(有两队指数的和为10,分别为(5,5),(3,7))

num1 = int(input('输入3<num<1000的整数:'))
def Prime_number(number):
    if number == 1:
        return False
    for j in range(2, number):
        res = number % j
        if res == 0:
            return False
    else:
        return True
list1 = [i for i in range(2, num1 + 1) if Prime_number(i)]
N = 0
for item in list1:
    if num1 - item in list1 and item <= num1 - item:
        N += 1
print(N)
2.4.7.2 生成器

生成器的定义:在Python中,一边循环一边计算的机制,称为生成器(generator)

列表所有数据都在内存中,如果有海量数据的话将会非常耗内存,如果仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

如果列表元素按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。简单一句话:又想要得到庞大的数据,又想让它占用空间少,那就用生成器。创建生成器总共有两种方法:

第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator。如下例所示:

L = (x * x for x in range(10))
print(L)  # 输出:<generator object <genexpr> at 0x0000025E4D4CE120>

方法二, 如果一个函数中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。调用函数就是创建了一个生成器(generator)对象

def fib(n):
    prev, curr = 0, 1
    while n > 0:
        n -= 1
        yield curr
        prev, curr = curr, curr + prev

print(fib(4)) #输出:<generator object fib at 0x000001467257F120>

在这里我们需要明确一个观点,以上的所创建的生成器都是一个创建了一个生成器(generator)对象,和原来的列表生成式和函数没有任何关系。其中yield表示的是函数运行到这里暂停的意思。

生成器的工作原理

(1)生成器(generator)能够迭代的关键是它有一个next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。

(2)带有 yield 的函数不再是一个普通函数,而是一个生成器generator。

可用next()调用生成器对象来取值。next 两种方式 t.__next__() 或者next(t)。这里需要明确一点就是,生成器是只能往下执行的,是不能回溯的。

可用for 循环获取返回值(每执行一次,取生成器里面一个值)(基本上不会用next()来获取下一个返回值,而是直接使用for循环来迭代)

(3)yield相当于 return 返回一个值,并且记住这个返回的位置,下次迭代时,代码从yield的下一条语句开始执行。简单来说:在每次调用next()的时候执行,遇到yield语句返回。当再次调用该对象时才会再次执行时从上次返回的yield语句处继续执行

(4).send() 和next()一样,都能让生成器继续往下走一步(下次遇到yield停),但send()能传一个值,这个值作为yield表达式整体的结果

def fib(n):
    prev, curr = 0, 1
    while n > 0:
        n -= 1
        yield curr
        prev, curr = curr, curr + prev

f = fib(10)
# 以下两种调用方式都是可以的 
print(f.__next__())
print(next(f))

但是当这种next的方法实在是太麻烦了,所以我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代,如下例所示:

def fib(n):
    prev, curr = 0, 1
    while n > 0:
        n -= 1
        yield curr
        prev, curr = curr, curr + prev
list1 = [i for i in fib(10)]
print(list1)

为啥可以使用for循环?因为generator也是可迭代对象(可以认为生成器是特殊的迭代器),并且不需要关心StopIteration的错误。

2.4.7.3 生成器实现单线程并发
# 生成器实现单线程并发
def consumer(name):  # 这是消费者
    while True:
        p = yield  # 生成器从外部获取参数
        print(f"prodcut {p} ,consumer is {name}")

def producer(name, generator_consumer):
    generator_consumer.__next__() # 生成器的启动需要传递一个空参数,之后才能send参数给生成器(语法规定)
    print(f"producer is {name}")
    for i in range(5):
        print(f"producer {name} send new product {i}")
        generator_consumer.send(i)

c1 = consumer("A")
c2 = consumer("B")
producer("P1",c1)
producer("P2",c2)

这其中可以看出,通过使用生成器的方式,只让生产者运行后给消费者send产品后,消费者就可以接收生产者生产的产品,并且进行自己的线程工作,虽然本质上还是一个单线程程序运行,但是在表面效果上已经实现了并发的效果。

2.4.8 迭代器

一个对象要想使用 for 的方式迭代出容器内的所有数据,这就需要这个类实现迭代器协议。

也就是说,一个类如果实现了迭代器协议,就可以称之为迭代器

Python 中,实现迭代器协议就是实现以下 2 个方法:

  • __iter__:这个方法返回对象本身,即 self
  • __next__:这个方法每次返回迭代的值,在没有可迭代元素时,抛出 StopIteration 异常

举例所示

class IteratorBase:
    """A 实现了迭代器协议 它的实例就是一个迭代器"""
    def __init__(self, n):
        self.idx = 0
        self.n = n

    def __iter__(self):
        print('__iter__')
        return self

    def __next__(self):
        if self.idx < self.n:
            val = self.idx
            self.idx += 1
            return val
        else:
            raise StopIteration()

# 迭代元素
a = IteratorBase(3)
for i in a:
    print(i)
# 再次迭代 没有元素输出 因为迭代器只能迭代一次
for i in a:
    print(i)
# __iter__
# 0
# 1
# 2
# __iter__

在这个例子中,我们定义了一个类 IteratorBase,它内部实现了 __iter____next__ 两个方法,也就是实现了迭代器协议

其中 __iter__ 方法返回了 self__next__ 方法实现了具体的迭代细节。

然后执行 a = IteratorBase(3),在执行 for i in a 时,我们看到调用了 __iter__ 方法,然后依次输出 __next__ 中的元素。

其实在执行 for 循环时,实际执行流程是这样的:

  1. for i in a 相当于执行 iter(a)
  2. 每次迭代时会执行一次 __next__ 方法,返回一个值
  3. 如果没有可迭代的数据,抛出 StopIteration 异常,for 会停止迭代

但是请注意,当我们迭代完 for i in a 时,如果再次执行迭代,将不会有任何数据输出。

如果我们想每次执行都能迭代元素,只需每次迭代一个新对象即可,如for i in IteratorBase(3):

但是我们在这里需要明确一点就是可迭代对象迭代器是有本质区别的。

from collections.abc  import Iterable
print(isinstance(需要判断的对象,Iterable)) # 验证需要判断的对象是不是可迭代对象
from collections.abc  import Iterator
print(isinstance(需要判断的对象,Iterator)) # 验证需要判断的对象是不是迭代器

可以举一个小例子:生成器,列表,字符串,字典都是可迭代对象,但是其中只有生成器是迭代器。

但是我们对可迭代对象的定义是:__iter__ 方法返回一个迭代器,那么这个对象就是可迭代对象,也就是说但凡是可以返回一个迭代器的对象,都可以称之为可迭代对象。

那么一个新的问题来了,那些不是迭代器的可迭代对象是如何实现迭代的呢?究其本质,是因为这个类,只定义了 __iter__ 方法而没有去定义__next__方法。如下例所示:

class IteratorBase:
    """A 实现了迭代器协议 它的实例就是一个迭代器"""
    def __init__(self, n):
        self.idx = 0
        self.n = n

    def __iter__(self):
        print('__iter__')
        return self

    def __next__(self):
        if self.idx < self.n:
            val = self.idx
            self.idx += 1
            return val
        else:
            raise StopIteration()
class IteratorPlus:
    # 不是迭代器 但B的实例是一个可迭代对象
    # 因为它只实现了 __iter__
    # __iter__返回了A的实例 迭代细节交给了A
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        return IteratorBase(self.n) # 相当于返回一个可以迭代的迭代器

# a是一个迭代器 同时也是一个可迭代对象
a = IteratorBase(3)
for i in a:
    print(i)

# b不是迭代器 但它是可迭代对象 因为它把迭代细节交给了A
b = IteratorPlus(3)
# 在进行迭代的时候 其实是运行了B的iter,但是返回的A的next方法
for i in b:
    print(i)

可以看出上述例子中,b是可迭代对象,因为其定义的__iter__ 方法返回一个迭代器,但它不是一个迭代器,因为其没有满足迭代器协议,其没有定义__next__方法,返回的只是另一个迭代器的__next__方法。所以使用最多的 listtuplesetdict 类型,都只是可迭代对象,但不是迭代器,因为它们都是把迭代细节交给了另外一个类,这个类才是真正的迭代器。

所以可以解决上文遗留下来的问题,为什么生成器是迭代器,因为调用 iter(生成器) 可以得知 __iter__ 返回的是实例本身,也就是说其实现了迭代器协议,也就是说其是迭代器,那也必然就是可迭代对象。

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值