python基础补充(三)


前面两篇内容,我们详细讲述了python的对象,表达式,控制流程和异常处理,这次我们来补充最后的一点基础内容,迭代器和生成器以及python其他便利的特点。

迭代器和生成器

对于python来说,函数仍然是非常重要的一个内容,但是有些时候函数并不能很好地解决问题,比如调用一个函数需要占用太多的时间空间,然而运行的结果又只有一小部分有用,就会造成很大的浪费。针对这样的问题,有没有比函数更好的解决问题的方法呢?

迭代器

迭代器听着很陌生,但实际上已经是我们的老朋友了。比如,我们有这样一个循环:

a=5
for i in a:
    print(i)

就会获得一个如下错误:
在这里插入图片描述
错误提示为a是int类型,不是可迭代对象。说到这里,你是不是就清楚了?作为for循环的循环条件,一定是可迭代对象,而迭代器便是生成这种对象的。有一些基本的容器类型如列表,元组和集合,都可以定义为可迭代类型,文件可以产生行迭代,用户的自定义类型也是可支持迭代的。
由于我们常用for进行迭代,所以使用迭代器iter()生成可迭代对象并不多用。迭代的机制有以下规定:

  1. 如果对象a可迭代,我们可以通过iter(a)生成一个迭代器类型,这个迭代器类型会存储a的全部有效内容;

  2. 迭代器是一种对象,这类对象是由可迭代对象生成的。迭代器有一个专属的内置方法next(),他可以找到当前下标(首次调用为0)的值,并将下标进行+1操作:

    a=range(5)
    print(type(a))
    b=iter(a)
    print(next(b))
    print(b.__next__())
    # next是一个特殊的方法,这两种使用方式都是python认可的
    print()
    for i in b:
        print(i,end=' ')
    # 输出结果为:<class 'range'>
    #             0
    #             1
    #
    #             2 3 4 
    

这个例子可以证明两点,一是 for循环的循环条件是一个可迭代的类型生成的迭代器类型,不同于C语言,python的for不需要通过计数器+1并判断是否满足循环条件来进行,而是不断对迭代器类型调用next函数,直到遍历到迭代器的最后一个元素。二是 next调用过后,下标信息会得以保存。如果已经对迭代器类型调用过next,再将该迭代器作为for的循环体,那么将从该迭代器的当前下标继续遍历下去。
利用这个特点,我们可以查看被打断的for便利到了什么位置:

a=range(10)
i=1
b=iter(a)
for i in b:
    print(i,end=' ')
    if i==2:
        break

print('\n',next(b),sep='') # 检查for循环停止的位置
# 输出为:0 1 2 
#         3

需要注意的是,普通的可迭代对象是不可以调用next的,只有迭代器对象才可以,另外,next的特殊之处还远不止于此:

a=iter([1,2,3,4,5,6])
for i in range(2):
    print(next(a))
print('**************************')
for i in range(2):
    print(a.__next__()) # 可以这样引用next函数
print('**************************')
print(list(a)) # a是迭代器,即使使用类型转换,也会调用next函数进行输出,
               # 而调用next后取到元素后下标不会回调
print(list(a))

# 对比情况:
b=[1,2,3,4,5,6]
print(list(b))
print(list(b))
# 输出为:1
#         2
#         **************************
#         3
#         4
#         **************************
#         [5, 6]
#         []

#         [1, 2, 3, 4, 5, 6]
#         [1, 2, 3, 4, 5, 6]

生成器

学习过了迭代器,我们来看一看生成器。首先看看这段代码:

a=range(10)
print(a)
# 输出结果为:range(0, 10)

这个结果是不是有点出乎意料?其实博主第一次接触range时以为它生成的是一个列表或者元组的类型,但实际上并非如此。举个例子,for i in range(100000)这个语法是python可以执行的,但是如果这个循环在过程中被打断,而range(100000)生成了100000个元素的列表,是不是造成了时间和空间的浪费呢?为了解决这种问题,python设置了“偷懒”的机制,即这里的range(100000)是在循环运行之中一个一个的生成下一个数字,如果for被打断了,那后面的数字就不会被生成。这个思路在python中很常见,这也就是生成器功能的一个雏形。
生成器的语法实现类似于函数,但它不会返回值。为了显示序列中的每一个元素,我们会使用yield语句。下面我们看一个例子:

def fun1(n,a):
    for i in range(n):
        a.append(i)
    return a
def fun2(n,a):
    for i in range(n):
        a.append(i)
    yield a
    
a=[]
b=[]
print(fun1(10,a))
print(fun2(10,b))
j=0 # 设置计数器,记录for循环运行次数
for i in fun2(10,b):
    print(i)
    j+=1
