Python(三)—— 函数初识&迭代器/生成器

7. 函数

7.1 函数初识

我们首先看下面一段代码,我们在进行钉钉打卡的时候,用一次就需要执行一次全部的代码,代码重复性较高,也比较麻烦,有没有什么办法,通过一个指令,它会自动执行以下代码

print("拿出手机")
print("打开钉钉软件")
print("找到打卡界面")
print("点击打卡成功")

那么这里,我写了一个打卡的功能,我将上面的那些重复代码封装到这个所谓的函数中,这样,我什么时候需要使用这个功能,我通过一个指令调用即可

def clock():
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("点击打卡成功")

clock()        # 函数名加括号,调用函数,即函数已运行


# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
点击打卡成功

上面这个就是一个函数,对比我们看一下函数的优势:

  • 减少代码的重复性
  • 使代码可读性更好

7.2 函数的结构与调用

7.2.1 函数的结构

首先咱们先看一下函数的结构:

def 函数名():

    函数体


'''
* def:关键词开头,空格之后接函数名称和圆括号(),最后还有一个冒号":"
  def:define的缩写,是固定的,不能变,他就是定义函数的关键字
* 空格:为了将def关键字和函数名分开,正常是空1格
* 函数名:函数名只能包含字符串、下划线和数字且不能以数字开头
  虽然函数名可以随便起,但我们给函数起名字还是要尽量简短,并具有可描述性
* 括号:是必须加的,因为有时还会在括号内添加变量(参数)
* 函数体缩进:下面的函数体一定全部都要缩进,这代表是这个函数的代码
'''

7.2.2 函数的调用

  • 使用函数名加小括号就可以调用了,写法:函数名(),这个时候函数的函数体会被执行
  • 只有解释器读到函数名()时才会执行此函数,若无这条指令,函数内即使有多行代码也是不执行的
  • 而且是这个指令你写几次,函数里面的代码就运行几次,就好比你在部队,我喊你名字,喊几次,你就回答几次,这就是指令
  • 直接调用定义好的函数就可以了,咱们打卡之后的得有个结果吧,比如是打卡成功还是打卡迟到总得有个结果,那么这个结果,怎么来描述和获得呢? 这就涉及到函数的返回值啦

7.2.3 函数的嵌套

  • 函数的嵌套,就是一个函数中,还有函数
  • 想要玩明白函数的嵌套,关键点:只要遇见 函数名() 就是函数的调用,如果没有就不调用

7.3 函数的返回值

一个函数就是封装一个功能,这个功能一般都会有一个最终结果的,比如你写一个打卡函数,最终打卡成功与否是不是需要返回你一个结果?还有咱们是不是都用过len这个函数,他是获取一个对象的元素的总个数,最终肯定会返回一个元素个数这样的结果:

s1 = 'abcdef'
print(len(s1))    # 6

那么我们函数的这个返回值如何设置呢?这就得用到python中的一个关键字:return

7.3.1 return

函数中遇到return,此函数结束,不在继续执行。函数的返回值返回给了 函数名() 这个整体,也就是这个执行者

def clock():
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("点击打卡成功")
    return '打卡没有迟到哦'     # return 设置了返回值,执行该函数会得到此返回值

CC = clock()                  # 打印CC等于执行了clock(),执行函数
print(CC)


# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
点击打卡成功
打卡没有迟到哦

return 会给函数的执行者返回值。当然,也可以返回多个值(元组形式)

def clock():
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("点击打卡成功")
    return '打卡没有迟到哦', '早退'

CC = clock()
print(CC)


# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
点击打卡成功
('打卡没有迟到哦', '早退')

7.3.2 总结

1、遇到return,函数结束,return下面的(函数内)的代码不会执行

2、return 会给函数的执行者返回值

  • 如果return后面什么都不写,或者函数中没有return则返回的结果是None
  • 如果return后面写了一个值,返回给调用者这个值
  • 如果return后面写了多个结果,返回给调用者一个tuple(元组),调用者可以直接使用元组的解构获取多个变量
def clock():
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("点击打卡成功")
    return '打卡没有迟到哦', '早退'

c1, c2 = clock()
print(c1)        # 返回值是:打卡没有迟到哦
print(c2)        # 返回值是:早退
print(c1,c2)     # 返回值是:打卡没有迟到哦 早退


# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
点击打卡成功
打卡没有迟到哦
早退
打卡没有迟到哦 早退

7.4 函数的传参

7.4.1 传参的认识

函数是以功能为导向的,上面我们写的函数里面的代码都是写死的,也就是说,这个函数里面的更改起来很麻烦,试想一下,我们之前学过的len,这个len是不是可以获取字符串的总个数?是不是可以获取列表的总个数?你更改了len函数内部的代码了?没有吧?你看下面的例子:

s1 = 'abcde'
l1 = [1, 2, 3]

print(len(s1))    # 5
print(len(l1))    # 3

那么我们写的函数也是可以将一些数据传到函数里面的,然后让里面的代码利用上这个数据产生我们想要的结果,再返回。举个例子:

def clock(name):
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s打卡成功" %name)

