Python生成器
生成器
根据程序设计者制定的规则循环生成数据,当条件不成立时则生成数据结束。
数据不是一次性全部生成出来,而是使用一个,再生成一个,可以节约大量的内存

创建生成器的方法:
生成器的推导式
生成器推导式 => (生成器推导式 => 数据的生成规则)
创建生成器:
python
my_generator = (i * 2 for i in range(5))
print(my_generator)
my_generator 本质是一个对象,其内部并没有具体的数据,只有数据的生成规则
运行验证如下:

next获取生成器下一个值
生成器中有一个关键的函数 => next()=> 每调用一次,其就会根据生成器的数据生成规则,创建一个元素。
然后向下移动
再次调用next()函数,则根据生成器的数据生成规则,在创建一个元素
python
#next获取生成器下一个值
value = next(my_generator)
print(value)
遍历生成器
python
for value in my_generator :
print(value)
输出结果:

生成器相关函数:
next函数获取生成器中的下一个值
for循环遍历生成器中的每一个值
总结:
1.生成器中并没有真正的保存具体的数据,其返回结果是一个对象,对象中保存了所有数据的生成规则
2.生成器中有一个特别重要的函数next(),每调用一次next()则自动生成器中根据生成规则获取一个元素
3.生成器的意义:降低程序的能耗(因为不需要存储大量的数据,只是在调用next()时,才生成一个元素,所有能耗大大降低)
生成器作用案例
1.使用列表存储一组数据,数据一共有1000万个,要求存储时,每个列表元素是循环次数的平方:
python
'''
生成器可以降低程序的能耗,加快程序的执行时间
'''
#导入time模块
import time
#获取当前程序开始的时间
start = time.time()
#使用列表存储一组数据,数据一共有100万个,要求存储时,每个列表元素是循环次数的平方
square_nums = [i * i for i in range(10000000)]
#获取程序结束前的时间
end = time.time()
#计算程序执行的总时间
print(f'程序执行时间:{end - start}')
执行时间:

2.使用列表存储一组数据,数据一共有1000万个,要求存储时,每个列表元素是循环次数的平方,查看内存使用:
列表推导式
python
import tracemalloc
tracemalloc.start()
data = [i for i in range(1000000)]
# data = (i * i for i in range(1000000))
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
输出结果:

生成器推导式
python
import tracemalloc
tracemalloc.start()
data = (i * i for i in range(1000000))
# 真正消费生成器
res = list(data)
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
输出结果:

推到总结:
1、内存占用核心差异
- 列表推导式
[]
- 程序运行瞬间,把 100 万个元素全部存入内存;
- 需要开辟一块连续内存保存全部数值;
- tracemalloc 会观测到巨大内存占用;
- 优点:可以随机索引
data[5000]、重复遍历; - 缺点:数据量大时极易造成内存暴涨、OOM。
- 生成器表达式
()
- 惰性计算(懒加载):不会一次性生成所有元素;
- 仅保存迭代逻辑、当前状态,不会预存全部结果;
- 只有调用
next()/for 循环迭代时,才临时算出单个值,用完就丢弃; - tracemalloc 观测到内存占用极低;
- 限制:只能单向遍历一次,不支持下标随机访问。
2、两份代码观测结果说明
第 1 段(列表推导式)输出:会看到明显很大的内存占用行数 第 2 段(生成器)输出:几乎看不到高额内存占用
yield生成器(在def函数中具有yield关键字)
yield生成器,其结构一共分为两部分:
1.首先要定义一个函数
2.在函数内部存在一个yield关键字
所以我们把1.2组合在一起就称之为yield生成器
另外要特别强调:yield生成器是一个对象而不是一个函数
重点:理解yield生成器的执行流程(其使用方式和意义和刚才介绍的一模一样)
python
def generator(n):
for i in range(n):
print('开始生成...')
yield i
print('完成一次...')
#使用时,由于生成器需要传递参数,所以通常将其赋予给某个变量
g = generator(5)
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
输出结果:

问题现象:

现象分析:
- 遇见 yield → 暂停,返回数据;下次 next 从暂停位置继续往下执行 (暂时把yield当做return理解,每次遇到yield,生成器就相当于执行1次)
- 调用
generator(5)不会执行函数代码,只是创建生成器对象 - 如果继续调用第 6 次
next(g):循环结束,没有下一个 yield,直接抛出StopIteration异常 - 和普通 return 区别:
return:直接终止函数,无法恢复;yield:暂停函数,保留上下文,可以再次恢复运行
用yield生成斐波那契数列
数学中有个著名的斐波那契数列要求:数列中第一个数为1,第二个数也为1,其后每个数都可由前两个数相加得到:例子:1.2.2.3.5.8...现在我们使用生成器来实现这个斐波那契数列,每次取值都通过算法来生成下一个数据,生成器每次调用只生成一个数据,可以节省大量的内存。
python
def fib(max):
n,a,b = 0,0,1
while n < max:
yield b
a,b = b,a+b
n += 1
for n in fib(5):
print(n)
算法分析:

输出结果:

代码分析:
fib(5):创建生成器对象,不执行函数内部代码- 进入 for 循环迭代生成器,每次迭代触发一次
next()
- 第 1 次:
yield b=1→ 输出 1,暂停 - 赋值迭代:
a=1,b=0+1=1,n=1 - 第 2 次:
yield b=1→ 输出 1,暂停 - 赋值迭代:
a=1,b=1+1=2,n=2 - 第 3 次:
yield b=2→ 输出 2,暂停 - 赋值迭代:
a=2,b=1+2=3,n=3 - 第 4 次:
yield b=3→ 输出 3,暂停 - 赋值迭代:
a=3,b=2+3=5,n=4 - 第 5 次:
yield b=5→ 输出 5,暂停 - n=5,不满足循环条件,生成器遍历结束
结果分析:
- 生成器特性 :
yield暂停函数、返回数据,下次迭代从暂停处继续执行 - 优势 :不用一次性生成完整数列,节省内存,适合海量数据迭代
for循环可以直接遍历生成器,自动调用next(),结束后自动捕获StopIteration异常,不报错
python中的深浅拷贝
基础概念:
1.变量:是一个系统表的元素,拥有指向对象的连接空间
2.对象:被分配的一块内存,储存其所代表的值
3.引用:是自动形成的从变量到对象的指针
4.类型:属于对象,而非变量
5.不可变对象:一旦创建就不可修改的对象,包括字符串、元组、数值类型
(该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量在指向这个新地址)
6.可变对象:可以修改的对象,包括列表、字典、集合
(该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发送改变,并没有发送复制行为,也没有开辟新的地址,通俗点说就是原地改变)
当我们写:
python
a = "python"
python解释器干的事情:
1.创建变量a
2.创建一个对象(分配一块内存),来存储值python
3.将变量与对象,通过指针连接起来,从变量到对象的连接称为引用(变量引用对象)

通常来说 变量a 存储的"python"的地址
方法证明:
python
#定义一个变量A
a = 'python'
#python中存在一个id()方法,可以用于获取变量指向内存空间
print(id(a))
数据结果:

扩展:可变数据类型:(值内存空间一旦固定,其值是可以发生改变(列表、字典、集合))
python
#扩展:可变数据类型
a = [1,3,5]
print(id(a))
a.append(7)
print(id(a))
输出结果:

可变是指值可以改变,但是a中存储的地址却为不变。
扩展:不可变数据类型:(值内存空间一旦固定,其值是不可以发生改变(数值、布尔、字符串、元组))
python
b = 10
print(id(b))
b = 'hello'
print(id(b))
输出结果:

代码分析:
-
b = 10Python 在内存创建 整数对象 10 ,变量b引用这个对象; 执行print(id(b))→ 输出第一个内存地址1683870556。 -
b = 'hello'⚠️ 不是修改原来 10 这个对象! Python 在内存新建一个 字符串对象 'hello' ,然后让变量b重新指向这个新对象 。 原先数字10的对象还存在内存中(没有变量引用,后续会被垃圾回收)。
执行 print(id(b)) → 输出新地址 72460480。
两次打印 id 结果不一样,证明:变量 b 指向了两个完全不同的内存对象。
赋值
赋值:只是复制了新对象的引用,不会开辟新的内存空间。
并不会产生一个独立的对象单独存在,只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。
python
#定义一个变量a
a = [1,3,5]
print(id(a))
#把变量a赋值给变量B
b = a
print(id(b))
输出结果:

变量a与变量b都指向同一个地址
浅拷贝
浅拷贝:创建新对象,其内容是原对象的引用。
浅拷贝之所以称为浅拷贝,是它仅仅只拷贝了一层,拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已
案例1:赋值

案例2:可变类型浅拷贝

