记录我的 python 学习历程-Day10 函数进阶

函数的参数Ⅱ

  • 形参的第三种:动态参数

    动太参数分为两种:动态接收位置参数:*args;动态接收关键字参数:**kwargs

    • 动态接收位置参数:*args

      def msg(*args):
          print('你的信息为:', args)
          
      msg('name', 111, False, [1, 3, 4, 5])
      
      # 运行结果:你的信息为: ('name', 111, False, [1, 3, 4, 5])
      

      ​ 解释一下上面参数的意义:首先来说args,args就是一个普通的形参,但是如果你在args前面加一个,那么就拥有了特殊的意义:在python中除了表示乘号,他是有魔法的。加 args,这样设置形参,那么这个形参会将实参所有的位置参数接收,放置在一个元组中,并将这个元组赋值给args这个形参,这里起到魔法效果的是 * 而不是args,a也可以达到刚才效果,但是我们PEP8规范中规定就使用args,约定俗成的。

      • 练习:传入函数中数量不定的 Int 型数据,函数计算所有数的和并返回。
      # 传入函数中数量不定的int型数据,函数计算所有数的和并返回。
      def cont(*args):
          n = 0
          for i in args:
              n += i
          return n
      
      cont = cont(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
      print(cont)
      
      # 运行结果: 55
      
    • 动态接收关键字参数: ****** kwargs

      ​ 实参角度有位置参数和关键字参数两种,python中既然有*args可以接受所有的位置参数那么肯定也有一种参数接受所有的关键字参数,那么这个就是** kwargs,同理这个 ** 是具有魔法用法的,kwargs约定俗成使用作为形参。举例说明:**kwargs,是接受所有的关键字参数然后将其转换成一个 '字典' 赋值给 kwargs 这个形参。

      def func(**kwargs):
          print(kwargs)
          
      func(name='Dylan', age=18, sex='男')
      
      # 运行结果:{'name': 'Dylan', 'age': 18, 'sex': '男'}
      
    • 动态形参的完整写法

      def fun(*args, **kwargs):
          print(args)
          print(kwargs)
          
      fun('周一', '周二', '周三', '周四', name='Dylan', age=18, sex='男')
      
      # 运行结果:
      # ('周一', '周二', '周三', '周四')
      # {'name': 'Dylan', 'age': 18, 'sex': '男'}
      

      ​ 如果一个参数设置了动态参数,那么他可以接收所有的位置参数,以及关键字参数,这样就会大大提升函数拓展性,针对于实参参数较多的情况下,解决了一一对应的麻烦。

  • *** 的魔性用法**

    • 函数中分为打散和聚合。

      • 聚合

        在函数定义时,哪果我只定义了一个形参称为:args,那么这一个形参只能接受一个实参:

        def msg(args):
            print('你的信息为:', args)
            
        msg('name')
        
        # 运行结果:你的信息为: name
        

        如果给其前面加一个 * 号,那么 args 就可以接收多个实参,并且返回一个元组(**kwargs 也是同理,将多个关键字参数转化成一个字典返回)
        所以:
        在函数定义时,*‘ * ’号起到的是聚合的作用。

      • 打散

        s = 'Dylan'
        li = [1, 2, 3, 4]
        tu = ('Name = Dylan', 'age = 18', 'sex = 男')
        
        def func(*args):
            print(args)
            
        func(*s, *li, *tu)
        
        # 运行结果:('D', 'y', 'l', 'a', 'n', 1, 2, 3, 4, 'Name = Dylan', 'age = 18', 'sex = 男')
        

        ​ 函数执行时,我将你位置参数的实参(可迭代类型)前面加上 * 或者 ** ,相当于将这些实参给拆解成一个一个的组成元素当成位置参数,然后传给 args或者 kwargs。
        所以:在函数的执行时,* 或者 ** 起到的是打散的作用。

        dic1 = {'name': 'Dylan', 'age': 18, 'sex': '男'}
        dic2 = {'0': 11, '1': 22, '2': 33}
        
        def func(**kwargs):
            print(kwargs)
            
        func(**dic1, **dic2) #
        
        # 运行结果:{'name': 'Dylan', 'age': 18, 'sex': '男', '0': 11, '1': 22, '2': 33}
        
    • 函数外可以处理剩余的元素。

      除了在函数中可以这样打散,聚合外,函数外还可以灵活的运用:

      # 之前讲过的分别赋值
      a,b = (1,2)
      print(a, b) # 1 2
      # 其实还可以这么用:
      a,*b = (1, 2, 3, 4,)
      print(a, b) # 1 [2, 3, 4]
      *rest,a,b = range(5)
      print(rest, a, b) # [0, 1, 2] 3 4
      print([1, 2, *[3, 4, 5]]) # [1, 2, 3, 4, 5]
      

      一看就明白,不多说了。

  • 形参的顺序

    位置参数必须在前面,即 :位置参数,默认参数。
    那么动态参数 * args, *kwargs放在哪里呢?
    动态参数 *args,肯定不能放在位置参数前面,这样我的位置参数的参数就接收不到具体的实参了:

    # 这样位置参数a,b 始终接收不到实参了,因为 *args全部接收完了
    def func(*args, a, b, sex='男'):
        print(args)
        print(a, b)
        
    func(1, 2, 3, 4, 5)
    
    # 运行结果:程序报错了
    

    那么动态参数必须在位置参数后面,他可以在默认参数后面么?

    # 这样也不行,我的实参的第三个参数始终都会将sex覆盖掉,这样失去了默认参数的意义。
    def func(a, b, sex='男', *args, ):
        print(args)  # (4, 5)
        print(sex)  # 3
        print(a, b)  # 1 2
    
    func(1, 2, 3, 4, 5)
    

    所以*args一定要在位置参数与默认值参数中间:位置参数,*args,默认参数

    # 直接报错:因为**kwargs是接受所有的关键字参数,如果你想改变默认参数sex,你永远也改变不了,因为它会先被**kwargs接受。
    def func(a,b,*args,**kwargs,sex='男',):
        print(args) # (4, 5)
        print(sex) # 3
        print(a,b) # 1 2
        print(kwargs)
    
    func(1, 2, 3, 4, 5, age=80, sex='666')
    

    所以截止到此:所有形参的顺序为:
    **位置参数,*args,默认参数, * * kwargs。 **

  • 形参的第四种参数:仅限关键字参数

    仅限关键字参数是python3x更新的新特性,他的位置要放在*args后面,** kwargs前面(如果有 **kwargs),也就是默认参数的位置,它与默认参数的前后顺序无所谓,它只接受关键字传的参数:

    def func(a,b,*args,sex= '男',c,**kwargs,):
        print(a,b)
        print(sex)
        print(args)
        print(c)
        print(kwargs)
        
    func(1,2,3,4,5,6,7,sex='女',name='Alex',age=80,c='666')
    
    """
    输出结果:
    1 2
    女
    (3, 4, 5, 6, 7)
    666
    {'name': 'Alex', 'age': 80}
    """
    

    这个仅限关键字参数从名字定义就可以看出他只能通过关键字参数传参,其实可以把它当成不设置默认值的默认参数而且必须要传参数,不传就报错。

    所以形参角度的所有形参的最终顺序为:
    *位置参数,args,默认参数,仅限关键字参数, kwargs。

名称空间和作用域

  • 名称空间

    ​ 在python解释器开始执行之后, 就会在内存中开辟一个空间, 每当遇到一个变量的时候, 就把变量名和值之间的关系记录下来, 但是当遇到函数定义的时候, 解释器只是把函数名读入内存, 表示这个函数存在了, 至于函数内部的变量和逻辑, 解释器是不关心的. 也就是说一开始的时候函数只是加载进来, 仅此而已, 只有当函数被调用和访问的时候, 解释器才会根据函数内部声明的变量来进行开辟变量的内部空间. 随着函数执行完毕, 这些函数内部变量占用的空间也会随着函数执行完毕而被清空.

    ​ 我们首先回忆一下Python代码运行的时候遇到函数是怎么做的,从Python解释器开始执行之后,就在内存中开辟里一个空间,每当遇到一个变量的时候,就把变量名和值之间对应的关系记录下来,但是当遇到函数定义的时候,解释器只是象征性的将函数名读如内存,表示知道这个函数存在了,至于函数内部的变量和逻辑,解释器根本不关心。

    ​ 等执行到函数调用的时候,Python解释器会再开辟一块内存来储存这个函数里面的内容,这个时候,才关注函数里面有哪些变量,而函数中的变量会储存在新开辟出来的内存中,函数中的变量只能在函数内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被清空。

    ​ 我们给这个‘存放名字与值的关系’的空间起了一个名字:命名空间

    代码在运行伊始,创建的存储“变量名与值的关系”的空间叫做全局命名空间

    在函数的运行中开辟的临时的空间叫做局部命名空间也叫做临时名称空间

    ​ 现在我们知道了,py文件中,存放变量与值的关系的一个空间叫做全局名称空间,而当执行一个函数时,内存中会临时开辟一个空间,临时存放函数中的变量与值的关系,这个叫做临时名称空间,或者局部名称空间。

    ​ 其实python还有一个空间叫做内置名称空间:内置名称空间存放的就是一些内置函数等拿来即用的特殊的变量:input,print,list等等

    总结:

    • 全局命名空间--> 我们直接在py文件中, 函数外声明的变量都属于全局命名空间

    • 局部命名空间--> 在函数中声明的变量会放在局部命名空间

    • 内置命名空间--> 存放python解释器为我们提供的名字, list, tuple, str, int这些都是内置命名空间

      # 内置名称空间:python源码给你提供的一些内置的函数,print input
      # print(666)
      # python分为三个空间:
          # 内置名称空间(builtins.py)
          # 全局名称空间(当前py文件)
          # 局部名称空间(函数,函数执行时才开辟)
      
  • 加载顺序

    ​ 所谓的加载顺序,就是这三个空间加载到内存的先后顺序,也就是这个三个空间在内存中创建的先后顺序,你想想他们能是同时创建么?肯定不是的,那么谁先谁后呢?我们捋顺一下:在启动python解释器之后,即使没有创建任何的变量或者函数,还是会有一些函数直接可以用的比如abs(-1),max(1,3)等等,在启动Python解释器的时候,就已经导入到内存当中供我们使用,所以肯定是先加载内置名称空间,然后就开始从文件的最上面向下一行一行执行,此时如果遇到了初始化变量,就会创建全局名称空间,将这些对应关系存放进去,然后遇到了函数执行时,在内存中临时开辟一个空间,加载函数中的一些变量等等。
    所以这三个空间的加载顺序为:
    内置命名空间(程序运行伊始加载)->全局命名空间(程序运行中:从上到下加载)->局部命名空间(程序运行中:调用时才加载)。

    # 加载顺序:
    # 内置名称空间 ---> 全局名称空间  ----> 局部名称空间(函数执行时)
    def func():
        pass
    func()
    a = 5
    print(666)
    
  • 取值顺序

    ​ 值顺序就是引用一个变量,先从哪一个空间开始引用。这个有一个关键点:从哪个空间开始引用(注意:只可引用,不可更改上一层的变量)这个变量。我们分别举例说明:

    # 如果你在全局名称空间引用一个变量,先从全局名称空间引用,全局名称空间如果没有,才会向内置名称空间引用。
    input = 666
    print(input) # 666
    
    # 如果你在局部名称空间引用一个变量,先从局部名称空间引用,局部名称空间如果没有,才会向全局名称空间引用,全局名称空间在没有,就会向内置名称空间引用。
    input = 666
    print(input) # 666
    input = 666
    def func():
        input = 111
        print(input) # 111
    func()
    

    下面这个例子比较清晰

    # 取值顺序(就近原则) 单向不可逆
    # LEGB原则
    input = 'Dylan'
    
    def func():
        input = 'xiaoyu'
        print(input)    # xiaoyu
            
    func()
    
    # (从局部找时)局部名称空间  ---> 全局名称空间  --->  内置名称名称空间
    
    input = 'Dylan'
    def func():
        print(input)    # Dylan
        
    func()
    print(input)    # Dylan
    
    

    所以空间的取值顺序与加载顺序是相反的,取值顺序满足的就近原则,从小范围到大范围一层一层的逐步引用。

  • 作用域

    作用域就是作用范围, 按照生效范围来看分为全局作用域和局部作用域

    • 全局作用域: 包含内置命名空间和全局命名空间. 在整个文件的任何位置都可以使用(遵循 从上到下逐⾏执行).

    • 局部作用域: 在函数内部可以使用.

    作⽤域命名空间:

    1. 全局作用域: 全局命名空间 + 内置命名空间

    2. 局部作⽤域: 局部命名空间

  • 内置函数globals(),locals()

    这两个内置函数放在这里讲是在合适不过的,他们就直接可以反映作用域的内容,有助于我们理解作用域的范围。

    • globals(): 以字典的形式返回全局作用域所有的变量对应关系。

    • locals(): 以字典的形式返回当前作用域的变量的对应关系。

    这里一个是全局作用域,一个是当前作用域,一定要分清楚,接下来,我们用代码验证:

    # 在全局作用域下打印,则他们获取的都是全局作用域的所有的内容。
    a = 2
    b = 3
    print(globals())
    print(locals())
    '''
    {'__name__': '__main__', '__doc__': None, '__package__': None,
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001806E50C0B8>, 
    '__spec__': None, '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>, 
    '__file__': 'D:/lnh.python/py project/teaching_show/day09~day15/function.py',
    '__cached__': None, 'a': 2, 'b': 3}
    '''
    
    # 在局部作用域中打印。
    a = 2
    b = 3
    def foo():
        c = 3
        print(globals()) # 和上面一样,还是全局作用域的内容
        print(locals()) # {'c': 3}
    foo()
    

高阶函数(函数的嵌套)

​ 其实我们见到了嵌套这个词不陌生,之前我们讲过列表的嵌套,列表的嵌套就是一个列表中还有列表,可能那个列表中还有列表......那么顾名思义,函数的嵌套,就是一个函数中,还有函数。

​ 想要玩明白函数的嵌套,关键点:只要遇见了函数名+()就是函数的调用. 如果没有就不是函数的调用,吃透这一点就算明白了。那么我们举例练习:

# 例1:
def func1():
    print('in func1')	# 1
    print(3)	# 2
def func2():
    print('in func2')	# 4
    print(4)	# 5
func1()
print(1)	# 3
func2()
print(2)	# 6

# 例2:
def func1():
    print('in func1')	# 3
    print(3)	# 4
def func2():
    print('in func2')	# 2
    func1()
    print(4)	# 5
print(1)	# 1
func2()
print(2)	# 6
# 例3:
def fun2(): 
    print(2) 	# 2
    def fun3(): 
        print(6) 	# 4
    print(4) 	# 3
    fun3() 
    print(8)	# 5
print(3)	# 1
fun2()
print(5)	# 6
posted @ 2019-12-27 00:03  Dylan_Yu  阅读(165)  评论(0编辑  收藏  举报