clock('雨落')


# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
雨落打卡成功

上面就是函数传参的示例,函数的参数可以从两个角度划分:

  • 形参:写在函数声明的位置的变量叫形参,形式上的一个完整,表示这个函数需要这个形参
  • 实参:在函数调用的时候给函数传递的值叫实参,实际执行的时候给函数传递的信息

函数的传参就是函数将实际参数交给形式参数的过程

def clock(name):      # 函数定义时(参数) 这个就是形参
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s打卡成功" %name)

clock('雨落')          # 函数执行时(参数) 这个就是实参


"""这个过程就是:代码运行到clock('雨落')开始执行此函数,同时将字符串'雨落'这个数据
传递给变量name,然后执行函数中的代码。如果遇到name,其实就是使用'雨落'这个数据"""

7.4.2 实参角度

7.4.2.1 位置参数

位置参数就是从左至右,实参与形参一一对应

def clock(name, time, location):     # 函数定义时(参数) 这个就是形参
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s于%s在%s打卡成功" %(name, time, location))

clock('雨落','10:00 AM','地球')       # 函数执行时(参数) 这个就是实参

# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
雨落于10:00 AM在地球打卡成功

练习1:编写函数,给函数传递两个参数a b,返回a参数和b参数相加的和

def f(a,b):
    c = a+b
    return c
​
num_sum = f(5,8)   # 此处传入实参,即a=5,b=8
print(num_sum)
​
# 输出结果:
13

练习2:编写函数:,给函数传递两个参数a b,比较a b的大小,返回a b中最大的那个数

def f(a,b):
    if a>b:
        return a
    else:
        return b
​
num_sum = f(5,8)
print(num_sum)
​
# 输出结果:
8

比较大小的这个写法有点麻烦,我们在这里学一个三元运算符

def f(a,b):
    c = a if a > b else b  # 当a>b就把a赋值给c,否则就把b赋值给c
    return c
​
msg = f(5,8)
print(msg)
​
# 输出结果:
8

7.4.2.2 关键字参数

位置参数好不好呢?如果是少量的参数还算OK,但是如果函数在定义的时候参数非常多怎么办?程序员必须记住有哪些参数。而且还有记住每个参数的位置,否则函数就不能正常调用了。那则么办呢?Python提出了一种叫做关键字参数,我们不需要记住每个参数的位置,只要记住每个参数的名字就可以了

def clock(name, time, location):
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s于%s在%s打卡成功" %(name, time, location))

clock(time='10:00 AM',name='雨落',location='地球')

# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
雨落于10:00 AM在地球打卡成功

7.4.2.3 混合参数

可以把上面两种参数混合使用,在调用函数的时候即可以给出位置参数,也可以指定关键字参数,混合参数一定要记住:关键字参数一定在位置参数后面

def clock(name, time, location):
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s于%s在%s打卡成功" %(name, time, location))

clock('雨落',time='10:00 AM',location='地球')

# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
雨落于10:00 AM在地球打卡成功
 
7.4.2.4 实参总结

综上:在实参的角度来看参数分为三种

  1. 位置参数
  2. 关键字参数
  3. 混合参数,位置参数必须在关键字参数前面

7.4.3 形参角度

7.4.3.1 位置参数

位置参数其实与实参角度的位置参数是一样的,就是按照位置从左至右,一一对应

def clock(name, time, location):
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s于%s在%s打卡成功" %(name, time, location))

clock('雨落','10:00 AM','地球')

# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
雨落于10:00 AM在地球打卡成功
 
7.4.3.2 默认值参数

在函数声明的时候,就可以给出函数参数的默认值。默认值参数一般是这个参数使用率较高,才会设置默认值参数,可以看看open函数的源码,mode=‘r’ 就是默认值参数。比如给出一个 name='雨落' 的默认值

def clock(time, location, name='雨落',):
    print("拿出手机")
    print("打开钉钉软件")
    print("找到打卡界面")
    print("%s于%s在%s打卡成功" %(name, time, location))

clock('10:00 AM','地球')

# 输出结果:
拿出手机
打开钉钉软件
找到打卡界面
雨落于10:00 AM在地球打卡成功

注意:必须先声明在位置参数,才能声明关键字参数

7.4.3.3 形参总结

综上:在形参的角度来看

  1. 位置参数
  2. 默认认值参数(大多数传进来的参数都是一样的,一般用默认参数)

7.4.4. 动态参数

那么无论是位置参数,还是默认参数,函数调用时传入多少实参,我必须写等数量的形参去对应接收, 如果不这样,那么就会报错

def hobby(a,b,c,):
    print('我喜欢:',a,b,c) 

hobby('唱','跳','rap','篮球')   # 实参与形参数量不对应,报错

如果我们在传参数的时候不很清楚有哪些的时候,或者说给一个函数传了很多实参,我们就要对应写很多形参,这样很麻烦,怎么办?我们可以考虑使用动态参数也叫万能参数

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

7.4.4.1 *args

我们按照上面的例子继续写,如果我请你吃的内容很多,但是我又不想用多个参数接收,那么我就可以使用动态参数*args