可变类型的浅拷贝概念
浅拷贝(Shallow Copy)指创建一个新对象,开辟一块新空间,然后值拷贝。但新对象内部的子对象仍引用原始对象中的子对象。对于可变类型(如列表、字典),浅拷贝仅复制最外层的结构,嵌套的可变对象不会被递归复制。
实现浅拷贝的方法
方法1:使用 copy() 方法
适用于列表、字典等可变类型,直接调用内置的 copy() 方法生成浅拷贝。
python
original_list = [1, [2, 3], 4]
shallow_copy = original_list.copy()
类似于:

方法2:使用 list() 或 dict() 构造函数
通过类型构造函数生成新对象,但嵌套对象仍共享引用。
python
original_dict = {"a": [1, 2], "b": 3}
shallow_copy = dict(original_dict)
方法3:使用切片操作(仅限列表)
对列表进行全切片 [:] 可生成浅拷贝。
python
original_list = [1, [2, 3], 4]
shallow_copy = original_list[:]
浅拷贝的特性验证
修改浅拷贝的最外层元素不会影响原对象,但修改嵌套的可变对象会同步影响原对象。
python
original_list = [1, [2, 3], 4]
shallow_copy = original_list.copy()
shallow_copy[0] = 99 # 仅修改浅拷贝的外层
print(original_list) # 输出 [1, [2, 3], 4](原对象未变)
shallow_copy[1][0] = 88 # 修改嵌套的可变对象
print(original_list) # 输出 [1, [88, 3], 4](原对象同步变化)
案例3:不可变类型浅拷贝

不可变类型的浅拷贝
在Python中,不可变类型(如整数、字符串、元组等)的浅拷贝行为与可变类型(如列表、字典等)有所不同。不可变类型的浅拷贝通常不会创建新的对象,而是直接引用原始对象。
不可变类型的浅拷贝特点
- 不可变对象在内存中一旦创建就无法修改。
- 进行浅拷贝时,Python会直接引用原始对象,而不是创建新对象。
- 因为不可变对象无法修改,所以即使多个变量引用同一个对象,也不会产生副作用。
示例代码
python
a = 10 # 整数是不可变类型
b = a # 浅拷贝,实际上是引用同一个对象
print(id(a) == id(b)) # 输出True,说明a和b引用的是同一个对象
a = 20 # 重新赋值,此时a指向新对象
print(a, b) # 输出20 10,b仍然指向原来的对象
字符串的浅拷贝
python
s1 = "hello"
s2 = s1 # 浅拷贝
print(id(s1) == id(s2)) # 输出True
s1 = "world" # s1指向新对象
print(s1, s2) # 输出world hello
元组的浅拷贝
python
t1 = (1, 2, 3)
t2 = t1 # 浅拷贝
print(id(t1) == id(t2)) # 输出True
为什么不需要深拷贝不可变类型 由于不可变对象无法被修改,深拷贝对于不可变类型来说没有实际意义。无论是浅拷贝还是深拷贝,结果都是引用同一个对象。因此,对于不可变类型,通常不需要考虑拷贝的问题。
总结
- 不可变类型的浅拷贝只是创建对原始对象的新引用。
- 修改其中一个变量(实际上是重新赋值)不会影响其他变量。
- 在Python中,不可变类型的拷贝操作是高效且安全的。
深拷贝
深拷贝:和浅拷贝对应,深拷贝拷贝了对象的所以元素,包括多层嵌套的元素。深拷贝出来的对象是一个全新的对象,不再与原来的对象有任何关联。
所以改变原有被复制对象不会对已经复制出来的新对象产生影响。只有一种形式,copy模块中的deepcopy函数。
可变类型深拷贝:

代码:
python
#定义一个变量
import copy
a = [1,3,5,[2.5]]
#定义一个变量b,对变量a进行深拷贝
b = copy.deepcopy(a)
print(a)
print(b)
print(id(a))
print(id(b))
输出结果:

结论:
1.对于简单的可变数据类型,深拷贝可以对对象进行完全拷贝,生成一块独立的内存空间,两个变量之间没有任何关系
2.对于复杂的可变类型数据,深拷贝可以对对象进行完全拷贝,不仅可以拷贝外层对象,也可以拷贝内存对象,而且完全独立。
不可变类型深拷贝:
不可变类型进行深拷贝不会给拷贝的对象开辟新的内存空间,而只是拷贝内存地址
简单的不可变类型数据代码示例:
python
#定义一个变量
import copy
a = (1,2,3)
#定义一个变量b,对变量a进行深拷贝
b = copy.deepcopy(a)
print(a)
print(b)
print(id(a))
print(id(b))
输出结果:

结论:
对于简单的不可变类型数据,深拷贝也只能拷贝对象的引用关系,所以看到的结果是a和b指向了相同的内存空间
复杂的不可变类型数据代码:
python
#定义一个变量
import copy
a = (1,2,3,(4.8))
#定义一个变量b,对变量a进行深拷贝
b = copy.deepcopy(a)
print(a)
print(b)
print(id(a))
print(id(b))
输出结果:

结论:对于复杂的不可变类型数据,深拷贝也只能拷贝对象的引用关系,所以看到的结果是a和b指向了相同的内存空间
深浅拷贝的特殊案例:
1.python中深浅拷贝特殊案例(可变嵌套不可变类型)
外层拷贝
代码:
python
import copy
a = [1,3,5,(4,7)]
b = copy.copy(a)
c = copy.deepcopy(a)
print(a)
print(b)
print(c)
print(id(a))
print(id(b))
print(id(c))
输出结果:

内层拷贝:
代码:
python
import copy
a = [1,3,5,(4,7)]
b = copy.copy(a)
c = copy.deepcopy(a)
print(a)
print(b)
print(c)
print(id(a[3]))
print(id(b[3]))
print(id(c[3]))
'输出结果:

结果分析:
外层是可变类型,所以可以进行完全拷贝(需要生成内存空间),但是内层对象是不可变数据类型,所以智能拷贝引用关系
2.python中深浅拷贝特殊案例(不可变嵌套可变类型)
外层拷贝:
代码:
python
import copy
d = (1,3,5,[7,9])
e = copy.copy(d)
f = copy.deepcopy(d)
print('外层结构')
print(id(d))
print(id(e))
print(id(f))
print('内层结构')
print(id(d[3]))
print(id(e[3]))
print(id(f[3]))
输出结果:

特殊结论:如果是一个不可变数据类型包含了可变数据类型,浅拷贝结论与之前结论一致,都只能拷贝引用关系
但是对于深拷贝,这个有点不同了,如果这种类型使用深拷贝,其整体都可以进行完全拷贝
特殊:外层是不可变,内层是可变
结果:整体都可以进行完全拷贝,结果如下图所示

正则表达式
在实际开发过程中经常会由查找符合某些复杂规则的字符串的需要比如:邮箱,图片地址,手机号码等这时候想匹配或者查找符合某些规则的字符串就可以使用正则表达式了

