1. 食材
葱烧肥牛:肥牛700克,小葱20克,洋葱400克;
料汁:生抽50毫升,老抽15毫升,料酒40毫升,蚝油20克,糖10克,水150毫升。
焯水:葱10克,姜10克,料酒20毫升
肥牛700克 | 小葱20克 | 洋葱400克 | 生抽50毫升 | 老抽15毫升 | 料酒40毫升 | 蚝油20克 | 糖10克 | 水150毫升 | 葱10克 |
---|---|---|---|---|---|---|---|---|---|
姜10克 | 料酒20毫升 | 熟白芝麻 |
葱烧肥牛:肥牛700克,小葱20克,洋葱400克;
料汁:生抽50毫升,老抽15毫升,料酒40毫升,蚝油20克,糖10克,水150毫升。
焯水:葱10克,姜10克,料酒20毫升
肥牛700克 | 小葱20克 | 洋葱400克 | 生抽50毫升 | 老抽15毫升 | 料酒40毫升 | 蚝油20克 | 糖10克 | 水150毫升 | 葱10克 |
---|---|---|---|---|---|---|---|---|---|
姜10克 | 料酒20毫升 | 熟白芝麻 |
大米 | 胡萝卜 | 土豆 | 香菇 | 腊肠 or 午餐肉 | 洋葱 | 豌豆(青豆) | 玉米粒 |
---|
有啥菜就加啥菜
洗好大米;
胡萝卜、土豆、香菇、腊肉、洋葱:切丁;
把腊肠放入米中;
加盐、生抽、老抽(上色)、蚝油——>抓匀;
土豆、洋葱——>加盐(3g)、糖(2g)、黑胡椒(2 拧)、香油——>拌匀;
放入电饭锅:
油拌过的土豆、洋葱,铺在电饭锅的底部;
接着下入米饭(包含腊肉)
最后放入配菜:胡萝卜、土豆、香菇、玉米粒、腊肉、豌豆(青豆)
加入水,和米是 1:1
AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Web、Linux」,招收学员面向国内外,国外占 80%。全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh
包菜 | 黄豆芽 | 上海青 | 油麦菜 | 干辣椒 |
---|---|---|---|---|
花椒 | 蒜 | 香葱 | 生姜 | 里脊肉 |
郫县豆瓣酱 | 鸡蛋 | 玉米淀粉 | 盐 | 白胡椒粉 |
白糖 | 老抽 | 蚝油 | 鸡精 | 白芝麻 |
Use Python advanced programming to develop a computer program
Achievement | Achievement with Merit | Excellence |
---|---|---|
Use advanced programming techniques to develop a computer program. | Use advanced programming techniques to develop an informed computer program. | Use advanced programming techniques to develop a refined computer program. |
前面我们提到的起始值非常重要。如果我们现在不再是从 0 开始,而是从一个更重要的数值(比如业务中的关键参数)开始,并且这个值必须要包含在计算的结果中,那你就不能先进行 num += 1
操作了。
下面几个例子说明起始值可能的重要性:
例如,假设我们要计算从起始值 50 到 100 的和,这个起始值 50 是非常重要的,必须要被包含在结果内。
如果一开始就把次数 +1 放在求和操作之前,会出现什么问题?
start = 50 # 起始值,从 50 开始
total = 0 # 保存累加结果
num = start
while num <= 100:
num += 1
total += num
print(f"从 {start} 到 100 的整数之和是:{total}")
运行后输出如下结果:
从 50 到 100 的整数之和是:3876
待会统一分析,正确的代码实现应该是这样的:
start = 50 # 起始值,从 50 开始
total = 0 # 保存累加结果
num = start
while num <= 100:
total += num # 必须首先使用起始值
num += 1 # 然后才对 num 进行递增
print(f"从 {start} 到 100 的整数之和是:{total}")
输出如下结果:
从 50 到 100 的整数之和是:3825
在第一个代码块中,循环体内先执行了 num += 1
,这导致了两个主要变化:
num
从 50 变成 51,然后将 51 加入总和,因而初始的 50 没有被累加。num
达到 100 时,循环内先将其增加到 101,再将 101 加入总和,因而最终累加的数列是 51 到 101,而不是 50 到 100。num += 1
的提前,导致在循环内得到的 101 没有经过循环的判断。因此,第一个代码块累加的是 51 到 101 的整数,其总和比第二个代码块(累加 50 到 100)的总和多了 101−50=51 。
简单来说,就是因为递增操作的位置不同导致了累加的区间不同,最终结果也不同。在未来操作时一定要注意 +1 的位置。
下面这个例子,和例子一很像。但是我在写的时候感觉,例子1好理解。例子2也有自身的特点,故而留着。
假设起始值是某个账户的初始余额 100 元,你想计算累计增加到 150 元的总金额:
initial_balance = 100
target_balance = 150
total_balance = 0
balance = initial_balance
while balance <= target_balance:
total_balance += balance
balance += 1
print(f"账户余额从 {initial_balance} 元到 {target_balance} 元的累计金额是:{total_balance}")
比如记录某事件发生的第一天到第 7 天的总次数需要如何变写呢?具体来说,操作顺序不同会造成完全不同的结果:如果 day += 1
放在输入事件次数之前,那么第一天的事件数据就被跳过了。
错误示范:
day = 1
total_events = 0
while day <= 7:
day += 1 # 提前增加日期,导致跳过第一天的输入
events_today = int(input(f"请输入第 {day} 天的事件次数: "))
total_events += events_today
print(f"7天内事件总次数是:{total_events}")
正确的顺序应该是先记录事件数据再增加日期,这样第一天的记录才能正常被统计进去。
正确示范:
day = 1
total_events = 0
while day <= 7:
events_today = int(input(f"请输入第 {day} 天的事件次数: "))
total_events += events_today
day += 1 # 记录完成后再增加日期
print(f"7天内事件总次数是:{total_events}")
假设你每天打卡签到,一旦错过了第一天,就会影响到连续打卡奖励。
错过第一天的版本:
day = 1
consecutive_days = 0
while day <= 7:
day += 1
sign_in = input(f"第 {day} 天,你打卡了吗?(yes/no): ")
if sign_in.lower() == 'yes':
consecutive_days += 1
else:
consecutive_days = 0 # 一旦错过一天,连续打卡数清零
print(f"你连续打卡了 {consecutive_days} 天!")
运行后输出如下:
第 2 天,你打卡了吗?(yes/no): yes
第 3 天,你打卡了吗?(yes/no): yes
第 4 天,你打卡了吗?(yes/no): yes
第 5 天,你打卡了吗?(yes/no): yes
第 6 天,你打卡了吗?(yes/no): yes
第 7 天,你打卡了吗?(yes/no): yes
第 8 天,你打卡了吗?(yes/no): yes
你连续打卡了 7 天!
一周 7 天,还出现了 8 天。显然是不对的,原因也是很明显的把 day += 1
,放在前面跳过了第一天。
正确代码如下:
day = 1
consecutive_days = 0
while day <= 7:
sign_in = input(f"第 {day} 天,你打卡了吗?(yes/no): ")
if sign_in.lower() == 'yes':
consecutive_days += 1
else:
consecutive_days = 0 # 一旦错过一天,连续打卡数清零
day += 1
print(f"你连续打卡了 {consecutive_days} 天!")
这些例子清楚地展示了初始值的必要性和关键性,特别是操作顺序的重要性,提醒我们在循环设计时一定要谨慎对待起始值和操作顺序的问题。
获取用户输入,把用户输入的数据转换成对应的类型。此题只需要考虑数据:整数、浮点数即可。
编写程序,通过循环的方式,不断从用户处获取输入的数据,并根据输入内容自动判断并转换为对应的数字类型(整数或浮点数)。具体实现要求如下:
使用循环持续接收用户的输入,直至用户选择退出程序。
判断与转换逻辑:
10
、-5
),则将其转换成整数类型。3.14
、-0.01
),则将其转换成浮点数类型。提示与反馈要清晰易懂,使用户明确知道输入是否有效。
建议实现提示: 可以使用字符串方法,如 split()
、count()
或 replace()
,帮助识别并判断输入内容的类型。
示例交互效果:
请输入一个数字(输入 q 退出):10
你输入的是整数:10
请输入一个数字(输入 q 退出):3.14
你输入的是浮点数:3.14
请输入一个数字(输入 q 退出):hello
输入不合法,请重新输入!
请输入一个数字(输入 q 退出):q
退出程序。
获取用户输入肯定是使用 input()
函数,而 input()
函数得到的肯定是字符串。既然要判断是否是合法的整数,这个应该是所有问题中最简单的:直接判断字符串是不是纯数字字符串,如果是纯数字字符串必然是合法的整数!
不过上面的策略还不够全面,我们还需要考虑负数的情况。对于负数的字符串,在判断是否是纯数字时可以通过吗?我们来使用代码测试一下:
numbers = '-12'
print(numbers.isdigit())
运行后输出如下:
False
显然,无法通过 .isdigit()
函数判断。对于这种情况,我们必须手动实现判断。
那么,我们人是如何判断一个字符串是一个合法的负数整数呢?
例如下面字符串负数整数:
numbers = '-12'
我们大脑是怎么确定变量 numbers
是合格的负整数呢?
那么我们代码中就可以使用什么来判断这两个条件呢?
.isdigit()
函数进行判断呢?因为有其它必要的符号存在,故而解决方案也很简单:我们就把用户输入的字符串拆开,然后分别判断。.startswith('-')
判断字符串开头,接着再提取出除开头的负号后,使用 .isdigit()
判断剩下的字符串是不是纯数字字符串。如果这两个条件都得到 True,表明是标准且合法的负整数。如何判断用户输入的是合法的浮点数呢?这个我们可以借助浮点数拥有的特点来实现,我们先来看一下浮点数有几种类型:
# 类型一:
numbers = '12.22'
# 类型二:
numbers = '-12.22'
在继续阅读之前,你先自己思考一下:作为一个人,我们是如何判断一个浮点数是否合法?
不管是类型一还是类型二,有什么共同的特点?合法的浮点数拥有如下特点:
为什么我们无法直接使用 .isdigit()
来判断是否是纯数字字符串呢?——因为,小数点的存在和符号的存在。那么我们就拆开!
.replace()
去除小数点之后,查看是否为纯数字。难点1、难点2已经解决。只要不符合,直接使用 else 即可。
先实现单次的判断(程序),再实现循环。循环只是重复:单次的判断逻辑(程序)。
获取用户输入
user_input = input("请输入一个数字(输入 q 退出):")
判断用户输入的数据,是否是整数
user_input.isdigit()
;user_input.startswith('-') and user_input[1:].isdigit())
;if user_input.isdigit() or (user_input.startswith('-') and user_input[1:].isdigit()):
number = int(user_input)
print(f"你输入的是整数:{number}")
判断用户输入的数据,是否是浮点数
user_input.count('.') == 1
;user_input.replace('.', '').isdigit()
;user_input.startswith('-') and user_input[1:].replace('.', '').isdigit()
;elif user_input.count('.') == 1:
if user_input.replace('.', '').isdigit() or (user_input.startswith('-') and user_input[1:].replace('.', '').isdigit()):
number = float(user_input)
print(f"你输入的是浮点数:{number}")
else:
print("输入不合法,请重新输入!")
完成单次判断程序
user_input = input("请输入一个数字(输入 q 退出):")
if user_input.isdigit() or (user_input.startswith('-') and user_input[1:].isdigit()):
number = int(user_input)
print(f"你输入的是整数:{number}")
elif user_input.count('.') == 1:
if user_input.replace('.', '').isdigit() or (user_input.startswith('-') and user_input[1:].replace('.', '').isdigit()):
number = float(user_input)
print(f"你输入的是浮点数:{number}")
else:
print("输入不合法,请重新输入!")
else:
print("输入不合法,请重新输入!")
运行测试如下
# 测试一
请输入一个数字(输入 q 退出):12
你输入的是整数:12
# 测试二
请输入一个数字(输入 q 退出):-12
你输入的是整数:-12
# 测试三
请输入一个数字(输入 q 退出):-12.3
你输入的是浮点数:-12.3
# 测试四
请输入一个数字(输入 q 退出):12.343
你输入的是浮点数:12.343
小提示:在未来你独自开发程序时,如果你可以一次性从循环代码开始写,那么祝贺你达到一定水平了。但是如果,你刚刚入门。那我现在带你实现的步骤就很适合你学习:先实现单次运行的程序,才考虑添加循环。学到此,你应该要有体会:从 if 开始,现在这些都类似框架,框架内部的代码都使用 if 之前所学的知识,故而前面的基础知识很重要。建立的基础逻辑也很重要。
添加循环很简单,此时只需要考虑以下点:
循环终止条件是:在用户输入 q 时退出,循环条件的改变使用变量。
添加循环外壳
其实,学到这你应该要慢慢有所感受。从 if 开始,前面都是基本语法。if 之后都是框架,只要前面基础语法学的好。框架语法都能理解,框架都不难。重点是如何巧妙利用 if 之前的基础语法。好好感受和去悟!
condition = False
while not condition:
user_input = input("请输入一个数字(输入 q 退出):")
if user_input.isdigit() or (user_input.startswith('-') and user_input[1:].isdigit()):
number = int(user_input)
print(f"你输入的是整数:{number}")
elif user_input.count('.') == 1:
if user_input.replace('.', '').isdigit() or (user_input.startswith('-') and user_input[1:].replace('.', '').isdigit()):
number = float(user_input)
print(f"你输入的是浮点数:{number}")
else:
print("输入不合法,请重新输入!")
else:
print("输入不合法,请重新输入!")
实现判断用户输入是否为 q 并退出
condition = False
while not condition:
user_input = input("请输入一个数字(输入 q 退出):")
if user_input.lower() == 'q':
condition = True
elif user_input.isdigit() or (user_input.startswith('-') and user_input[1:].isdigit()):
number = int(user_input)
print(f"你输入的是整数:{number}")
elif user_input.count('.') == 1:
if user_input.replace('.', '').isdigit() or (user_input.startswith('-') and user_input[1:].replace('.', '').isdigit()):
number = float(user_input)
print(f"你输入的是浮点数:{number}")
else:
print("输入不合法,请重新输入!")
else:
print("输入不合法,请重新输入!")
下面代码是我之前上课时每每带学生敲打实现的思路,不是非常完整。但我还是想贴在本文后面,如同围棋看别人棋谱。代码我想也是如此,下面的代码不是最优实现。但其中思路是值得查阅思考的:
代码一:
number = input("Enter a number: ")
num_to_lst = number.split('.')
if len(num_to_lst) == 1:
if num_to_lst[0].isdigit():
print(f"number is {int(number)} and type is {int(number)}")
elif len(num_to_lst) == 2:
if num_to_lst[0].isdigit() and num_to_lst[1].isdigit():
print(f"number is {float(number)} and type is {float(number)}")
else:
print("input is not a number")
代码二:
while True:
user_input = input("Enter a number: ")
count_point = user_input.count(".")
if user_input.isdigit():
integer = int(user_input)
break
elif count_point == 1:
index = user_input.index(".")
pre_point = user_input[:index]
post_point = user_input[index:]
if pre_point.isdigit() and post_point.isdigit():
float_point = float(user_input)
break
else:
print("Invalid input")
.update()
:批量更新字典原本对于字典添加数据,只能一次操作一个键值对添加:
student = {'name': '李雷', 'age': 19}
student["gender"] = "male"
student["class"] = "class1"
print(student)
只能像上面的代码一样,一次添加一组键值对,不能批量。(这里回应“杠精”:先不考虑循环添加情况)。
所以 update()
方法应运而生, 这个方法可以把另一个字典的内容合并进当前字典,已有的键会被覆盖,新键会被添加。
你可以使用另一个字典来更新一个字典,这将添加新的键值对到原始字典中,并覆盖任何现有的键的值。
示例一:
dict1 = {'name': '李雷', 'age': 19}
dict2 = {'age': 20, 'class': '1班', 'gender': 'male'}
dict1.update(dict2)
print(dict1)
# ---output---
{'name': '李雷', 'age': 20, 'class': '1班', 'gender': 'male'}
示例二: 在这个例子中,dict1 使用 dict2 的内容进行了更新。键 'b'
的值从 2
更新为 3
,键 'c'
被添加到了字典中。
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
dict1.update(dict2)
print(dict1) # 输出:{'a': 1, 'b': 3, 'c': 4}
你也可以使用关键字参数来更新字典,代码示例如下。
dict1 = {'name': '李雷', 'age': 19}
dict1.update(name="AI悦创", age=28)
print(dict1)
# ---output---
{'name': 'AI悦创', 'age': 28}
这里,使用关键字参数 name 和 age 直接更新了 dict1。
可以传递一个可迭代对象(比如元组列表),其中每个元素都是一个键值对。
示例一: 列表嵌套元组
student = {'name': '李雷', 'age': 19}
data_lst = [('age', 20), ('class', '1班'), ('gender', 'male')]
student.update(data_lst)
print(student)
# ---output---
{'name': '李雷', 'age': 20, 'class': '1班', 'gender': 'male'}
示例二: 元组嵌套元组
student = {'name': '李雷', 'age': 19}
data_lst = (('age', 20), ('class', '1班'), ('gender', 'male'))
student.update(data_lst)
print(student)
# ---output---
{'name': '李雷', 'age': 20, 'class': '1班', 'gender': 'male'}
示例三: 元组嵌套列表
student = {'name': '李雷', 'age': 19}
data_lst = (['age', 20], ['class', '1班'], ['gender', 'male'])
student.update(data_lst)
print(student)
# ---output---
{'name': '李雷', 'age': 20, 'class': '1班', 'gender': 'male'}
其实怎么嵌套都可以,你可以自行思考和尝试。
update()
还支持通过迭代器提供的键值对进行更新。如果迭代器产生的是两项的元组,则第一项将被视为键,第二项视为值。
dict1 = {'a': 1}
pairs = zip(['b', 'c'], [2, 3])
dict1.update(pairs)
print(dict1)
# ---output---
{'a': 1, 'b': 2, 'c': 3}
在这个例子中,zip()
函数生成了一个元组列表,这些元组被用来更新 dict1
。
update()
方法时,如果键已存在,则其值将被新的值覆盖。update()
方法不返回任何值(即返回 None
),是直接修改字典本身。update()
方法时,如果键的值是一个可变数据类型(如列表或字典),需要注意,如果通过 update()
方法修改了这个键的值,原来对应的值也会被更改,除非进行深拷贝。(这个会在后面讲解)这种方法特别有用,在你需要合并两个字典或者在已有字典中添加新的信息时。例如,当你处理来自不同来源的数据并希望将它们合并为一个统一的数据结构时,update()
方法非常方便。
在软件开发中,我们经常会设置一份默认配置(default_config),比如默认主题、字体、语言等。如果用户手动设置了配置(user_config),我们就使用用户的优先,这时候 update()
就非常合适。
# 默认配置
default_config = {
'theme': 'light',
'font_size': 12,
'language': 'zh-cn',
'auto_save': True,
}
# 用户配置(可能来自用户设置、配置文件等)
user_config = {
'theme': 'dark',
'font_size': 14
}
# 合并配置:用户配置覆盖默认配置
default_config.update(user_config)
print(default_config)
输出:
{'theme': 'dark', 'font_size': 14, 'language': 'zh-cn', 'auto_save': True}
📌 说明:
theme
和 font_size
被用户更新了;language
和 auto_save
使用默认值保留;小结: .update()
是实现“默认 + 自定义”组合的理想工具。
假设你在做数据分析,有多个部门提交了各自的销售数据,我们希望将这些数据整合到一个总表中:
# 各部门上报的数据
sales_dept1 = {'一月': 12000, '二月': 15000}
sales_dept2 = {'二月': 10000, '三月': 18000}
sales_dept3 = {'一月': 5000, '三月': 7000}
# 创建一个汇总字典
total_sales = {}
# 把所有数据累加进来
for dept_data in [sales_dept1, sales_dept2, sales_dept3]:
for month, amount in dept_data.items():
if month in total_sales:
total_sales[month] += amount
else:
total_sales[month] = amount
print(total_sales)
输出:
{'一月': 17000, '二月': 25000, '三月': 25000}
📌 说明:
假设你正在做一个用户注册系统,用户第一次注册时填写了部分信息,以后登录后可以继续补全或修改这些信息:
user_info = {'name': '小明', 'age': 18}
# 用户后续填写了更多信息
new_data = {'gender': '男', 'city': '北京'}
# 用 update 方法更新用户信息
user_info.update(new_data)
print(user_info)
输出:
{'name': '小明', 'age': 18, 'gender': '男', 'city': '北京'}
小结: 用 .update()
把用户后续填写的信息合并到原始资料中,是不是很方便?
小红和小刚分别在网上购物,各自有自己的购物车,现在他们决定合并下单:
cart_xiaohong = {'苹果': 3, '香蕉': 2}
cart_xiaogang = {'香蕉': 1, '橙子': 4}
# 合并购物车
cart_xiaohong.update(cart_xiaogang)
print(cart_xiaohong)
输出:
{'苹果': 3, '香蕉': 1, '橙子': 4}
⚠️ 注意:同样的商品(例如“香蕉”),后加入的会覆盖前面的数量。
📝 小结: 如果你希望数量相加,而不是覆盖,可以使用别的处理方式,比如循环 + +=
。
假设你从 Excel 或数据库中一次性加载一条学生记录,想把它合并进已有的数据结构中:
student = {'name': '李雷', 'age': 18}
# 从数据库查询的新字段
db_data = {'grade': 95, 'class': '1班'}
student.update(db_data)
print(student)
输出:
{'name': '李雷', 'age': 18, 'grade': 95, 'class': '1班'}
📝 小结: 在数据处理场景中,update()
可以帮你“拼接”完整信息。
上面是我尽可能想到的具体例子和简易代码示例,便于你对这部分知识点的理解。
在 Python 中,嵌套的 for
循环是指在一个 for
循环内部再包含一个或多个 for
循环。这种结构在处理多维数据(例如二维、三维的列表或数组)以及需要多层次迭代的场景中非常常见,可以大幅提高编程的灵活性与效率。
嵌套的 for
循环的基本结构如下:
一个嵌套的 for
循环通常的书写形式如下:
for 变量1 in 可迭代对象1:
for 变量2 in 可迭代对象2:
# 执行代码块
每个 for
循环工作原理如下:
可迭代对象1
中取出一个元素赋值给变量1
。可迭代对象2
中逐一取出元素赋值给变量2
。查看下面的代码示例,助于理解:
for i in range(2):
print(f"外层循环执行了第 {i + 1} 次")
for j in range(3):
print(f"\t内层循环执行了第 {j + 1} 次")
运行结果如下:
外层循环执行了第 1 次
内层循环执行了第 1 次
内层循环执行了第 2 次
内层循环执行了第 3 次
外层循环执行了第 2 次
内层循环执行了第 1 次
内层循环执行了第 2 次
内层循环执行了第 3 次
看了上面的输出,你会对 for 循环嵌套有更好的理解。我接下来还有写一个生活中的例子来辅助理解嵌套循环,学代码不要脱离现实!一切都来源于生活~
让我们用一个贴近生活的场景来解释嵌套 for 循环的思想。想象你是餐厅的经理,负责统计一晚上的销售收入。
场景描述:
在一家繁忙的餐厅中,每张桌子都有几位顾客,每位顾客可能会点多道菜。为了统计当晚的总收入,你会采取以下步骤:
这个过程可以形象地理解为:
这种分步走的做法正是嵌套循环的思想:先处理大范围的分组(桌子),再在每个分组内部逐个处理细节(顾客订单)。这种方法不仅让你更有条理地完成任务,还能防止遗漏细节。
通过这种方式,你就能准确地统计出餐厅一整晚的销售额,同时也能发现某一桌的消费是否特别高、哪种菜品更受欢迎等信息。这正是嵌套 for 循环在现实生活中的一个直观应用。
假设我们有一个二维列表(即列表的列表),我们可以使用嵌套的 for
循环来遍历每一个元素。
假设我们有一个二维列表(列表中存放列表),现在需要依次遍历并访问里面的每个元素。示例代码如下:
假设我们有一个二维列表(即列表的列表),比如一个 3x3 的矩阵。我们希望依次访问矩阵中的每个元素。下面的示例代码展示了如何使用嵌套循环来完成这个任务:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
for row in matrix: # 外层循环:遍历每一行
for item in row: # 内层循环:遍历当前行中的每个元素
print(item)
这段代码首先遍历 matrix
中的每一行(外层循环),然后遍历该行中的每个元素(内层循环),并打印出来。
运行结果将依次打印 1 2 3 4 5 6 7 8 9
。这里的 row
是外层循环的元素(即矩阵中的一行),item
则代表当前行中的某个具体元素。
逐步解析:
for row in matrix:
这一行代码会依次取出 matrix
中的每一行(每一行本身也是一个列表),并将它赋值给变量 row
。for item in row:
每当外层循环取得一行数据后,内层循环会遍历该行中的所有元素,依次将元素赋值给 item
,并执行 print(item)
。小实验:尝试在代码中添加注释或打印调试信息,观察内外层循环的执行顺序,加深理解。
小试牛刀: 用 for 循环嵌套求 matrix 数据总和。
用 for 循环嵌套对二维列表求和,二维列表数据如下:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
第一步:先思考如何实现,我们要计算出列表 matrix 中的总和,前提是什么?前提是先得到每个数字!
第二步:得到列表中的每个数字,前面已经讲解过:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
for row in matrix: # 外层循环:遍历每一行
for item in row: # 内层循环:遍历当前行中的每个元素
print(item)
第三步:得到每一个数字后,我们需要实现累加。现实生活中,我们加一个数时会怎么计算?
[1, 2, 3]
;3 + 3 = 6
。total
吧。total
吗?——肯定不行,语法也不允许。肯定是需要给 total 赋一个值,因为我们是要求和,赋什么值不会影响最终的计算结果呢?——零(0),零加上任何数都等于本身!最终代码如下:
# 我们先初始化一个变量 total 用于累加每个元素的值。
total = 0
第四步:直接进行累加即可
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
total = 0
for row in matrix:
for item in row:
total += item # 外层循环遍历每一行,内层循环遍历每一行中的每个数字,并不断将数字加到 total 上。
print(total)
这段代码会将所有数字相加并打印出结果 45
,如果可以:试着在内层循环中添加 print(item)
,观察程序是如何逐步累加的。
上面成功实现求和,但 for 循环嵌套并不是必须的,我们如何实现只用一个 for 循环实现呢?
并非所有情形都必须使用嵌套循环,我们学会跳出思维惯性,开阔自己的思维。有时解决问题的方法不止一种,如同上面的求和任务。若仅仅是想把所有数字相加,还可以用下面的写法,一个 for
循环就足够了。
这里我们可以利用 Python 内置的 sum()
函数对每一行列表求和,然后再累加。例如:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
total = 0
for row in matrix:
total += sum(row)
print(total)
这种方式代码更简洁,但理解嵌套循环的工作原理对于你日后面对更复杂问题时非常重要。如果说推荐,我肯定推荐现在的第二种方法。这里用到 for 嵌套,纯粹是为了教你用而用。
在更复杂的情形下,我们可能需要按“列”来计算和。
现在让我们挑战一下:是我们的“老熟人”二维数组 matrix
,要求计算每一列的元素之和,并将结果存储在一个一维数组中。该任务不仅锻炼你对嵌套循环的理解,同时也会让你更熟悉如何操作数组索引。
对于下面的矩阵:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
对于上述 matrix
,你的程序应该输出:
每列的和: [12, 15, 18]
说明:
第一列(第 0 列)的和:1 + 4 + 7 = 12
第二列(第 1 列)的和:2 + 5 + 8 = 15
第三列(第 2 列)的和:3 + 6 + 9 = 18
要求:你必须使用 for
循环来遍历二维数组并计算每列的和。
提示:你可以通过初始化一个长度为 n
的数组来存储每列的和,并在遍历每行时将每列的值加到相应的数组位置上。
好好思考一下,看看上面如何实现。接下来,我会一步步带你实现。
想象一下,你有一个由多行数字组成的表格,每一列都排列着一组数字。假如你要计算每一列的总和,首先你会在心中设定一个记录器,每一列都有一个“计数器”。
当你逐行查看这张表格时,每读到一行,就会把这一行中每一列的数字分别加到对应的计数器上。
比如,第一行的三个数字分别加到第一、第二、第三个计数器上;接着第二行同样处理……直到所有行都看完。
最终,每个计数器中的数值就是对应列的总和。
接下来,我举个具体的例子,来让你更好的理解。文字表达终归有局限性,但是我会尽可能表达完全且清楚。
假设你面前有一个表格,表格中的数字排列如下:
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 4 | 5 | 6 |
+---+---+---+
| 7 | 8 | 9 |
+---+---+---+
这个表格其实就是一个二维数组,每一行代表一组数据,每一列代表一类数据。现在,你的任务是计算每一列数字的总和。
具体步骤演示:
准备工作
为了计算每列的和,我们需要给每一列设立一个“计数器”。
假设初始时,每个计数器的值都为 0。
例如,第一列、第二列、第三列的初始值分别为 0、0、0。
第一列求和
第一行:第一列的数字是 1。把 1 加到第一列的计数器上,原来的 0 变为 1。
第二行:第一列的数字是 4。将当前的计数器值 1 加上 4,得到 1 + 4 = 5。
第三行:第一列的数字是 7。再将 5 加上 7,得到 5 + 7 = 12。
结果:第一列的和就是 12。
第二列求和
第一行:第二列的数字是 2。初始值 0 加上 2,变为 2。
第二行:第二列的数字是 5。当前值 2 加上 5,得到 2 + 5 = 7。
第三行:第二列的数字是 8。再加上 8,7 + 8 = 15。
结果:第二列的和为 15。
第三列求和
第一行:第三列的数字是 3。初始值 0 加上 3,得到 3。
第二行:第三列的数字是 6。当前值 3 加上 6,得到 3 + 6 = 9。
第三行:第三列的数字是 9。再加上 9,9 + 9 = 18。
结果:第三列的和为 18。
总结一下,这个二维数组每一列的和分别为:
这种按列求和的方式就好比你在给每一列都开了一个小账户,依次将每行对应列的数字存入账户,最后得到每个账户的总金额。
初始化:
len(matrix[0])
获取第一行的元素个数)。column_sums = [0] * len(matrix[0])
表示我们为每一列都设立了一个“计数器”,初始值均为 0。
嵌套循环累加:
for row in matrix: # 遍历每一行
for i in range(len(row)): # 遍历每一行的每个元素(按列)
column_sums[i] += row[i] # 将当前数字加到对应的列累计和中
输出结果:
遍历完成后,column_sums
中存储的就是每一列的累计和,然后输出结果:
print("每列的和:", column_sums)
完整代码如下:
# 定义二维数组(矩阵)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# 初始化一个列表,长度为列数,每个元素初始为 0
column_sums = [0] * len(matrix[0])
# 嵌套循环:外层遍历每一行,内层遍历每一行的每个元素
for row in matrix:
for i in range(len(row)):
column_sums[i] += row[i]
# 输出每列的和
print("每列的和:", column_sums)
运行结果:
每列的和: [12, 15, 18]
解释:
初始化部分:我们创建了一个列表 column_sums
,其长度与矩阵的列数一致。就好比给每一列开了一个“账户”,初始余额为 0。
嵌套循环部分:外层循环每次取出一行 row
,内层循环遍历这行中每个数字,通过 i
代表列的索引,将当前行中第 i
个数字累加到 column_sums[i]
中。这正对应了现实生活中逐行将每一列数字累加到各自账户的过程。
输出部分:最后,我们输出 column_sums
,显示每列累计的结果。
接下来,我提供多种实现代码,便于你学习和参考其中思路。前面要理解了之后,再看下面的代码。下面的代码和前面的很类似,我就不重复赘述了。
方法一:按列遍历累加(基于索引)
# 定义矩阵
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# 初始化一个列表来存储每列的和,列表长度由第一行的元素个数决定
column_sums = [0] * len(matrix[0])
# 遍历每一列(外层循环)
for i in range(len(matrix[0])):
# 遍历每一行(内层循环),逐行取出对应列的数字
for j in range(len(matrix)):
column_sums[i] += matrix[j][i]
# 输出每列的和
print("每列的和:", column_sums)
方法一:讲解
初始化:先用 len(matrix[0])
获取第一行的列数,并创建一个与之等长的列表 column_sums
,初始每个元素为 0,代表每列的累加器。
双重循环:
外层循环以列为单位,索引 i
表示当前正在处理的列。
内层循环遍历所有行,通过 matrix[j][i]
取出当前行第 i
列的数字,并累加到 column_sums[i]
中。
输出:最后打印结果 [12, 15, 18]
,分别代表每列的和。
方法二:逐行累加(适用于固定列数的情况)
如果你更喜欢直接处理每一行,可以这样操作:假设你知道矩阵每行的长度固定为 3,可以用单独的变量来累计每一列的和。
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# 初始化三个变量分别记录三列的和
col1 = 0
col2 = 0
col3 = 0
# 遍历每一行,将每列的值累加到对应变量上
for row in matrix:
print(row[0], row[1], row[2])
col1 += row[0]
col2 += row[1]
col3 += row[2]
column_sums = [col1, col2, col3]
print(column_sums) # 输出:12 15 18
九九乘法表是学习嵌套循环的经典练习题。通过编写乘法表程序,你不仅能巩固嵌套循环的概念,还能体验如何利用循环控制输出格式。
下面是我们的程序实现的目标结果:
嵌套循环也常用于生成乘法表等需要行和列计算的场景。
嵌套循环是生成乘法表等涉及行和列的计算中常用的技巧。我们可以通过观察生活中如何“排列表格”的直观过程,来理解代码中的循环结构。
for i in range(1, 10)
表示从 1 到 9 的行。for j in range(1, 10)
表示每一行内也从 1 到 9 依次计算乘积。end='\t'
保持在同一行打印多个结果;每完成一行内层循环后,用 print()
输出换行符。for i in range(1, 10): # 行,从1到9
for j in range(1, 10): # 列,同样从1到9
print(f"{i} * {j} = {i * j}", end='\t')
print() # 每完成一行乘法后换行
在这个例子中,外层循环和内层循环都使用 range(1, 10)
,分别代表乘法表的行和列。每次外层循环进入新的一行,内层循环依次计算并打印该行所有乘积。并且在每内层循环结束后,使用 print()
函数来创建一个新行。
运行前面的代码,我们一起来看看有没有什么问题:
我们可以看见很明显的两个问题:
3 * 1 = 3
,正确结构为:1 * 3 = 3
;认真观察一下,看看怎么解决。
一图胜千言,看下图。“上”、“下”两个部分,哪个部分是我们需要的部分?——“下”是我们需要的部分。
那么“下”部分有什么问题?——位置反了。一起看看特点:
1 * 1 = 1
2 * 1 = 2
3 * 1 = 3
4 * 1 = 4
5 * 1 = 5
6 * 1 = 6
7 * 1 = 7
8 * 1 = 8
9 * 1 = 9
我们从上面贴出的部分输出,可知:第一列的数字是 1、2、3、4、5、6、7、8、9
,第二列的数字都是 1
。这显然和实际的九九乘法表不一样,实际的 1
在前面,而 1、2、3、4、5、6、7、8、9
在后面。
那么按照嵌套循环的逻辑可知:都是 1
的是外层循环产生的,而 1、2、3、4、5、6、7、8、9
是由内层循环产生的。
接着,我们结合我们现有的代码:
print(f"{i} * {j} = {i * j}", end='\t')
可知,直接交换 i 和 j 的位置即可。
print(f"{j} * {i} = {i * j}", end='\t')
现在修改后的完整代码如下:
for i in range(1, 10): # 行,从1到9
for j in range(1, 10): # 列,同样从1到9
print(f"{j} * {i} = {i * j}", end='\t')
print() # 每完成一行乘法后换行
到此,我们一起解决了乘法表中数字位置的切换!
第二个问题比较简单,去掉多余的输出即可。我们看看下面的图:
观察上图输出可知,我们需要的下半部分的三角形输出。并且观察上下部分特点可知:下班部分的输出结构需要满足 j <= i
。
为什么是:j <= i
,因为原本代码是:f"{j} * {i} = {i * j}"
,前面的数字代表 j,后面的数字代表 i。而且从规律可知:j <= i
。规律用文字不好描述,还是看下图:
所以,我们只要添加一个 if 判断即可筛选我们想要的下半部分三角形:
if j <= i:
print(f"{j} * {i} = {i * j}", end='\t')
修改后的完整代码如下:
for i in range(1, 10): # 行,从1到9
for j in range(1, 10): # 列,同样从1到9
if j <= i:
print(f"{j} * {i} = {i * j}", end='\t')
print() # 每完成一行乘法后换行
我的一位来自香港理工大学的学员,在上课时提出的奇思妙想:想要保留上半部分的代码且格式。
当时可是费了不少劲,才写出来的。代码贴在下面,有兴趣的查看即可。
for i in range(1, 10):
# 在每行的开头添加足够的制表符,以形成缩进
print("\t\t\t" * (i - 1), end='')
for j in range(i, 10):
print(f"{i} * {j} = {i * j}\t", end='')
print() # 每完成一行乘法后换行
运行后输出如下:
for i in range(1, 10): # 行数:1 到 9
for j in range(1, i + 1): # 列数:1 到 i
print(f"{j} * {i} = {i*j}", end="\t")
print() # 每完成一行后换行
接下来,着重分析 i + 1
的由来。
在传统的九九乘法表里,第 1 行只有 1 x 1
一个乘积,第 2 行通常是 1 x 2
、2 x 2
,第 3 行是 1 x 3
、2 x 3
、3 x 3
,依此类推。
也就是说,第 i 行只需要展示从 1
一直到 i
这几个数和 i
相乘的结果。这样能让表格看起来更简洁,并且避免重复组合(例如 2 x 3
和 3 x 2
)。
如果我们想在代码中表达“第 i 行的列数只到 i
”,就可以在内层循环里写 range(1, i + 1)
。
提示:
你可以尝试把 range(1, i + 1)
改成 range(1, i)
,运行看看会出现什么情况,直观感受为什么需要加 +1
。
在学习编程时,多加这一步“实验对比”是非常好的习惯,它能帮助你快速抓住关键知识点,并把它记得更牢。
嵌套循环是编程中常见的一个概念,通过适当使用可以解决很多复杂的问题,但也需要注意其对性能的影响。
将程序任务涉及到的事物抽象为一个个的对象,以这些对象为中心来写程序。是不是很抽象,很难理解?不用慌,我们从头开始讲!
很多朋友最开始学编程的时候,是从 C++ 或者 JAVA 语言入手的。甚至现在国内外 Python 课程开设,不用要求学生提前掌握其它编程语言。读者们好不容易磕磕绊绊地搞懂了最基本的数据类型、赋值判断和循环,却又迎面撞上了 OOP (object oriented programming) 的大墙,一头扎进公有私有保护、多重继承、多态派生、纯函数、抽象类、友元函数等一堆专有名词的汪洋大海中找不到彼岸,于是就放弃了进阶之路。
Description: ICS4U.party will be an essential, local message board where users can post messages and reply to threads anonymously without needing an account or password. The focus is on simplicity, all data is saved in a simple text file to simulate storage. It is inspired by online forums like Reddit or s**
ak.party, but it’s trimmed down to keep the core features, and is easy and manageable.
在学习 Python 时,我们经常会听到 “可迭代”(iterable) 这个词。对于初学者来说,这个概念可能有些抽象。那么,什么是“可迭代”?它和“不可迭代”有什么区别?今天我们就用通俗易懂的方式来讲解这个概念,让你一看就懂!这部分的知识,也是为了便于后续的学习而增加的。
用一个类似的知识来辅助下面知识点的理解:分子属于可迭代对象,原子属于不可迭代对象。为什么这么说呢:分子可以拆分成原子,但原子属于不可再拆分。可以再被拆分的称为可迭代,不可再被拆分的称为不可迭代。
在 Python 编程中,“可迭代”(iterable)是指 可以一个一个取出元素的东西。
想象你有一本书,书里有很多页。你可以一页一页地翻,这本书就是可迭代的,因为你可以“遍历”它的每一页。
再比如:想象你有一个音乐播放列表,里面有很多歌曲,你可以一首一首地播放。这个播放列表就是可迭代的,因为你可以逐个获取里面的每一首歌,并且按顺序播放或随机播放。
在 Python 中,可迭代的对象包括:
对象类型 | 示例代码 | 说明 |
---|---|---|
列表(list) | ["苹果", "香蕉", "橙子"] |
有序、可变,适合存储多个元素 |
元组(tuple) | ("猫", "狗", "兔子") |
有序、不可变,适合存储多个固定值 |
字符串(str) | "Hello Bornforthis.cn" |
由字符组成,可以逐个遍历 |
字典(dict) | {"name": "Bornforthis", "age": 20} |
由键值对组成,可迭代键、值或项 |
集合(set) | {"红色", "蓝色", "绿色"} |
无序、唯一元素集合 |
文件对象(file) | open("test.txt") |
可以逐行读取内容(也就是可以一行一行读取) |
如果一个东西可以一个一个取出元素,并且能被 for
循环使用,那它就是 可迭代的(iterable)。这在 Python 编程中很重要,因为很多操作都依赖于可迭代对象,比如 for
循环、列表推导式、map()
、filter()
等等。
你可以把“可迭代”理解成 可以按顺序拿出来用的东西,这样就容易理解了!
你有可能会想:为什么代码要放在这个部分,而不是放在例子下面?——因为,这部分代码涉及的 for 循环语法还未讲解,但觉得还是放一下,方便读者阅读理解。这部分的代码阅读准则如下:
在 Python 里,播放列表就像是一个列表(list):
playlist = ["歌曲1:你眼中的星辰", "歌曲2:灵眸", "歌曲3:花式篮球(少年版)"]
for song in playlist:
print(f"正在播放{song}")
📌 输出:
正在播放歌曲1:你眼中的星辰
正在播放歌曲2:灵眸
正在播放歌曲3:花式篮球(少年版)
在 Python 中“不可迭代”的(non-iterable)的是指不可被拆分,也就是不可以一个一个取出元素的东西。
不可迭代对象(Non-iterable)是指不可被拆分,不能一个一个取出元素,不能用 for
循环遍历的对象。
不可迭代(non-iterable) 的对象指的是不能一个一个取出元素。想象你有一块石头,这块石头是不可迭代的。你不能像翻书一样,从石头里一块一块地取出“页”来读,因为它是一个整体,没有分成多个可以依次取出的部分。
再比如:想象你有一杯水,这杯水是不可迭代的。你不能像数硬币一样,一枚一枚地取出水滴,因为水是一个整体,无法被逐个“遍历”出来。
在 Python 中,以下几类对象通常是不可迭代的:
对象类型 | 示例 | 是否可迭代? | 原因 |
---|---|---|---|
整数(int) | 123 |
❌ 不是 | 不是一组元素,无法逐个取出 |
浮点数(float) | 3.14 |
❌ 不是 | 不能遍历其中的内容 |
布尔值(bool) | True 、False |
❌ 不是 | 只有两个值,不能逐个取出 |
None值(NoneType) | None |
❌ 不是 | 代表“无”,没有元素可遍历 |
📌 记住:不可迭代的对象通常是单个数据,而不是一组元素! 💡
如果一个东西不能一个一个取出元素,也不能被 for
循环遍历,那它就是 不可迭代的(non-iterable)。虽然不可迭代对象不能用于 for
循环,但它们在编程中依然非常重要,主要用于存储和计算单个值,以及控制程序逻辑。
这里和前面可迭代的要求一样,只求理解不可迭代的含义即可。
# 尝试对不可迭代的对象使用 for 循环
non_iterable = 1
for item in non_iterable:
print(item) # ❌ TypeError: 'int' object is not iterable
为什么会报错?——1
是整数,它本身不是一组元素,你可以把这个 1 再拆开吗?显然是不能继续拆开的。(如果此时有杠精的话,请告诉我你可以把 1 拆解成什么?)不能被 for
逐个遍历,所以是不可迭代的。
前面讲了什么是可迭代、不可迭代,并且给你列举了在 Python 当中哪些是否可以迭代的表格。有些读者阅读之后,怕自己判断不正确或各种原因,我提供一个代码来辅助判断是否数据可迭代。
方法一:使用 collections.abc.Iterable
Python 提供了 collections.abc.Iterable
作为判断是否可迭代的基类,可以用 isinstance()
来检查:
from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable)) # ✅ True,列表是可迭代的
print(isinstance("hello", Iterable)) # ✅ True,字符串是可迭代的
print(isinstance(100, Iterable)) # ❌ False,整数不是可迭代的
print(isinstance(3.14, Iterable)) # ❌ False,浮点数不是可迭代的
优点:推荐的 Pythonic 方式,符合 Python 的面向对象原则。
方法二:使用 iter()
函数
Python 的 iter()
内置函数会尝试获取对象的迭代器。如果对象可迭代,则 iter()
不会报错,不可迭代则报错:
print(iter([1, 2, 3])) # ✅ 正常返回一个迭代器
print(iter(100)) # ❌ TypeError: 'int' object is not iterable
优点:直接测试对象是否实现了 __iter__()
或 __getitem__()
方法。
方法三:使用 type()
检查
如果你只想检查常见的可迭代类型(如 list
, tuple
, set
, dict
, str
, range
),等于说是提前把可迭代的类型放进元组,然后使用 in 来判断检测的类型是否存在目标可迭代元组中。
print(type([1, 2, 3]) in (list, tuple, set, dict, str, range)) # True
print(type(123) in (list, tuple, set, dict, str, range)) # False
print(type("hello") in (list, tuple, set, dict, str, range)) # True
print(type({1, 2, 3}) in (list, tuple, set, dict, str, range)) # True
print(type(range(10)) in (list, tuple, set, dict, str, range)) # True
缺点:这种方法只能判断有限的内置类型,不能检测自定义的可迭代对象。
方法四:检查 __iter__()
或 __getitem__()
方法
Python 的迭代协议要求对象实现 __iter__()
方法,或者对于旧式迭代器,实现 __getitem__()
方法:
def is_iterable(obj):
return hasattr(obj, '__iter__') or hasattr(obj, '__getitem__')
print(is_iterable([1, 2, 3])) # True
print(is_iterable(123)) # False
print(is_iterable("hello")) # True
优点:简单直接,适合了解对象的内部机制。
方法五:使用 dir()
检查
可以通过 dir()
方法查看对象的方法列表:
def is_iterable(obj):
return '__iter__' in dir(obj) or '__getitem__' in dir(obj)
print(is_iterable([1, 2, 3])) # True
print(is_iterable(123)) # False
print(is_iterable("hello")) # True
优点:类似 hasattr()
,但 dir()
返回的内容更丰富,适合调试时使用。
其实,是否可以 for 循环也是判断是否可迭代的方法。 这里就不给演示代码了,前面我也提供了。可以循环的不会报错,不可以循环的会报错。
至于如果你想要问我要一个准确答案:推荐使用哪种方法来实现呢?这里我可以明确的告诉你,没有什么推荐不推荐。上面每种方法的大致好坏我都写出来了。但是具体实际使用的时候怎么选呢?我给你下面几个原则:
对于上面的方法四、方法五,你不理解也没事。我为了本书的完整性,都提供一下。便于以后你达到 Python 某种水平时,本书依然适配,依然是你的选择足矣。
在写本章节中,我想了许多例子,但是刚刚要着笔写又觉得例子有漏洞,或许是私教学生带的比较多,所以考虑的比较全面。鉴于每个读者年龄段和理解不一样,我多举几个不同的不可迭代的例子。以便让你真正理解,这个部分的知识点。
这里有 10 个贴切的可迭代 VS. 不可迭代对象的类比,方便理解什么是迭代和不可迭代的:
灯泡 💡
鸡蛋 🥚
足球 ⚽
硬币 🪙
水滴 💧
砖块 🧱
篮球🏀
杯子 ☕
苹果🍏
网球拍🎾
可迭代的对象像是一堆网球拍,你可以一个一个拿出来。
单个网球拍是整体,不能像一堆网球拍那样“逐个遍历”它的元素。
数字🔢
1,2,3,4,5,6,7,8,9
,你可以一个一个取出竹筐中的立体数字。音乐专辑/音乐播放列表💽
购物清单🧾
总结:
这样对比理解是不是更清晰了呢?多思考,多理解,这很重要。
能 for
循环的,就是可迭代的;不能 for
循环的,就是不可迭代的! 💡
总之,虽然有重复论述之嫌。但还是要再提一次:可迭代的对象就像一个“可以一个个取出来用的集合”,而不可迭代的对象通常是单个独立的东西。
未来便于你随时查阅,我制作了如下表格:
对象类型 | 可迭代? | 原因 |
---|---|---|
list (列表) |
✅ 是 | 包含多个元素,可以逐个取出 |
tuple (元组) |
✅ 是 | 和列表类似 |
str (字符串) |
✅ 是 | 由字符组成,可以逐个取出 |
dict (字典) |
✅ 是 | 键或值可以逐个取出 |
set (集合) |
✅ 是 | 包含多个元素 |
file (文件对象) |
✅ 是 | 可以逐行读取 |
int (整数) |
❌ 不是 | 不是一组元素,无法逐个取出 |
float (浮点数) |
❌ 不是 | 不能遍历其中的内容 |
bool (布尔值) |
❌ 不是 | 只有 True /False ,无元素可取 |
语法:
tuple.count(item)
示例:
t = (1, 2, 2, 3, 2)
print(t.count(2)) # 输出: 3
语法:
tuple.index(item[, start[, end]])
示例:
t = ('a', 'b', 'c', 'b', 'd')
print(t.index('b')) # 输出: 1
print(t.index('b', 2)) # 输出: 3,从索引2开始查找
语法:
len(tuple)
示例:
t = (1, 2, 3, 4)
print(len(t)) # 输出: 4
语法:
max(tuple)
min(tuple)
示例:
t = (10, 20, 5, 15)
print(max(t)) # 输出: 20
print(min(t)) # 输出: 5
语法:
sum(tuple[, start])
示例:
t = (1, 2, 3, 4)
print(sum(t)) # 输出: 10
print(sum(t, 10)) # 输出: 20,从10开始累加
语法:
sorted(tuple[, key][, reverse])
示例:
t = (3, 1, 2)
sorted_t = sorted(t)
print(sorted_t) # 输出: [1, 2, 3]
print(t) # 输出: (3, 1, 2),原元组不变
+
)示例:
t1 = (1, 2)
t2 = (3, 4)
t3 = t1 + t2
print(t3) # 输出: (1, 2, 3, 4)
*
)示例:
t = (1, 2)
print(t * 3) # 输出: (1, 2, 1, 2, 1, 2)
in
和 not in
)示例:
t = ('apple', 'banana')
print('banana' in t) # True
print('orange' not in t) # True
示例:
t = (0, 1, 2, 3, 4)
print(t[1:4]) # (1, 2, 3)
print(t[::-1]) # (4, 3, 2, 1, 0)
示例:
t = (10, 20, 30)
a, b, c = t
print(a, b, c) # 10 20 30
示例:
t = (1, (2, 3), 4)
print(t[1][0]) # 2
示例:
t1 = (0, False, '')
t2 = (0, True, '')
print(any(t1)) # False
print(any(t2)) # True
print(all((1, 2, 3))) # True
print(all((1, 0, 3))) # False
示例:
lst = [1, 2, 3]
t = tuple(lst)
print(t) # (1, 2, 3)
示例:
t = (1, 2, 3)
lst = list(t)
lst.append(4)
t = tuple(lst)
print(t) # (1, 2, 3, 4)
序号 | 内置函数名称或操作 | 功能说明 | 使用代码示例 |
---|---|---|---|
1 | count() |
统计元素出现次数 | t.count(3) |
2 | index() |
返回首次出现元素索引 | t.index('a') |
3 | len() |
元组长度 | len(t) |
4 | max() |
最大元素 | max(t) |
5 | min() |
最小元素 | min(t) |
6 | sum() |
元素求和 | sum(t) |
7 | sorted() |
排序返回新列表 | sorted(t) |
8 | tuple() |
转换为元组 | tuple([1,2,3]) |
9 | 元组拼接 (+ ) |
拼接多个元组 | t1 + t2 |
10 | 元组重复 (* ) |
重复元组元素 | (1,2)*3 |
11 | 成员检测 (in/not in ) |
检测元素存在 | 'a' in t |
12 | 元组拆包 | 元素拆包赋值 | a,b=(1,2) |
13 | 嵌套元组 | 元组内套元组 | ((1,2),(3,4)) |
14 | 元组切片 | 提取元组子序列 | t[1:3] |
15 | 单元素元组表示法 | 创建单元素元组 | (1,) |
16 | 空元组 | 创建空元组 | t=() |
17 | any() |
元素至少有一个为真 | any(t) |
18 | all() |
所有元素为真 | all(t) |
19 | zip() (与列表类似) |
同时迭代多个元组 | for x,y in zip(t1,t2): |
20 | 元组推导式生成器表达式 | 快速生成元组(迭代对象) | tuple(x*2 for x in t) |
21 | 元组与列表相互转换 | 通过列表修改元组 | list(t) 和 tuple(lst) |
示例:
# 函数返回多值
def get_point():
return (10, 20)
x, y = get_point()
print(x, y) # 10, 20
y = x
所存在的问题,是真的备份吗?阅读下面的代码,思考下面两个问题:
注意: 下面的代码我是有意写这么长的字符串,有两个目的:
x = ['毒药', '感冒药', '解药']
y = x
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y: {id(y)}') # id() 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y: {id(y)}')
阅读到此,想必你有了你自己的思考。我们现在来看看,我对于上面两个问题的回答。
从代码 y = x
可知,开发者想要复制一个 x 列表的副本给变量 y 并且想要修改列表 y 时不影响原列表 x。换句话说,y 应该是 x 的一个独立备份。
为什么我们会有这种需求?让我们结合现实场景来思考。
假设你有一个包含 100万条数据的 Excel 文件,为下载这个数据文件获取这个文件的代价极高:
注意:这里我是特意这么夸张,请不要想其它的。我只是为了让你尽可能感觉到这个数据是多么昂贵的,并且获取这个数据所要付出的代价是巨大的,因为只有昂贵才会重视,便于我塑造情景。
这里再次强调:如果再下载一次还是需要 1000元、下载耗费3个小时、并且还是只能下载一次。此时你要操作这个拥有 100万数据且下载成本极高的 Excel 会怎么操作?
阅读到此时,请停顿一下。思考一下,在这种情况下,你会怎么做?
毫无疑问,肯定是在操作前做一个备份,以便在操作时:如果不小心手贱按错按键,或者破电脑突然卡住导致你操作的数据没有及时保存或关闭等情况,导致数据缺失或损坏。此时就需要使用备份文件来恢复,这样就避免重新获取数据而产生的高额代价。所以在上面的代码中也是这样的意图,列表 x 作为备份数据,以确保对 y 的修改不会影响到原始数据 x,以此保证原数据不被影响。
对于问题2请先观察下面的代码实际输出,看看输出结果中存在什么问题和特点?
注意:观察下面的代码时你要具备一个原则:有时候,不要只看局部。站的高点,站的远点,使我们有全局视角,这样往往更便于我们发现事物中所存在的关联关系或规律。
意思就是:你在观察下面的输出结果时,不要单纯的一行一行看,或者看完下一行忘记上一行。要把自己从中抽离出来,看整个输出。去及时的观察和对比,这样才能发现其中的规律或者存在的问题。
Original:
x: ['毒药', '感冒药', '解药']
y: ['毒药', '感冒药', '解药']
id_x: 4470743680 id_y:4470743680
After:
x: ['消炎药', '感冒药', '解药']
y: ['消炎药', '感冒药', '解药']
id_x: 4470743680 id_y:4470743680
我们会发现,当我们修改 y 列表中的第一个元素后,x 列表的内容也发生了变化,这显然不符合我们的预期。理论上,我们的目标是 仅修改 y,不影响 x,但结果表明 x 和 y 竟然是同一个列表!
为什么会产生这个问题情况呢?我们可以从 id()
函数的输出中找到答案。
从上面的输出结果可知:列表 x 和列表 y 的物理地址是相同的,意味着虽然做了备份(y = x
),但没想到列表 x 和 y 是指向同一个列表,故而导致修改列表 y 时,列表 x 的数据也被修改。与我们原本的意图(备份)事与愿违,并且产生了“巨大”代价!明显我们的代码不符合预期。
从上面的分析后,我们可知 y = x
只是进行了列表地址的赋值(类似引用),x、y 实际上指向的是同一个列表。可以理解成两个列表元素一样,Python 就偷懒不再创建一个新的相同列表,直接创建一个地址引用。
为了更直观地理解这个问题,我们可以借助一个现实生活中的类比:
Python 在 y = x
赋值时的行为类似于百度网盘的“创建链接”机制:并没有真正复制列表,而只是让 y
指向了 x
,所以 x
和 y
变成了 同一个对象的两个引用。
上面分析了一下,接下来我来给你归整归整。我们说列表 x 和列表 y 指向的是同一个变量,我们口说无凭,来看看下面的凭证:
证明一:Python id()
用来检查变量物理地址(也就是在计算机中,所在的位置)并结合上面的代码结果可知:x、y 指向的是同一个列表,因为 id 相同。(id_x: 4470743680
、id_y:4470743680
)
证明二:可以直接使用可视化:https://pythontutor.bornforthis.cn/visualize.html#mode=edit查看,毕竟一图胜千言。在可视化界面中(下图),你会发现 x
和 y
其实是 同一个列表对象的两个名称,本质上它们没有区别。因此,修改 y
也会影响 x
,这就是问题的根本原因。
有时候,不要只看局部。站的高点,站的远点,使我们有全局视角,这样往往更便于我们发现事物中所存在的关联关系或规律。——AI悦创 2024-01-01 23:17:10
既然 y = x
不能满足我们“创建副本”的需求,那么 如何正确地复制一个列表,而不影响原列表呢?
别担心,我们接下来就会深入探讨 Python 中的深拷贝与浅拷贝,以及 如何避免此类问题。
copy()
进行浅拷贝在前面的内容中,我们发现 直接赋值 y = x
并不会创建一个新的列表,而只是让 y
指向 x
,导致修改 y
的同时 x
也会被修改。这显然不符合我们的备份需求。
那么,如何正确创建一个新的副本,确保 y
和 x
互不影响呢?为了解决这个问题,我们可以使用列表自带的 copy()
方法,这会创建一个“浅拷贝”(shallow copy),从而获得一个独立的对象副本。
让我们看看使用 copy()
进行列表复制的效果:
x = ['毒药', '感冒药', '解药']
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y:{id(y)}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y:{id(y)}')
代码输出如下:
Original:
x: ['毒药', '感冒药', '解药']
y: ['毒药', '感冒药', '解药']
id_x: 4428223616 id_y:4428537536
After:
x: ['毒药', '感冒药', '解药']
y: ['消炎药', '感冒药', '解药']
id_x: 4428223616 id_y:4428537536
从输出结果来看,我们可以发现两个重要的现象:
id_x
和 id_y
变了:初始状态下,x
和 y
的内容完全相同,但物理地址(id_x
和 id_y
)已经 不再相同,这表示 copy()
方法为 y
创建了一个全新的列表对象。y
和 x
现在是两个独立的对象,互不影响。y
后,x
没有被影响:之前 y = x
时,修改 y[0]
为 '消炎药'
时会影响 x[0]
,但现在 y
已经是 x
的一个独立副本,因此修改 y
不会影响 x
,这正是我们想要的备份效果。copy()
方法在 Python 中执行的是 浅拷贝(Shallow Copy)。它的本质是 创建一个新的列表对象,并将原列表中的元素引用复制到新列表中。简单来说:
copy()
复制了列表本身,即创建了一个新的 y
。y
里的元素仍然指向 x
里的同样的元素。如果列表中包含可变的嵌套对象(如列表里还有列表、元组、字典等),这些 子对象依旧是原来对象的引用。也就是说,对同一层级的数据做了拷贝,但 更深层次 的数据结构还会被引用到原对象。.copy()
当作和“深拷贝”一样使用,但是前提是:列表的元素都是字符串、数字型、布尔型等不可变数据类型,并没有深层的嵌套结构,自然也就不会出现意料之外的相互影响。这可能有点绕,我们可以用下面的图示帮助理解:
原列表 x:
x ---> ['毒药', '感冒药', '解药']
↑ ↑ ↑
新列表 y: | | |
y ---> ['毒药', '感冒药', '解药']
可以看到,x
和 y
是两个不同的列表对象(id()
值不同),但它们里面的元素其实是 指向同一个数据的引用。
copy()
的适用场景✅ 适用于:
y
是 x
的副本,修改 y
不会影响 x
。x.copy()
(或 x[:]
)就足够解决“大部分”复制需求。❌ 不适用于:
copy()
仅复制了外层列表,如果列表内部还包含可变对象(如子列表),这些子列表仍然是共享的,即修改 y
内部的子列表仍然会影响 x
。copy()
的局限性:浅拷贝的陷阱在前面的讲解中,我们为列表 x
创建了一个副本,从而避免了修改 y
时影响 x
的情况。然而,这里又引出了一个新的问题:为什么在有些情况下,修改 y
仍旧会影响到 x
?请先阅读下面的代码,并思考它在做什么,最终产生了怎样的结果,这个结果是否出乎你的意料。
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
y[3][0] = '苹果'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
运行结果如下所示:
Original:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 5306734464 id_y: 5310717568
id_children x[3]: 4347684800
id_children y[3]: 4347684800
After 1:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 5306734464 id_y: 5310717568
id_children x[3]: 4347684800
id_children y[3]: 4347684800
After 2:
x: ['毒药', '感冒药', '解药', ['苹果', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['苹果', '瓜子', '八宝粥']]
id:
id_x: 5306734464 id_y: 5310717568
id_children x[3]: 4347684800
id_children y[3]: 4347684800
x
也被修改了?在 After 2
这一步,我们仅修改了 y
中子列表的内容后(y[3][0]
),理论上 x
不应该受到影响。但实际结果显示,x
的对应部分也跟着改变了,也就是 x[3][0]
也被修改成了 '苹果'
,这是因为 .copy()
方法只执行了浅拷贝(shallow copy),它并不会深入到子列表或更深层级的数据结构去创建新的副本,而是让 x
和 y
共享同一个子列表。
浅拷贝的工作机制:
.copy()
仅复制了 x
的 第一层结构,但不会递归地复制内部的嵌套列表。
也就是说,y[3]
仍然指向 x[3]
的 同一个对象,它们共享相同的内存地址。这可以通过 id(x[3]) == id(y[3])
来验证,两者的内存地址完全一致。
可以从以下两个方面来验证这一点:
x[3]
和 y[3]
的 id
完全一致,这说明它们实际上引用的是同一个对象(子列表)。你还可以为上面的代码添加一行代码来更直观的验证:print(id(x[3]) == id(y[3]))
。x
和 y
在最外层各自拥有不同的地址,但它们第三个索引位置(子列表)都指向同一个存储空间。还是使用:https://pythontutor.bornforthis.cn/visualize.html#mode=edit,图示如下:如果想要彻底独立的副本(包括子列表也要复制),就需要使用 深拷贝(deep copy)
所以,copy 实现的是浅拷贝,只拷贝列表的第一层,嵌套的列表则不会拷贝。
因此,如果你希望获得完全独立的副本(包括子列表和其他更深层的嵌套对象都相互独立),则需要使用深拷贝(deep copy)。
deepcopy()
:如何彻底复制列表?使用深拷贝需要导入库:
为了使用深拷贝功能,首先需要导入 copy
模块中的 deepcopy
函数:
from copy import deepcopy
下面用一个示例演示深拷贝的效果:
from copy import deepcopy
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = deepcopy(x)
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\n\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\n\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
y[3][0] = '苹果'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\n\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
示例输出如下:
Original:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4408501568
id_y: 4408501056
id_children x[3]: 4408175296
id_children y[3]: 4408501312
After 1:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4408501568
id_y: 4408501056
id_children x[3]: 4408175296
id_children y[3]: 4408501312
After 2:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['苹果', '瓜子', '八宝粥']]
id:
id_x: 4408501568
id_y: 4408501056
id_children x[3]: 4408175296
id_children y[3]: 4408501312
deepcopy()
解决了问题?deepcopy()
会递归地复制整个数据结构,而不仅仅是第一层。
x[3]
和 y[3]
现在是两个不同的列表,修改 y[3]
不会影响 x[3]
。
如何证明上面的结论呢?依然使用的是两种方法:
id
值已经不同,说明 x
和 y
中的子列表各自拥有独立的内存空间。你还可以基于上面的代码,通过添加 print(id(x[3]) != id(y[3]))
得到 False 可以验证,它们的 ID 已经不同。id 即代表物理地址,物理地址不同则不是同一个数据。使用的网站依然是:https://pythontutor.bornforthis.cn/visualize.html#mode=edit
虽然前面提到浅拷贝不会递归复制子列表(子列表没有完全 copy 出来),导致修改 y
时也会影响到 x
,但在某些特定情况下,浅拷贝的改动并不会影响原列表,也就是修改列表 y 是不会影响列表 x。
请先思考一下:
我们上面说了列表的深浅拷贝,但是浅拷贝的时候,虽然子列表没有完全 copy 出来,但是有一种情况下修改列表 y 是不会影响列表 x。是不会互相影响。
所以,接下来依然是思考一下🤔:~~也就是说,在浅拷贝的代码中,什么情况下修改子列表不会互相影响。~~在浅拷贝的代码示例中,在哪种情形下我们修改了 “子列表” 却不影响另外一个列表?
下面我也提供了浅拷贝代码,请认真激发你的有效思考:
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
y[3][0] = '苹果'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
既然,我们修改引用的子列表会互相影响。那我们大胆一点想一想,直接把整个列表修改了,是不是就不会互相影响了呢?下面这段代码提供了线索。请对比与前面代码的差异之处,并观察最终输出:
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
# y[3][0] = '苹果'
y[3] = 'www.bornforthis.cn'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
运行结果如下:
Original:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4412075712 id_y: 5778383360
id_children x[3]: 4349618112
id_children y[3]: 4349618112
After 1:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4412075712 id_y: 5778383360
id_children x[3]: 4349618112
id_children y[3]: 4349618112
After 2:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', 'www.bornforthis.cn']
id:
id_x: 4412075712 id_y: 5778383360
id_children x[3]: 4349618112
id_children y[3]: 4698471152
从上面的输出可以得知,当直接把 y[3]
替换成一个新的字符串('www.bornforthis.cn'
)时,x
中并不会受到影响,因为此时我们并没有去修改原本共享的子列表,而是让 y
指向了一个全新的对象,也意味着 y[3]
指向了一个新的内存地址。
对于 “浅拷贝” 而言,列表 x 的 x[3]
仍然指向原来的列表 ['香蕉', '瓜子', '八宝粥']
,并不会受到影响。只要针对某个索引重新赋值为新的对象,原列表对应位置的引用不会跟着变动,自然也就不会再相互影响。
简而言之,浅拷贝只会在“修改同一个嵌套对象本身”时产生联动影响;如果是直接给某个嵌套对象赋予一个新引用,则不会影响到原列表。
通过上面两个示例的对比,我们可以清晰地看到浅拷贝与深拷贝的区别,也了解到在一些特殊操作方式下,浅拷贝的修改并不会波及到原列表。一般而言,如果你只关心最外层结构的复制,且内部嵌套对象可以共享,那么浅拷贝就够用了;而当你需要创建完全独立的副本时,深拷贝才是更合适的选择。
结论一:copy()
只能进行浅拷贝,嵌套对象不会被复制。
结论二:deepcopy()
进行递归复制,完全独立于原对象。
结论三:浅拷贝仅在修改嵌套对象时才会导致意外影响,直接替换元素不会影响原列表。
你好,我是悦创。
因为出版社书稿字数、页数限制等因素,本页面为本书相关资源。如果有部分想要详细讲解或补充,请直接评论即可!「支持实时更新内容」
<div class="catalog-display-container">
<Catalog base='/Books/' hideHeading='false' />
</div>
前面我们本地化部署了 ChatGLM3-6B,对于大模型有了进一步的了解。
目前我们接触的无论是千亿大模型,如 130B、ChatGPT,还是小规模的大模型,如 6B、LLaMA2,都是通用大模型,就是说通过通用常识进行预训练的,如果我们在实际使用过程中,需要大模型具备某一特定领域知识的能力,我们就需要对大模型进行能力增强,具体如何做呢?
微调只是众多优化大模型能力的方法之一,除此之外,还有其他方式,比如接入外挂知识库,或者借助 Agent 调用外部 API 数据源。下面我们来具体看看这些方法的不同之处。
微调指的是在已有预训练模型的基础上,通过特定任务的数据再次训练,使模型更好地适应特定场景。这种方式相对成本较低,模型能够从训练数据中学习特定领域的表达和逻辑,具备一定的语义理解能力。
知识库通过构建向量数据库或其他结构化数据源,为大模型提供一个额外的信息查找渠道,充当“外挂记忆”。
API 接入与知识库类似,也是为模型提供实时信息的外部接口,能够扩展模型的知识边界。
检查你的显卡:
👉 打开命令行,输入:
nvidia-smi
自动生成侧边栏,侧边栏文章顺序默认安装数字升序排练。
"/Books/": "structure"