def hobby(*args):
    print('我喜欢:',*args) 

hobby('唱','跳','rap','篮球')

# 运行结果
我喜欢: 唱 跳 rap 篮球

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

练习:传入函数中数量不定的int型数据,函数计算所有数的和并返回

def my_max(*args):
    n = 0                 # 起始值设为0
    for i in args:
        n += i            # 让*args内部所有数字相加
    return n              # 返回最终值,并将这个值返回给my_max()这个函数
​
count = my_max(1,3,5,7)
print(count)              # 打印这个函数,即得到最终的n值

# 输出结果:
16

7.4.4.2 **kwargs

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

def func(**kwargs):
    print(kwargs)

func(name='雨落',sex='男')

# 输出结果:
{'name': '雨落', 'sex': '男'}

7.4.4.3 动态参数的混合写法

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

def func(*args,**kwargs):
    print(args)       # ('唱跳', 'rap', '篮球')
    print(kwargs)     # {'name': '雨落', 'sex': '男'}

func('唱跳', 'rap', '篮球',name='雨落',sex='男')

# 输出结果:
('唱跳', 'rap', '篮球')
{'name': '雨落', 'sex': '男'}

7.4.4.4 * 的魔性用法
7.4.4.4.1 聚合

刚才我们研究了,在函数定义时,如果我只定义了一个形参称为args,那么这一个形参只能接受几个实参? 是不是只能当做一个位置参数对待?它只能接受一个参数:

def hobby(args):
    print('我喜欢:',args)

hobby('唱跳')

# 输出结果:
我喜欢: 唱跳

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

7.4.4.4.2 打散

此时不着急给大家讲这个打散,而是出一个小题:你如何将三个数据(这三个数据都是可迭代对象类型)s1 = '雨落',l1 = [1, 2, 3, 4],tu1 = ('唱跳', 'rap', '篮球',)的每一元素传给动态参数*args?(就是args最终得到的是 ('雨', '落', 1, 2, 3, 4, '唱跳', 'rap', '篮球')),错误示范:

s1 = '雨落'
l1 = [1, 2, 3, 4]
tu1 = ('唱跳', 'rap', '篮球',)

def func(*args):
    print(args)

func(s1,l1,tu1)     # 此处会分别当做三个位置参数传入

# 输出结果:
('雨落', [1, 2, 3, 4], ('唱跳', 'rap', '篮球'))

这样肯定是不行,他会将这个三个数据类型当成三个位置参数传给args,没有实现要求,好像你除了直接写,没有别的什么办法,那么这里就得用到我们的魔法用法 :*

s1 = '雨落'
l1 = [1, 2, 3, 4]
tu1 = ('唱跳', 'rap', '篮球',)

def func(*args):
    print(args)

func(*s1,*l1,*tu1)     # 此处会将每个数据类型的元素打散全部传入

# 输出结果:
('雨', '落', 1, 2, 3, 4, '唱跳', 'rap', '篮球')
 

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

dic1 = {'name': '雨落', 'age': 18}
dic2 = {'hobby': '唱跳', 'sex': '男'}

def func(**kwargs):
    print(kwargs)

func(**dic1,**dic2)    # 同理,此处也会把每个数据类型的元素打散全部传入

# 输出结果:
{'name': '雨落', 'age': 18, 'hobby': '唱跳', 'sex': '男'}

7.4.4.4.3 处理剩下的元素

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

# 之前讲过的分别赋值
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]

7.4.5 形参的仅限关键字参数

仅限关键字参数的位置要放在*args后面,**kwargs前面,也就是默认参数的位置,它与默认参数的前后顺序无所谓,它只接受关键字传的参数:

--------- 错误示范 ---------

def func(a,b,*args,c):
    print(a,b)         # 1 2
    print(args)        # (3, 4, 5)

func(1, 2, 3, 4, 5)

情况说明:这样*args容易把(3,4,5)全部抢走,那么c就没有对应的实参
--------- 正确示范 ---------

def func(a,b,*args,c):
    print(a,b)         # 1 2
    print(args)        # (3, 4)
    print(c)           # 5

func(1, 2, 3, 4, c=5)  # 把c写成仅限关键字参数,那么c只接受关键字参数

7.4.6 形参的顺序

首先,位置参数,与默认参数 他两个的顺序:位置参数在前,默认参数在后。动态参数*args,肯定不能放在位置参数前面,这样我的位置参数的参数就接收不到具体的实参了

def func(*args,a,b,sex='男'):
    print(args)         # (1, 2, 3, 4, 5)把实参全部接收完了
    print(a,b)          # a b没有实参传入会报错

func(1, 2, 3, 4, 5)

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

def func(a,b,sex='男',*args,):
    print(args)        # (4, 5)
    print(sex)         # 3  实参的第三个参数把默认参数sex覆盖掉了
    print(a,b)         # 1 2

func(1, 2, 3, 4, 5)

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

动态参数**kwargs 放在哪里?kwargs可以放在默认参数前面么?