print(j)
# 输出结果为:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#             <generator object fun2 at 0x000001946A6C2900>
#             [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#             1

可以看到,生成器函数会首先占住一个地址,但是仅当需要使用生成器生成内容时,里面的指令才会被执行。而生成器的“返回值”同样也是可迭代类型,所以我们可以把yield放到一个循环之中:

def fibonacci(): # 构建斐波那切数列生成器
    a=0
    b=1
    while True:
        yield a # 将a放入生成器数据
        future=a+b
        a=b
        b=future
j=0
a=[]
for i in fibonacci():
    a.append(i) # 由一串元素组成的某类型实例才能转换成列表,元组等类型
    j+=1
    if j==10:
        break # 没有终止条件,fibonacci数列会一直生成下去
print(a)
# 输出为:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

在这个生成器中,我们如果不设置终止条件,生成器会一直给出下一个值。如果把生成器换成函数,那么这样调用就会陷入死循环了。当然,我们还可以对生成器做个改进:

def fibonacci(n): # 让生成器可以决定生成数字的上限,
			      # 避免调用时没有设置终止条件导致死循环
    a=0
    b=1
    for i in range(n):
        a,b=b,a+b # 这种写法将在后面打包解包中介绍
        yield a 

这样的修改就会让生成器更合理。

python的其他便利特点

在这一节,我会继续给大家介绍一些Python中可以让代码变得简洁的其他写法。

解析语法

一种很常见的编程任务是基于另一个序列产生一系列的值。它的一般形式如下:

a=[expression for value in iterable if condition]

这句话在逻辑上就等价于:

a=[]
for value in iterable:
    if condition:
        a.append(expression) 
    # expression是基于value的表达式

举个例子,假如我们需要输出1到10中所有奇数的平方,就可以这样写:

a=[i**2 for i in range(10) if i%2!=0]
print(a)
# 输出为:[1, 9, 25, 49, 81]

以上的内容是简单的解析语法,它是一个列表解析。我们还可以用类似的语法生成集合解析,生成器解析,字典解析:

result1=(i**2 for i in range(10) if i%2!=0) # 生成器解析
print(result1)
result2={
    
    i:i**2 for i in range(10) if i%2!=0} # 字典解析
print(result2)
result3={
    
    i**2 for i in range(10) if i%2!=0} # 集合解析
print(result3)
# 输出结果为:<generator object <genexpr> at 0x000001CEE7F24BA0>
#             {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
#             {1, 9, 81, 49, 25}

当然,我们也可以把生成器解析当成可迭代对象当成for循环条件,大家可以自行实验。

序列类型的打包和解包

打包

不知道大家有没有尝试过给一个变量赋多个值呢?其实这种复制方法也是python所允许的,我们来看一下:

data=1,2,3,4,5
print(data,type(data))
# 输出为:(1, 2, 3, 4, 5) <class 'tuple'>

我们给data赋了五个值,而打印出来data是一个元组类型。这是由于python语法会把这五个数字自动打包成一个元组。有了这个设定,我们也就可以使用return返回多个值了,因为python会把它们自动打包成元组类型:

def tul(a,b):
    return a,b
print(tul(10,20))
# 输出结果为:(10, 20)

讲到这里,我们来稍作回忆。不知大家还是否记得可变参数传参呢?这实际上就是一种打包的实际应用,正常情况下,一个类型的元素只能赋值给一个函数参数,然而自动打包可以让一个参数接收多个被打包好的元素,这也就是为什么使用可变参数传递多个参数,函数中会收到一个元组类型的元素的原因:

def sum (*a):
    sum=0
    print(type(a))
    for i in a:
        sum+=i
    return sum
print(sum(1,3,5,7,9))
# 输出为:<class 'tuple'>
#         25

大家是不是更理解可变参数传参了呢?

解包

了解了打包,我们接下来看看对应的解包:

a,b,c=fibonacci(3) # 这里沿用之前的改进版斐波那切数列生成器
print(a,b,c)
# 输出为:1 1 2

对应的,我们用多个标识符去接收元组或生成器的内容(前提是标识符数量和元组或迭代器的长度必须相同),那么他们也会从左至右依次被赋予对应值。

同时分配

那么,如果我们把一系列的数字分配给一系列的标识符是不是可以呢?我们来看:

a,b,c=1,2,3
print(a,b,c)
# 输出为:1 2 3

这个就叫做同时分配,它会将右面的值打包成一个元组,然后将元祖解包,分给左面的标识符。有了这个技术,我们可以用简单的语句实现诸如交换变量之类的关联值:

a=1
b=2
a,b=b,a+b
print(a,b)
# 输出为:2 3

这段代码熟悉吗?这是我们对斐波那契数列生成器的改进代码。应用的就是自动分配技术,有了这项技术,我们就可以在不借助中间变量的情况下完成同时对两个或是多个变量值的修改。

作用域与命名空间

Python中,如果我们需要计算两个数的和,那么在计算之前,一定需要将这两个标识符与作为值的对象进行关联,这就是我们通常所理解的赋值,而这个赋值的过程实际上被称为名称解析
标识符都有一个有效的作用范围,最高级的赋值通常是全局变量,而如果在类似函数体内使用的普通标识符,它的有效作用范围只在函数体内,即局部变量。Python中每一个定义域使用了一个抽象名称,称为命名空间。命名空间管理当前作用域内的所有标识符。我们分析一下这段代码:

def space1(grades):
    gpa=3.56
    major='CS'
def space2(data):
    n=2
    target='A'
    item=data[2]

space1(['A-','B+','A-']) # 调用space1,自动生成命名空间
space2(['A-','B+','A-']) # 调用space2,自动生成命名空间

以下这是对命名空间的分析:
在这里插入图片描述
例子中这些标识符的作用域除列表外的作用域都只有各自的函数体。而如果写成这样的形式:

def space():
    arguments.append('B-')
    print(arguments)

arguments=['A-','B+','A-']
space()
print(arguments)
# 输出为:['A-', 'B+', 'A-', 'B-']
#        ['A-', 'B+', 'A-', 'B-']

从结果可以看出,全局变量可以直接在函数中使用。

第一类对象

第一类对象是指可以分配给标识符的类型实例,可以作为参数传递,或由一个函数返回。我们现在所知道的基本数据类型,基本都是第一类对象,函数本身也作为第一类对象处理。我们现在只需要理解作为第一类对象,是可以将其内容传递给其他的第一类对象或作为参数传递给函数即可。

模块和import

大家都应该知道我们调用了某个模块后,想要引用其中的函数,需要以下的语法:

import math
print(math.sqrt(2))
# 输出为:1.4142135623730951

如果我们不想写math.,可以用以下方式引用:

from math import sqrt
print(sqrt(2))

其实,还有一种更简单的方法,可以一下把一个模块中的所有函数以及变量全都引用过来,供我们直接调用:

from math import *
print(sqrt(2))

但是这种方法是有风险的,风险在于如果模块中有一些与当前命名空间冲突的函数,或与另一个模块导入的函数名重复,函数的内容就会彼此覆盖:

from math import *
def sin(n):
    return n
print(sin(pi))
# 输出为:3.141592653589793

覆盖的顺序是按照导入或定义的顺序来的,也就是说,我们先导入了sin,然后再定义一个sin,新定义的sin会覆盖掉引入的sin,反之自定义的sin就会被覆盖掉。
其实,使用from import虽然也能够产生覆盖的现象,但是由于引用的函数有限且明确,非常方便我们进行检查。因此,我更推荐使用from import进行引入。
下面,给大家介绍一些数据结构和算法相关的现有python模块:

模块名 描述
array 为原始类型提供了紧凑的数组存储
collection 定义额外的数据结构和包括对象集合的抽象基类
copy 定义通用函数来复制对象
heapq 提供基于堆的优先队列函数
math 定义常见的数学常数和函数
os 提供与操作系统交互
Re 对处理正则表达式提供支持
sys 提供了与Python解释器交互的额外等级
time 对测量时间或延迟程序提供支持
random 提供随机数生成

说到随机数,伪随机数也是实际中很常用的。大家可以看一看伪随机和真随机的区别
本节的结尾,再给大家仔细介绍一下Random类的实力支持的方法和random模块的顶级函数:

语法 描述
seed(hashable) 基于参数的散列值初始化伪随机数生成器
random() 在开区间(0.0,1.0)返回一个伪随机浮点值
randint(a,b) 在闭区间[a,b]返回一个为随机整数
randrange(start,stop,step) 在参数指定的python标准范围内返回一个伪随机整数
choice(seq) 返回一个伪随机选择的给定序列中的元素
shuffle(seq) 重新排列给定的伪随机序列中的元素

这些用法光听说是不够的,大家下去要自行练习。本节课给大家介绍了迭代器生成器,解析语法,打包解包,作用域空间和模块,理解这些对我们后续的数据结构学习以及编程能力的提升都有很大帮助。通过这三节,基本给大家补充了足够多的python基础知识,下节开始我们会继续对面对对象编程做一个补充,大家做好准备~

猜你喜欢

转载自blog.csdn.net/weixin_54929649/article/details/124358763