概述:(正则表达式就是字符串的匹配规则,结果:匹配满足正则条件的字符串)
正则表达式描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。
模式:一种特定的字符串模式,这个模式是通过一些特殊的符合组成的
某种:也可以理解为是一种模糊匹配。
精准匹配:select * from blog where title = 'python'
模糊匹配:select * from blog where title like '%python%'
正则表达式并不是python所特有的,在其他语言中都支持正则表达式的。
正则表达式的功能
正则表达式的核心功能
数据验证
正则表达式常用于表单字段的格式验证,确保用户输入符合特定模式。例如验证手机号、邮箱、身份证号或IP地址的合法性。通过预定义规则快速判断输入是否有效,减少后端处理无效数据的压力。
手机号验证(中国大陆):
regex
^1[3-9]\d{9}$
邮箱验证(基础版):
regex
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
数据检索
在文本处理中快速定位符合特定模式的字符串,适用于日志分析、文本编辑或爬虫数据提取。支持复杂匹配规则如多条件组合、模糊匹配等。
提取HTML中的链接:
regex
<a\s+href="([^"]+)"
匹配日期格式(YYYY-MM-DD):
regex
\d{4}-\d{2}-\d{2}
数据脱敏
对敏感信息进行部分隐藏,平衡数据可用性与隐私保护。常用于显示姓名、联系方式等场景。
手机号脱敏(保留前3后4位):
regex
(\d{3})\d{4}(\d{4})
替换为:$1****$2
姓名脱敏(中文姓名保留首字):
regex
([\u4e00-\u9fa5])([\u4e00-\u9fa5]+)
替换为:$1**
数据过滤
识别并处理文本中的敏感词或非法内容,常用于论坛、聊天系统的内容审核。通过模式匹配实现批量过滤。
基础关键词过滤:
regex
(敏感词A|敏感词B|违规词)
替换为:***
高级过滤(忽略大小写和部分变形):
regex
(?i)s[!@#$%^&*]*e[!@#$%^&*]*x
实现要点
- 验证场景 :需严格限定字符串起止(
^和$) - 检索场景 :优先使用非贪婪匹配(
.*?) - 脱敏场景:注意分组捕获与反向引用
- 过滤场景:考虑性能优化(如预编译正则)
re模块的介绍
1.什么是re模块
在python中需要通过正则表达式对字符串进行匹配的时候,可以使用一个re模块
2.re模块使用三步走

注意:
第二步**:match():专门用于匹配以某些字符/字符串开头的内容(只能匹配开头)**
findall(目标,要查找的内容):匹配以某些字符/字符串开头的内容
第三步**:如果数据匹配失败,则返回None**
正则表达式快速入门
案例1:查找一个字符串中是否具有数字"8"
python
import re
result = re.findall('8','136526856541')
if result:
print('匹配成功')
else:
print('匹配失败')
输出结果:

案例2:查找一个字符串是否具有数字('\d'代表数字)
python
import re
result = re.findall('\d','13aadsda6526856541')
print(result)
输出结果:

案例3:查找一个字符串中所有的非数字字符('\D'代表非数字)
python
import re
str = 'assvsds5#%%^@'
result = re.findall('\D',str)
print(result)
输出结果:

正则表达式的编写(三步走:查什么,查多少,从哪查)
1.查什么

字符簇常见写法:
① abcdefg 代表匹配abcdefg字符中的任意某个字符(1个)
② aeiou 代表匹配a、e、i、o、u五个字符中的任意某个字符
③ a-z 代表匹配a-z之间26个字符中的任意某个
④ A-Z 代表匹配A-Z之间26个字符中的任意某个
⑤ 0-9 代表匹配0-9之间10个字符中的任意某个
python
import re
str1 = '0sdasfa125'
result = re.match('[0-9]',str1)
print(result.group())
输出结果:

⑥ 0-9a-zA-Z 代表匹配0-9之间、a-z之间、A-Z之间的任意某个字符
字符簇+托字节结合代表取反的含有:
① \^aeiou 代表匹配除了 a、e、i、o、u 以外的任意某个字符
② \^a-z 代表匹配除了 a-z 以外的任意某个字符
\d等价于0-9,代表匹配0-9之间的任意字符
\D等价于0-9,代表匹配0-9之外的任意字符
代码示例:
python
str3 = '$123545'
result = re.match('[^0-9]',str3)
print(result.group())
输出结果:

2.查多少

基本语法:
正则匹配字符.或者\w或\s+跟查多少
如\w{6,8}
如.*=>匹配前面的字符出现0次或者多次
代码示例:
python
import re
#定义一个字符串
str1 = '123abc'
# 用于匹配连续的三个数
pattern = r'\d{3}'
result1 = re.search(pattern,str1)
print(result1.group())
# 用于匹配多个字符(最少匹配0个,最多匹配n个)
str2 = 'src="ds25858,jpg'
result2 = re.match('.*',str2)
print(result2.group())
#匹配1或0
str3 = '1'
result3 = re.match('10?',str3)
print(result3.group())
结果:

3. 从哪查

1.^ 匹配字符串开头示例
示例 1:校验字符串是否以数字开头
python
import re
pattern = r'^\d' # 以数字开头
print(re.match(pattern, '123abc')) # 匹配成功,匹配到 '1'
print(re.match(pattern, 'abc123')) # 匹配失败,返回 None
示例 2:匹配以 http 开头的网址
python
import re
pattern = r'\d$'
print(re.search(pattern, '商品编号886')) # 匹配成功,匹配到 '6'
python
import re
url = 'https://www.baidu.com'
result = re.match(r'^https?', url)
if result:
print("是http/https开头的网址:", result.group())
2.$ 匹配字符串结尾示例
示例 1:校验域名是否以 .com 结尾
python
import re
pattern = r'\.com$'
print(re.search(pattern, 'www.baidu.com')) # 匹配成功
print(re.search(pattern, 'www.baidu.cn')) # 匹配失败
示例 2:校验字符串是否以数字结尾
python
import re
pattern = r'\d$'
print(re.search(pattern, '商品编号886')) # 匹配成功,匹配到 '6'
3.、^ + $ 组合使用(最常用:全字符串校验)
同时限定开头和结尾,要求整个字符串完全符合规则,是表单校验的标准写法
示例 :校验 6~8 位纯数字密码
python
import re
pattern = r'^\d{6,8}$'
print(re.match(pattern, '123456')) # 成功:恰好6位数字
print(re.match(pattern, '12345')) # 失败:不足6位
print(re.match(pattern, '123456789')) # 失败:超过8位
print(re.match(pattern, '123a56')) # 失败:包含非数字字符
核心作用:加了 ^$ 后,字符串必须从头到尾完全匹配规则,多一个字符、少一个字符都不通过。