def func(a,b,*args,**kwargs,sex='男'):
    print(args)       # (4, 5)
    print(sex)        # 3
    print(a,b)        # 1 2
    print(kwargs)     # 默认参数sex的实参会先被**kwargs接受

func(1, 2, 3, 4, 5)

刚刚我们提到的仅限关键字参数 位置是在*args后面,**kwargs前面,所以总结下来的排序就是:

  • 位置参数
  • *args
  • 默认参数
  • 仅限关键字参数
  • **kwargs

  • 练习1
def foo(a,b,*args,c,sex=None,**kwargs):
    print(a,b)       # 1 2
    print(c)         # 6
    print(sex)       # None
    print(args)      # (3, 4)
    print(kwargs)    # {}
​
foo(1,2,3,4,c=6)
  • 练习2
def foo(a,b,*args,c,sex=None,**kwargs):
    print(a,b)       # 1 2
    print(c)         # 报错,c没有对应的实参
    print(sex)       # 男
    print(args)      # (),没有元素,空元组
    print(kwargs)    # {'name': '雨落', 'hobby': '唱跳'}
​
foo(1,2,sex='男',name='雨落',hobby='唱跳')
  • 练习3
def foo(a,b,*args,c,sex=None,**kwargs):
    print(a,b)       # 1 2
    print(c)         # 报错,c没有对应的实参,被*args先接收完了
    print(sex)       # 男
    print(args)      # (3, 4)
    print(kwargs)    # {'name': '雨落'}
​
foo(1,2,3,4,name='雨落',sex='男')
  • 练习4
def foo(a,b,*args,c,sex=None,**kwargs):
    print(a,b)       # 1 2
    print(c)         # 18
    print(sex)       # None
    print(args)      # ()
    print(kwargs)    # {}
​
foo(1,2,c=18)
  • 练习5
def foo(a,b,*args,c,sex=None,**kwargs):
    print(a,b)       # 1 2
    print(c)         # 13
    print(sex)       # None
    print(args)      # ([3,4,5])
    print(kwargs)    # {'hobby': '唱跳'}
​
foo(1, 2, [3, 4, 5],c=13,hobby='唱跳')
  • 练习6
def foo(a,b,*args,c,sex=None,**kwargs):
    print(a,b)       # 1 2
    print(c)         # 12
    print(sex)       # 男
    print(args)      # (3, 4)
    print(kwargs)    # {'name': '雨落'}
​
foo(*[1, 2, 3, 4],**{'name':'雨落','c':12,'sex':'男'})

7.5. 名称空间,作用域

7.5.1 名称空间

Python中经常提到的三个空间:

  • 全局命名空间:我们直接在py文件中, 函数外声明的变量都属于全局命名空间
  • 局部命名空间:在函数中声明的变量会放在局部命名空间
  • 内置命名空间:存放Python解释器为我们提供的名字, list, tuple, str, int这些都是内置命名空间

7.5.2 加载顺序

所谓的加载顺序,就是这三个空间加载到内存的先后顺序,也就是这个三个空间在内存中创建的先后顺序:在启动python解释器之后,即使没有创建任何的变量或者函数,还是会有一些函数直接可以用的就已经导入到内存当中供我们使用,所以肯定是先加载内置名称空间,然后就开始从文件的最上面向下一行一行执行,此时如果遇到了初始化变量,就会创建全局名称空间,将这些对应关系存放进去,然后遇到了函数执行时,在内存中临时开辟一个空间,加载函数中的一些变量等等。

所以这三个空间的加载顺序为:

  • 内置命名空间(程序运行伊始加载)
  • 全局命名空间(程序运行中:从上到下加载)
  • 局部命名空间(程序运行中:调用时才加载)

7.5.3 取值顺序

取值顺序就是引用一个变量,先从哪一个空间开始引用。这个有一个关键点:从哪个空间开始引用这个变量

如果你在全局名称空间引用一个变量,先从全局名称空间引用,
全局名称空间如果没有,才会向内置名称空间引用

input = 666
print(input)  

# 输出结果:
666
如果你在局部名称空间引用一个变量,先从局部名称空间引用;局部名称空间如果没有,
才会向全局名称空间引用,全局名称空间在没有,就会向内置名称空间引用

input = 666
def func():
    input = 111
    print(input)
func()

# 输出结果:
111

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

  • 加载顺序:内置名称空间 → 全局名称空间 → 局部名称空间
  • 取值顺序:局部名称空间 → 全局名称空间 → 内置名称空间

7.5.4 作用域

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

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

作用域命名空间:

  • 全局作用域:全局命名空间 + 内置命名空间
  • 局部作用域:局部命名空间

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

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

# 在全局作用域下打印,则他们获取的都是全局作用域的所有的内容。
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}

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

a = 2
b = 3
def foo():
    c = 3
    print(globals())     # 和上面一样,还是全局作用域的内容
    print(locals())      # {'c': 3}
foo()

7.6. 关键字global,nonlocal

7.6.1 global

讲这个关键字之前,先给大家看一个现象:

a = 1
def func():
    print(a)     # 此处是正常的 
func()
​
-----------------------------
​
a = 1
def func():
    a += 1       # 此处会报错   
func()

局部作用域对全局作用域的变量(此变量只能是不可变的数据类型)只能进行引用,而不能进行改变,只要改变就会报错,但是有些时候,我们程序中会遇到局部作用域去改变全局作用域的一些变量的需求,这怎么做呢?这就得用到关键字global

1、global功能一:在局部作用域中可以更改全局作用域的变量

count = 1
def search():
    global count     # 此处申明了全局变量
    count = 2        # 更改了全局变量
​
search()             # 执行函数
print(count)         # 得到的结果为2

2、global功能二:在局部作用域可以声明一个全局变量(限于字符串,数字)

def func():
    global a
    a = 3
    
func()
print(a)             # 3

7.6.2 nonlocal

nonlocal是python3x新加的功能,与global用法差不多,就是在局部作用域如果想对父级作用域的变量进行改变时,需要用到nonlocal,当然这个用的不是很多,了解即可

nonlocal的总结:

  • 不能更改全局变量
  • 在局部作用域中,对父级作用域(或者更外层作用域非全局作用域)的变量进行引用和修改,并且引用的哪层,从那层及以下此变量全部发生改变
def add_b():
    b = 42
    def do_global():
        b = 10
        print(b)               # 10  同级作用域do_global()的b
        def dd_nonlocal():
            nonlocal b         # 引用父级作用域的b
            b = b + 20
            print(b)           # 30
        dd_nonlocal()
        print(b)               # 30
    do_global()
    print(b)                   # 42  同级作用域add_b()的b     
add_b()

7.7. 函数名的运用

函数名的定义和变量的定义几乎一致,在变量的角度,函数名其实就是一个变量,具有变量的功能:可以赋值;但是作为函数名他也有特殊的功能就是加上()就会执行对应的函数,所以我们可以把函数名当做一个特殊的变量,那么接下来,我们就来研究一下这个特殊的变量

7.7.1 函数的内存地址

def func():        
    print("呵呵")
print(func)
​
# 输出结果:
<function func at 0x1101e4ea0>

通过上面代码可以我们知道,函数名指向的是这个函数的内存地址,其实深一步理解可得知,与其说函数名() 可以执行这个函数,不如说是函数的内存地址()才是执行这个函数的关键,就好比:

a = 1
b = 2
c = a + b
print(c)

# 输出结果:
3 

a + b 并不是变量的相加,而是两个变量指向的int对象的相加

7.7.2 赋值给其他变量

def func():
    print("呵呵")
print(func)
​
a = func     # 把函数当成一个变量赋值给另一个变量
a()          # 函数调用 func()

# 输出结果:
<function func at 0x104d21d00>
呵呵

通过变量的赋值,变量a,和变量func都指向的这个函数的内存地址,那么a() 当然可以执行这个函数了

7.7.3 当做容器类的元素

函数名就是一个变量,我的变量是可以当做容器类类型的元素的

def func1():
    print("in func1: 嘻嘻")
def func2():
    print("in func2: 哈哈")
def func3():
    print("in func3: 咯咯")
def func4():
    print("in func4: 吱吱")
    
lst = [func1, func2, func3, func4]
for i in lst:
    i()
    
# 输出结果:
in func1: 嘻嘻
in func2: 哈哈
in func3: 咯咯
in func4: 吱吱

7.7.4 当做函数的参数

变量可以做的,函数名都可以做到

def func1():           
    print('in func1')      # 第五步:打印
​
def func2(f):              # 第二步:把func1赋值给f
    print('in func2')      # 第三步:打印
    f()                    # 第四步:执行func1函数
​
func2(func1)               # 第一步:执行func2函数
​
# 输出结果:
in func2
in func1

7.7.5 作为函数的返回值

def func1():
    print('in func1')     # 第六步:打印
​
def func2(f):             # 第二步:把func1赋值给f
    print('in func2')     # 第三步:打印
    return f              # 第三步:把f,即func1返回给执行者,即ret
​
ret = func2(func1)        # 第一步:执行func2函数
ret()                     # 第五步:ret = func1,执行func1函数
                          # ret, f, func1 都是指向的func1这个函数的内存地址
# 输出结果:
in func2
in func1

小结:函数名是一个特殊的变量,他除了具有变量的功能,还有最主要一个特点就是加上() 就执行,其实他还有一个学名叫第一类对象

7.8. f-strings格式化输出

f-strings 是python3.6开始加入标准库的格式化输出新的写法,这个格式化输出比之前的%s 或者 format 效率高并且更加简化,非常的好用,相信我,你们学完这个之后,以后再用格式化输出这就是你们唯一的选择

7.8.1 简单举例

他的结构就是F(f)+ str的形式,在字符串中想替换的位置用{}展位,与format类似,但是用在字符串后面写入替换的内容,而他可以直接识别

name = '雨落'
age = 18
sex = '男'
msg = f'姓名:{name},性别:{age},年龄:{sex}'    # 大写字母F也可以
print(msg)

# 输出结果:
姓名:雨落,性别:18,年龄:男

7.8.2 任意表达式

他可以加任意的表达式,非常方便

# int类型:直接计算出结果
print(f'{3*21}')                   # 63
​
​
# str类型
name = 'yuluo'
print(f"全部大写:{name.upper()}")   # 全部大写:YULUO
​
​
# dict类型
teacher = {'name': '雨落', 'age': 18}
msg = f"The teacher is {teacher['name']}, aged {teacher['age']}"
print(msg)                         # The teacher is 雨落, aged 18
​
​
# list类型
l1 = ['雨落', 18]
msg = f'姓名:{l1[0]},年龄:{l1[1]}.'
print(msg)                         # 姓名:雨落,年龄:18.

7.8.3 插入表达式

可以用函数完成相应的功能,然后将返回值返回到字符串相应的位置

def sum_a_b(a,b):
    return a + b
    
a = 1
b = 2
print('求和的结果为' + f'{sum_a_b(a,b)}')
​
# 输出结果:
求和的结果为3

7.8.4 支持多行f

可以把一句话分为多行表达,使用多个f进行表示(\ 表示换行符(续行符))

name = 'yuluo'
age = 18
ajd = 'handsome'

speaker = f'Hi {name}.'\
f'You are {age} years old.'\
f'You are a {ajd} guy!'
print(speaker)

# 输出结果:
Hi yuluo.You are 18 years old.You are a handsome guy!

7.8.5 其他问题

遇到多个花括号{}时,至多取n/2个

print(f"{{73}}")            # {73}      两个取一个
print(f"{{{73}}}")          # {73}      三个取一个
print(f"{{{{73}}}}")        # {{73}}    四个取两个
print(f"{{{{{73}}}}}")      # {{73}}    五个取两个
print(f"{{{{{{73}}}}}}")    # {{{73}}}  六个取三个

! , : { } ; 这些标点不能出现在{} 这里面,例如print( f' { ; 12}' )会报错,所以使用lambda表达式会出现一些问题,解决方式:可将lambda嵌套在圆括号里面解决此问题

x = 5
print(f'{(lambda x: x*2) (x)}')     # 10

总结:f-string的格式化输出更加简洁,方便,易读。而且他的处理速度对之前的%s 或者format 有了较高的提升,所以以后尽量使用此种格式化输出

8. 迭代器

8.1 可迭代对象

8.1.1 可迭代对象定义

  • 可迭代对象从字面意思来说就是一个可以重复取值的实实在在的东西

  • 到目前为止我们接触到的可迭代对象有str list tuple dic set range 文件句柄等,那么int,bool这些为什么不能称为可迭代对象呢,有什么方法可以判别一下呢

  • 可迭代对象的判别方法:

    在python中,但凡内部含有 __iter__ 方法的对象,都是可迭代对象

8.1.2 查看对象内部方法

该对象内部含有什么方法除了看源码还有什么其他的解决方式么?当然有了, 可以通过dir() 去判断一个对象具有什么方法

s1 = 'yuluo'
print(dir(s1))
​
# 输出结果:
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__iter__', '__le__', '__len__', '__lt__',
'__mod__', '__mul__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold',
'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format',
'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal',
'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable',
'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip',
'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace',
'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split',
'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate',
'upper', 'zfill']

dir()会返回一个列表,这个列表中含有该对象的以字符串的形式所有方法名。这样我们就可以判断python中的一个对象是不是可迭代对象了:

i = 100
s1 = 'yuluo'
​
print('__iter__' in dir(i))   # False 说明i不是可迭代对象
print('__iter__' in dir(s1))  # True  说明s1这个字符串是可迭代对象

8.1.3 小结

1、从字面意思来说:可迭代对象就是一个可以重复取值的实实在在的东西;从专业角度来说:但凡内部含有 __iter__ 方法的对象,都是可迭代对象

2、可迭代对象的优点:可以直观的查看里面的数据

3、可迭代对象的缺点:

  • 占用内存
  • 可迭代对象不能迭代取值(除去索引,key以外)

那么这个缺点有人就提出质疑了,即使抛去索引,key以外,这些我可以通过for循环进行取值呀!对,他们都可以通过for循环进行取值,其实for循环在底层做了一个小小的转化,就是先将可迭代对象转化成迭代器,然后在进行取值的。那么接下来,我们就看看迭代器

8.2 迭代器

8.2.1 迭代器的定义

1、从字面意思来说迭代器,是一个可以迭代取值的工具,器:在这里当做工具比较合适

2、从专业角度来说,迭代器是这样的对象:实现了无参数的 __next__ 方法,返回序列中的下一个元素,如果没有元素了,那么抛出StopIteration异常

3、python中的迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代

4、简单来说:

在python中,内部含有 __iter__ 方法并且含有 __next__ 方法的对象就是迭代器

8.2.2 迭代器的判断方法

通过dir() 方法,判断对象是否具有__next__ 方法

o1 = '雨落'
o2 = [1, 2, 3]
o3 = (1, 2, 3)
o4 = {'name': '雨落','age': 18}
o5 = {1, 2, 3}
f = open('file',encoding='utf-8', mode='w')
​
print('__iter__' in dir(o1))  # True
print('__iter__' in dir(o2))  # True
print('__iter__' in dir(o3))  # True
print('__iter__' in dir(o4))  # True
print('__iter__' in dir(o5))  # True
print('__iter__' in dir(f))   # True
​
​
print('__next__' in dir(o1))  # False
print('__next__' in dir(o2))  # False
print('__next__' in dir(o3))  # False
print('__next__' in dir(o4))  # False
print('__next__' in dir(o5))  # False
print('__next__' in dir(f))   # True

f.close()

通过以上代码可以验证,之前我们学过的这些对象:str list tuple dict set range,只有文件句柄是迭代器,剩下的那些数据类型都是可迭代对象

 

8.2.3 可迭代对象如何转化成迭代器

通过 __iter__ 方法

l1 = [1, 2, 3, 4, 5, 6]
obj = l1.__iter__()      # 或者 iter(l1)
print(obj)
 
# 输出结果: 
<list_iterator object at 0x000002057FE1A3C8>

8.2.4 迭代器取值

可迭代对象是不可以一直迭代取值的(除去用索引,切片以及Key),但是转化成迭代器就可以了,迭代器是利用 __next__() 进行取值

l1 = [1, 2, 3,]
obj = l1.__iter__()     # 或者 iter(l1)
​
ret = obj.__next__()
print(ret)              # 第一个__next__()取到了1
ret = obj.__next__()
print(ret)              # 第二个__next__()取到了2
ret = obj.__next__()
print(ret)              # 第三个__next__()取到了3
ret = obj.__next__()  # StopIteration
print(ret)              # 第四个__next__()超范围了,取不到值,报错
​
'''迭代器利用next取值:一个next取对应的一个值,如果迭代器里面的值取完了,还要next,
那么就报StopIteration的错误'''

8.2.5 while模拟for的内部循环机制

刚才我们提到了,for循环的循环对象一定要是可迭代对象,但是这不意味着可迭代对象就可以取值,因为for循环的内部机制是:将可迭代对象转换成迭代器,然后利用next进行取值,最后利用异常处理处理StopIteration抛出的异常

l1 = [1, 2, 3, 4, 5, 6]
​
# 1 将可迭代对象转化成迭代器
obj = iter(l1)
​

# 2 利用while循环,next进行取值
while 1:
    

# 3 利用异常处理终止循环
try:
    print(next(obj))
except StopIteration:
    break

8.2.6 小结

1、从字面意思来说:迭代器就是可以迭代取值的工具;从专业角度来说:在python中,内部含有 __iter__ 方法并且含有 __next__ 方法的对象就是迭代器

2、迭代器的优点:

  • 节省内存:迭代器在内存中相当于只占一个数据的空间:因为每次取值都上一条数据会在内存释放,加载当前的此条数据
  • 惰性机制:next一次,取一个值,绝不过多取值

3、迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式

4、迭代器的缺点:

  • 不能直观的查看里面的数据
  • 取值时不走回头路,只能一直向下取值
l1 = [1, 2, 3, 4, 5, 6]
obj = iter(l1)
​
for i in range(2):
    print(next(obj))    # 指针从1的前面到达3的前面,即打印1 2
​
for i in range(2):
    print(next(obj))    # 指针从3的前面到达5的前面,即打印3 4
    
# 输出结果:
1
2
3
4

8.3 可迭代对象与迭代器对比

8.3.1 可迭代对象

1、是一个私有的方法比较多,操作灵活(比如列表,字典的增删改查,字符串的常用操作方法等),比较直观,但是占用内存,而且不能直接通过循环迭代取值的这么一个数据集

2、应用:

当你侧重于对于数据可以灵活处理,并且内存空间足够,将数据集设置为可迭代对象是明确的选择

8.3.2 迭代器

1、是一个非常节省内存,可以记录取值位置,可以直接通过循环next方法取值,但是不直观,操作方法比较单一的数据集

2、应用:​​​​​​

当你的数据量过大,大到足以撑爆你的内存或者你以节省内存为首选因素时,将数据集设置为迭代器是一个不错的选择。(可参考为什么python把文件句柄设置成迭代器)

9. 生成器

9.1 生成器初识

生成器的本质就是迭代器,在Python社区中,大多数时候都把迭代器和生成器是做同一个概念。生成器和迭代器唯一的不同就是:迭代器都是Python给你提供的已经写好的工具或者通过数据转化得来的,(比如文件句柄,iter([1,2,3])。生成器是需要我们自己用Python代码构建的工具

9.2 生成器的构建方式

在python中有三种方式来创建生成器:

  • 1、通过生成器函数
  • 2、通过生成器推导式
  • 3、Python内置函数或者模块提供(其实1,3两种本质上差不多,都是通过函数的形式生成,只不过1是自己写的生成器函数,3是Python提供的生成器函数而已)

9.3 生成器函数yield

首先,我们先看一个很简单的函数:

def func():
    print(11)
    return 22
​
ret = func()
print(ret)
​
# 运行结果:
11
22

将函数中的return换成yield,这样func就不是函数了,而是一个生成器函数

def func():
    print(11)
    yield 22

我们这样写没有任何的变化,这是为什么呢?我们来看看函数名加括号获取到的是什么

def func():
    print(11)
    yield 22
​
ret = func()
print(ret)
​
# 运行结果:
<generator object func at 0x000001A575163888>

运行的结果和最上面的不一样,为什么呢?由于函数中存在yield,那么这个函数就是一个生成器函数,我们在执行这个函数的时候,就不再是函数的执行了,而是获取这个生成器对象

那么生成器对象如何取值呢?

之前我们说了,生成器的本质就是迭代器,迭代器如何取值,生成器就如何取值。所以我们可以直接执行 __next__() 来执行以下生成器

def func():
     print("111")
     yield 222
​
gener = func()            # 这个时候函数不会执行. ⽽是获取到生成器
ret = gener.__next__()    # 这个时候函数才会执行
​
print(ret)                # 并且yield会将func生产出来的数据 222 给了 ret 
​
# 输出结果:
111
222

并且我的生成器函数中可以写多个yield

def func():
    print("111")
    yield 222
    print("333")
    yield 444
​
gener = func()
ret = gener.__next__()     # 执行__next__()后,打印111,指针停在第一个yield后面
print(ret)                 # 把222 的值返回给第一个yield
​
ret2 = gener.__next__()    # 执行__next__()后,打印333,指针停在第二个yield后面
print(ret2)                # 把444 的值返回给第二个yield
​
ret3 = gener.__next__()    #所有yield执行完毕,再次__next__()程序报错
print(ret3)

当程序运行完最后一个yield,那么后面继续运行__next__()程序会报错,一个yield对应一个next,next超过yield数量,就会报错,与迭代器一样

yield与return的区别:

  • return一般在函数中只设置一个,他的作用是终止函数,并且给函数的执行者返回值
  • yield在生成器函数中可设置多个,他并不会终止函数,next会获取对应yield生成的元素

举例:我们来看一下这个需求:我向楼下卖包子的老板订购了10000个包子,包子铺老板非常实在,一下就全部都做出来了

def eat():
    lst = []
    for i in range(1,10000):
        lst.append('包子'+str(i))
    return lst
​
e = eat()
print(e)

这样做没有问题,但是我们由于学生没有那么多,只吃了2000个左右,剩下的8000个,就只能占着一定的空间,放在一边了。如果包子铺老板效率够高,我吃一个包子,你做一个包子,那么这就不会占用太多空间存储了,完美

def eat():
    for i in range(1,10000):
        yield '包子'+str(i)
​
e = eat()
for i in range(200):
    next(e)

这两者的区别:

  • 第一种是直接把包子全部做出来,占用内存
  • 第二种是吃一个生产一个,非常的节省内存,而且还可以保留上次的位置
def eat():
    for i in range(1,10000):
        yield '包子'+str(i)
​
e = eat()
​
for i in range(200):
    next(e)
    
for i in range(300):
    next(e)
    
# 多次next包子的号码是按照顺序记录的

9.4 send()方法

next只能获取yield生成的值,但是不能传递值

def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield
        print(f'{name} start to eat {food}')

people = gen('yuluo')
next(people)
next(people)
next(people)
​
# 输出结果:
yuluo ready to eat
yuluo start to eat None
yuluo start to eat None

而使用send这个方法是可以传递值的

def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield 222
        print(f'{name} start to eat {food}')

people = gen('yuluo')
next(people)               # 第一次必须用next让指针停留在第一个yield后面
                           # 与next一样,可以获取到yield的值
ret = people.send('baozi')
print(ret)                 # ret接收到yield的返回值222

# 输出结果:
yuluo ready to eat
yuluo start to eat baozi
222

还可以给上一个yield发送值

def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield
        print(f'{name} start to eat {food}')

people = gen('yuluo')
next(people)
people.send('包子')
people.send('饺子')
people.send('面条')

# 输出结果:
yuluo ready to eat
yuluo start to eat 包子
yuluo start to eat 饺子
yuluo start to eat 面条

send和next()区别:

相同点:

  • send 和 next()都可以让生成器对应的yield向下执行一次
  • 都可以获取到yield生成的值

不同点:

  • 第一次获取yield值只能用next不能用send(可以用send(None))
  • send可以给上一个yield置传递值

9.5 yield from

可以直接把可迭代对象中的每一个数据作为生成器的结果进行返回

---------- yield:返回一个列表 ----------

def func():
    lst = ['唱跳','rap','篮球']
    yield lst
g = func()
print(next(g))       # ['唱跳','rap','篮球']
​
​
------ yield from:把元素依次迭代返回 ------

def func():
    lst = ['唱跳','rap','篮球']
    yield from lst
g = func()
print(next(g))       # 唱跳
print(next(g))       # rap
print(next(g))       # 篮球

yield from是将列表中的每一个元素返回,如果写两个yield from 并不会产生交替的效果

def func():
    lst1 = ['1', '2']
    lst2 = ['3', '4']
    yield from lst1
    yield from lst2
​
g = func()
for i in g:
    print(i)
​
# 输出结果:
1
2
3
4

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值