写 Python 时,你很快就会写出 name = "小林"、score = 92 这样的代码。它们看上去简单,真正容易让新手摔跟头的地方却藏在后面:为什么两个变量会一起变化?为什么 "18" + 1 会报错?为什么列表切片有时像复制,有时又没有彻底分开?
这些问题其实都绕着同一条主线:Python 程序处理的是对象,变量名只是我们用来找到对象的名字;对象的类型决定它能做什么,也决定它能不能在原地改变。先把这层关系弄清楚,后面的数字、字符串、列表和元组就不会变成一堆孤立的语法。
这一章会用一个“整理报名信息”的小场景贯穿讲解。我们会从一个姓名、一个年龄,逐步扩展到课程清单和遍历输出。每遇到一个常见坑,都先看它怎么发生,再把背后的机制说清楚,最后给出更稳妥的写法。

Python 的常见数据类型各有不同的行为,后续操作都建立在这些类型差异之上。
先看最普通的一行赋值:
student_name = "林晓"很多入门材料会说“变量是盒子,里面装着值”。这个比喻方便起步,却解释不了共享修改的问题。更贴近 Python 实际行为的理解是:运行右边的表达式,得到一个字符串对象,再把名字 student_name 绑定到这个对象上。
这里有三个角色:
student_name 是名称,也就是代码里使用的标识符;"林晓" 是一个字符串对象,它有自己的值和类型;= 建立名称到对象的绑定关系,它不是数学里的“左右永远相等”。所以,下面的代码完全合法:
status = "待确认"
print(status)
status = "已确认"
print(status)输出是:
待确认
已确认第二次赋值不是钻进原来的字符串里,把“待确认”改成“已确认”。它会让 status 改为指向另一个字符串对象。原对象如果不再被任何地方使用,之后可以由 Python 回收;我们平时不用手动管理这件事。
再看一段很容易被忽略的代码:
courses = ["Python", "数据库"]
selected_courses = courses
selected_courses.append("计算机网络")
print(courses)
print(selected_courses)结果是:
['Python', '数据库', '计算机网络']
['Python', '数据库', '计算机网络']selected_courses = courses 没有复制列表,只是给同一个列表对象又贴了一个名字。通过任意一个名字修改这个列表,另一个名字再次查看时都会看到新内容。这种关系叫作别名,它是 Python 初学阶段最常见的“明明没改它,它怎么变了”的来源。
Python 可以一次给多个名称赋值:
student_name, age, city = "林晓", 18, "杭州"右边会先得到三个对象,左边再按位置接收。名称数量与值数量不匹配时会报 ValueError,因为 Python 不知道缺少的名称应该绑定什么,或者多出来的值该交给谁。
这种“按结构接收值”的写法也叫解包。交换两个名称的绑定时,不需要手写一个临时变量:
first_score = 88
second_score = 95
first_score, second_score = second_score, first_score
print(first_score, second_score) # 95 88先计算右边这一点很关键。Python 会先拿到交换前的两个对象,再完成左边的绑定,不会因为先改了 first_score 而丢失原值。
还有一种短写看起来方便,实际会制造共享:
morning_courses = afternoon_courses = []
morning_courses.append("Python")
print(afternoon_courses) # ['Python']同一个空列表对象同时绑定给两个名称。想要两份独立清单,就应分别创建:
morning_courses = []
afternoon_courses = []这里没有“多重赋值一定不好”的结论。给多个名称绑定整数、字符串这类不可变对象,通常不会产生共享修改;给多个名称绑定同一个可变对象时,才要明确这是不是你真正需要的关系。

变量更像对象上的名称标签;重新赋值会让名称改为指向另一个对象。
下面的实验把名称和对象分开放在两块区域。请按顺序执行重新绑定与列表追加,观察“名称换了指向”和“原对象内容改变”在画面上有什么不同。
判断一行赋值时,可以在心里念一遍:先算右边,得到对象,再让左边的名字指向它。这个顺序能解释重新赋值、多重赋值、别名以及很多函数参数问题。
id() 观察身份,用 type() 观察类型Python 中每个对象都有身份、类型和值。id() 可以返回一个用来代表对象身份的整数,type() 可以告诉我们对象的类型:
score = 95
print(type(score))
print(id(score))你每次运行时看到的 id 数字可能不同,所以不要把它当成业务数据保存。它适合用来观察“两个名字现在是否指向同一个对象”。更直接的身份比较写法是 is:
first = [1, 2]
second = first
third = [1, 2]
print(first == second) # True,内容相等
print(first is second) # True,是同一个对象
print(first == third) # True,内容相等
print(first is third) # False,不是同一个对象== 询问“值是否相等”,is 询问“是否就是同一个对象”。日常比较数字、字符串或列表内容时用 ==;判断一个值是不是 None 时用 is None。不要为了省事把所有相等判断都写成 is。
变量名首先得符合语法。Python 名称可以包含字母、数字和下划线,但不能以数字开头,也不能使用 for、class、if 这样的关键字。
student_name = "林晓" # 合法
score_2026 = 95 # 合法
_temporary = "草稿" # 合法,但开头下划线通常带有“内部使用”的约定含义下面这些写法不行:
# 2nd_score = 95 # 不能以数字开头
# student-name = "林晓" # 连字符会被理解成减法符号
# class = "二班" # class 是关键字Python 允许中文名称,例如 学生姓名 = "林晓" 在语法上没问题。不过,在需要跨团队协作、调用第三方库或与大量英文 API 配合的项目中,英文小写加下划线通常更方便。中文可以继续用在注释、界面文本和示例数据中。
对比下面两组名字:
n = "林晓"
s = 95
l = ["Python", "数据库"]student_name = "林晓"
exam_score = 95
selected_courses = ["Python", "数据库"]第二组略长,却省掉了猜测。新手常担心名字长会不会“拖慢程序”,这个担心可以放下:变量名首先是写给人看的。清楚的名字能降低拼错和误用的概率,也让报错信息更容易定位。
一般变量名使用小写字母,多个单词用下划线分隔。约定不修改的模块级值常写成全大写:
MAX_COURSES = 6
DEFAULT_CITY = "杭州"这里的“大写常量”仍然可以被重新赋值,Python 不会强制保护它。大写是一种协作约定,意思是“正常情况下不要改”。
还要避免用 list、str、type、input 这类内置名称当变量名。它们不一定触发语法错误,却会遮住原本的内置功能:
# 不推荐
list = [1, 2, 3]
# 后面再调用 list("abc") 时,list 已经不是内置类型了更稳妥的名字是 numbers、items 或 course_list。如果变量含义实在与关键字接近,常见约定是在末尾加下划线,例如 class_,但能换成 course_class 之类更明确的词时,通常更好。
Python 常被称为动态类型语言。这里的“动态”不是说对象会随时偷偷换类型,而是说名称没有被固定为某一种类型。同一个名称在不同时刻可以绑定到不同类型的对象:
value = 18
print(type(value))
value = "18"
print(type(value))输出:
<class 'int'>
<class 'str'>第一次的整数对象始终是整数,第二次的字符串对象始终是字符串。发生变化的是 value 的绑定。理解成“名字换了指向”,比理解成“变量本身从整数变成字符串”更准确。
动态绑定给小程序带来便利,但不意味着随便混用类型也能运行。类型决定一个对象支持哪些操作:
age_text = "18"
# print(age_text + 1)
# TypeError: 字符串和整数不能直接相加Python 不会因为字符串看起来像数字,就擅自猜测你想做加法还是拼接。你需要明确转换:
age_text = "18"
next_age = int(age_text) + 1
print(next_age)这样得到整数 19。如果你的目的是组成一段文字,就反过来把数字格式化进字符串:
age = 18
message = f"明年 {age + 1} 岁"
print(message)type() 用来观察,isinstance() 用来判断一类对象type(value) 会返回对象当前的具体类型,特别适合刚学习时观察表达式结果:
print(type(3)) # <class 'int'>
print(type(3.0)) # <class 'float'>
print(type("3")) # <class 'str'>程序真正需要判断“这个值能否按某类对象处理”时,常用 isinstance():
value = 18
print(isinstance(value, int)) # True它会考虑类型之间的继承关系,而 type(value) is int 只接受具体类型恰好是 int。例如 Python 的 bool 是 int 的子类:
print(type(True)) # <class 'bool'>
print(isinstance(True, int)) # True这解释了 True + True 可以得到 2,但业务代码里仍应把布尔值当作真假状态,而不是拿它悄悄代替数量。需要数字 0 或 1 时明确转换,读者才知道这是有意为之。
也别把类型检查铺满每一行。若一段程序刚从用户输入获得年龄,在入口完成 strip()、int() 和范围验证,后面就可以把 age 当整数使用。反复检查同一个内部变量往往会遮住真正的数据流。类型观察的目的,是帮助我们理解和守住边界,不是让代码对任何混乱输入都沉默接受。

名称本身没有固定类型,当前绑定的对象决定了取到的值和类型。
语法允许反复改变类型,不等于业务代码里应该这样做:
score = 95
score = "缺考"这段代码能运行,但后面算平均分时就会突然遇到类型问题。更清楚的做法是拆开含义:
score = None
attendance_status = "缺考"名字一旦表达了明确含义,尽量让它在一段逻辑里保持这种含义。必要时用 type() 或 isinstance() 检查外部输入,但不要在每行代码后都检查类型。更有效的做法是:在数据刚进入程序时转换和验证,后面都使用已经整理好的数据。
本章会反复遇到几类常用对象:int 和 float 表示数字,str 表示文本,bool 表示真假,list 与 tuple 表示有顺序的一组对象,None 表示当前没有值。它们不是几种“变量”,而是名称可能绑定到的不同对象类型。
类型也不是一张只供查看的标签。它直接决定 + 的含义:数字相加会计算和值,字符串相加会拼接文本,列表相加会拼接序列;不兼容的两个对象相加则抛出 TypeError。看到运算符时别只背符号,要一起看左右对象是什么类型。
名称绑定解释了“谁指向谁”,可变性解释了“操作之后还是不是原来的对象”。这两部分放在一起,才足以判断代码会不会影响别处。
常见的不可变类型包括整数、浮点数、布尔值、字符串和元组。常见的可变类型包括列表和字典。不可变的意思是:对象创建后,它自身的值不能在原地改掉。你写出的“修改”通常是得到另一个对象,再重新绑定名称。
score = 90
old_score = score
score = score + 5
print(score) # 95
print(old_score) # 90整数 90 没有变成 95。表达式 score + 5 得到新整数,score 改为指向它;old_score 仍然指向原来的 90。因此,共享一个不可变对象通常不会出现“一方修改、另一方跟着变”的现象。
列表的行为不同:
scores = [90, 88]
backup_name = scores
scores.append(95)
print(scores) # [90, 88, 95]
print(backup_name) # [90, 88, 95]append() 在原列表对象上增加元素,没有让 scores 换到新列表,所以另一个名称会看到变化。
下面两段代码都出现了列表和赋值,影响范围却不同:
first = [1, 2]
second = first
first = first + [3]
print(first) # [1, 2, 3]
print(second) # [1, 2]first + [3] 产生一个新列表,随后 first 重新绑定。second 仍指向旧列表。
first = [1, 2]
second = first
first.append(3)
print(first) # [1, 2, 3]
print(second) # [1, 2, 3]append() 修改原列表,于是两个名称都能看到变化。排查共享问题时,真正需要问的是:“这一步创建了新对象,还是在原对象上改?”
+= 对列表通常会尝试原地扩展,因此也值得单独留意:
first = [1, 2]
second = first
first += [3]
print(second) # [1, 2, 3]而普通的 first = first + [3] 会得到新列表。两种写法结果表面相似,存在别名时却不等价。代码里一旦有共享对象,就不要把 += 机械理解成更短的 x = x + y。
字符串方法很像在修改原文:
raw_city = " 杭州 "
clean_city = raw_city.strip()其实 strip() 返回了一个新字符串。原字符串仍然存在,raw_city 仍指向它。所有看起来会改变字符串内容的方法,都必须通过返回值把新结果交出来。理解这点后,“为什么 text.strip() 执行了却没有保存效果”就不神秘了。
元组的不可变性还多一层细节:不能替换的是元组自己的元素引用。如果某个元素指向可变列表,列表内部仍可改变。换句话说,可变性属于每个具体对象,不能只看最外层括号就下结论。
选择列表还是元组,不只是方括号和圆括号的区别。先看数据之后会怎样变化:课程清单需要追加、删除和重新排序,列表合适;屏幕分辨率的宽高是一组固定位置,元组更能表达“不增删位置”的意图;一段文本需要加工时,字符串虽然不可变,但通过返回新字符串就能完成工作。
拿不准类型时,可以连续问三个问题。第一,这份数据表示什么:"18" 是用户刚输入的文字,18 才是准备参与年龄计算的数量;电话号码虽然全是数字,却常常不做加减,保留成字符串反而合理。第二,它之后会怎样操作:需要求和就用数字,需要按位置逐个处理就用序列,需要增删元素就优先考虑可变容器。第三,空缺怎样表达:尚未填写用 None,已经填写但内容为空用空字符串,数量确实为零就用 0。把含义分开后,条件判断才不会把不同状态误当成同一件事。
类型选择不必追求“看起来最节省字符”。例如邮政编码可能以零开头,如果转成整数,开头的零会消失;学号也许含字母,或者未来会改变编码规则。这些数据虽然长得像数字,实际用途是标识,不参与算术,用字符串更稳妥。反过来,价格从输入框读到时即使是字符串,也不能一直以文本保存,因为字符串排序会把 "100" 放在 "20" 前后一个不符合数值直觉的位置。先按业务含义选类型,再写转换代码,错误会少很多。
判断可变性时可以做一个小实验:保留旧名称,再执行操作,最后分别打印两个名称并用 is 比较身份。实验的目的不是依赖某次 id 数字,而是看操作究竟重新绑定了名称,还是改动了共享对象。
报名系统里,年龄、课程数、价格和折扣都可能是数字,但它们并不都属于同一种数字对象。最常见的是整数 int 和浮点数 float。
age = 18 # int
course_count = 3 # int
price = 199.0 # float
discount = 0.85 # floatPython 的整数可以非常大,实际范围主要受可用内存限制。大整数可以用下划线分组,让人更容易数位数:
annual_visits = 12_500_000
print(annual_visits) # 12500000下划线只是源码里的可读性标记,不会出现在数值本身。
print(7 + 3) # 10
print(7 - 3) # 4
print(7 * 3) # 21
print(7 / 3) # 2.3333333333333335
print(7 // 3) # 2
print(7 % 3) # 1
print(/ 是真除法,即使刚好整除也返回浮点数:
print(8 / 4) # 2.0
print(type(8 / 4)) # <class 'float'>// 是向下取整的整除。对正数来说,它看起来像“丢掉小数部分”;碰到负数时要更小心:
print(-7 // 3) # -3因为 -3 才是不大于 -2.333... 的整数。需要“朝零截断”时,不要把 // 当万能工具,可以明确使用 int(),同时在代码里说明你的业务意图。
整数和浮点数一起计算时,结果通常是浮点数:
print(3 + 0.5) # 3.5
print(2 * 3.0) # 6.0
print(type(3 + 0.5)) # <class 'float'>不要只看输出“像不像整数”。6.0 与 6 比较值时相等,但类型不同;把结果交给只接受整数位置的操作时,区别会显出来:
count = 6.0
# range(count) # TypeError:range 需要整数这时先追问 count 为什么变成浮点数。若它本来就表示课程数量,最好从数据入口让它保持整数;如果是某次除法得到的结果,就要明确舍入规则,不能只因为代码报错而随手套 int()。
运算顺序遵循常见的数学优先级,幂和乘除先于加减,括号可以明确改变顺序:
total = 20 + 5 * 2
grouped = (20 + 5) * 2
print(total) # 30
print(grouped) # 50即使你记得优先级,复杂表达式里也可以用括号把意图写清楚。括号不是“基础不好才用”,它能让下一位读代码的人少做一次心算。
round() 用于按位数舍入显示或继续计算,但二进制浮点表示会影响边界结果,而且 Python 的常见舍入规则不一定等于现实业务中的“逢五必进”。凡是学费、税费、积分结算之类有制度规则的数据,先确认规则,再选择算法和类型。
下面这个结果几乎每个初学者都会遇到:
print(0.1 + 0.2)输出可能是:
0.30000000000000004原因不是 Python 算错了,而是计算机通常用二进制浮点格式保存小数,很多看起来有限的十进制小数无法被精确表示。存进去的其实是一个非常接近原值的数,运算后那点差异显了出来。
界面展示可以使用格式控制:
total = 0.1 + 0.2
print(f"合计:{total:.2f}") # 合计:0.30但格式化只改变显示,不会改变底层误差。遇到金额结算等需要明确十进制规则的场景,后续应该学习标准库里的 decimal.Decimal,而不是依赖 round() 把所有问题盖住。
不要用 0.1 + 0.2 == 0.3 这类写法判断浮点计算结果是否“完全相等”。一般数值计算会比较两者是否足够接近;涉及金额时则应选择符合业务精度要求的表示方式。
course_count = 2
course_count += 1
print(course_count) # 3对整数来说,+= 计算出新整数,再让名称指向新对象。对列表来说,+= 通常会在原列表上扩展,这就可能让所有别名一起看到变化。相同符号会根据类型表现出不同细节,所以理解对象是否可变比死记符号更可靠。
字符串 str 用来表示文本。单引号、双引号都能创建字符串,选择能减少转义的一种即可:
message = '老师说:“先运行一次再判断。”'
english = "I'm learning Python."字符串是 Unicode 文本序列,可以放中文、英文、标点和表情。Python 没有单独的“字符类型”,对字符串取一个位置,得到的仍然是长度为 1 的字符串。
course = "Python入门"
print(course[0]) # P
print(type(course[0])) # <class 'str'>反斜杠可以表示字符串里的特殊字符:
schedule = "上午:变量\n下午:列表"
print(schedule)输出:
上午:变量
下午:列表常见的 \n 表示换行,\t 表示制表符,\\ 表示一个反斜杠。如果字符串本身是 Windows 路径或正则表达式一类反斜杠很多的文本,可以了解原始字符串:
path = r"C:\new_course\notes"
print(path)原始字符串会让大多数反斜杠保留字面含义,但结尾不能只剩一个未配对的反斜杠。初学时先掌握普通转义,遇到路径再用原始字符串,比较不容易把两套规则混在一起。
raw_name = " lin xiao "
clean_name = raw_name.strip().title()
print(repr(raw_name)) # ' lin xiao '
print(repr(clean_name)) # 'Lin Xiao'strip()、lower()、upper()、title() 都会返回新字符串,原字符串不变。最常见的失误是调用了方法,却没有接住结果:
city = " 杭州 "
city.strip()
print(repr(city)) # ' 杭州 '正确写法要么保存结果,要么在马上使用的地方继续调用:
city = city.strip()
print(city)
字符串方法不会在原对象上直接改写内容,而是根据处理结果产生新的字符串。
当文字里需要带变量时,优先考虑 f 字符串:
student_name = "林晓"
age = 18
average_score = 92.375
summary = f"{student_name},{age} 岁,平均分 {average_score:.1f}"
print(summary)花括号里可以放表达式,:.1f 表示按一位小数显示。不要把复杂业务逻辑全塞进花括号,先用名字保存中间结果会更清楚:
next_age = age + 1
message = f"明年 {next_age} 岁"course = "Python基础"
print(course[0]) # P
print(course[-1]) # 础
print(course[:6]) # Python
print(course[6:]) # 基础索引取一个位置,切片取一个范围。字符串不可变,因此下面这行会报 TypeError:
course = "Jython基础"
# course[0] = "P"要“修改”字符串,就组合出一个新字符串:
course = "P" + course[1:]
print(course) # Python基础strip() 只负责去掉两端空白,不会判断姓名是否合理,也不会把任意文字变成数字:
raw_age = " 十八 "
clean_age = raw_age.strip()
print(clean_age) # 十八
# int(clean_age) 仍然会抛出 ValueError这是数据处理中很有用的一条分界:清理负责统一格式,验证负责判断是否符合规则,转换负责得到目标类型。三个动作可以连续写,但头脑里要分开。否则,看到 strip() 成功就容易误以为输入已经“安全可用”。
字符串支持 len() 和成员判断,可以用来做一些最基础的验证:
student_name = input("请输入姓名:").strip()
if not student_name:
print("姓名不能为空")
elif len(student_name) > 20:
print("姓名过长")
else:
print(f"已记录:{student_name}")检查某段文字是否出现,可以使用 in:
course_title = "Python 数据处理"
if "Python" in course_title:
print("这是 Python 相关课程")这里判断的是子字符串,不是把文本拆成单词后再判断。中文没有空格分词也不妨碍简单包含检查,但更复杂的名称规则需要根据真实业务定义,不能只靠几个字符串方法猜测。
连接多段文本时,少量内容用 f 字符串最直观;已有一组字符串时,join() 更合适:
courses = ["Python", "数据库", "网络"]
summary = "、".join(courses)
print(summary) # Python、数据库、网络分隔符写在 join() 前面,列表中的每一项必须是字符串。如果列表里混有整数,先明确每项应该怎样显示,再转换为字符串,不要指望 join() 自己猜。
布尔类型 bool 只有两个值:True 和 False,首字母必须大写。它通常来自比较:
age = 18
has_selected_course = True
print(age >= 18) # True
print(has_selected_course is True) # True日常条件判断不必写成 if has_selected_course == True:,直接写更清楚:
if has_selected_course:
print("可以提交报名信息")数字零、空字符串、空列表、空元组、None 和 False 在条件里都会被视为假,非空内容通常视为真:
print(bool(0)) # False
print(bool("")) # False
print(bool([])) # False
print(bool("0")) # True,因为这是一个非空字符串
print(bool([0])) # True,因为这是一个非空列表这个规则很方便,但不要让它吞掉业务差别。比如“还没有填写成绩”和“成绩就是 0”是两种不同状态:
score = None
if score is None:
print("成绩尚未录入")
elif score == 0:
print("成绩已录入,分数为 0")None 表示“没有值”或“尚未得到结果”。它不是数字 0,不是空字符串,也不是字符串 "None"。判断它时推荐写 is None 或 is not None,因为我们关心的是那个唯一的空值对象。
如果 0、空字符串和空列表在你的业务里都是合法数据,就不要只写 if not value: 来判断“缺失”。这行代码会把它们与 None 一起归到假值。需要区分缺失状态时,明确写 value is None。
当一个学生选择了多门课,用很多独立变量会很快失控:
course_1 = "Python"
course_2 = "数据库"
course_3 = "计算机网络"列表把相关数据按顺序放在一起:
courses = ["Python", "数据库", "计算机网络"]列表可以混放不同类型的对象:
record = ["林晓", 18, True]语法允许不等于就适合所有场景。如果一份列表表示“课程名称”,让元素都保持字符串,后面的排序、搜索和遍历会更简单。混合信息更适合在后续章节用字典或专门的数据对象表达。

不可变对象发生变化时会产生新对象;列表、字典和集合则可以在原对象上修改。
列表可变,下面这些操作都在原列表上发生:
courses = ["Python", "数据库"]
courses.append("计算机网络")
courses.insert(1, "数据分析")
courses[0] = "Python程序设计"
print(courses)输出:
['Python程序设计', '数据分析', '数据库', '计算机网络']append(x) 把一个对象作为一个新元素放到末尾。extend(iterable) 则逐个加入一批元素:
courses = ["Python"]
new_courses = ["数据库", "网络"]
courses.append(new_courses)
print(courses) # ['Python', ['数据库', '网络']]courses = ["Python"]
new_courses = ["数据库", "网络"]
courses.extend(new_courses)
print(courses) # ['Python', '数据库', '网络']两个结果都合法,结构却不同。想加入“一个列表对象”时用 append;想把其中元素逐个并入时用 extend。
courses = ["Python", "数据库", "网络"]
courses.remove("数据库") # 按值删除第一次出现的匹配项
removed = courses.pop() # 删除末尾元素,并把它返回
del courses[0] # 按位置删除,不返回被删元素remove() 找不到目标值时会报 ValueError,pop() 的位置不存在时会报 IndexError。如果删除前不确定值是否存在,可以先用 in 判断:
course_to_remove = "数据库"
if course_to_remove in courses:
courses.remove(course_to_remove)这是列表操作里非常典型的一跤:
scores = [88, 96, 75]
scores = scores.sort()
print(scores) # Nonesort() 已经把原列表排好序,它没有返回排好后的列表,而是返回 None。当你把结果重新赋给 scores,名字就离开了原列表,转而指向 None。
正确写法有两种,取决于是否要保留原顺序:
scores = [88, 96, 75]
scores.sort()
print(scores) # [75, 88, 96]scores = [88, 96, 75]
sorted_scores = sorted(scores)
print(scores) # [88, 96, 75]
print(sorted_scores) # [75, 88, 96]看到列表方法时先问一句:它是“修改原列表”,还是“返回新对象”?append()、extend()、insert()、remove()、reverse() 和 sort() 主要修改原列表,并返回 None;sorted() 是函数,会返回新的列表。
列表不只是用来装数据,也提供了几种很直白的查询方式:
courses = ["Python", "数据库", "Python"]
print(len(courses)) # 3
print("数据库" in courses) # True
print(courses.count("Python")) # 2
print(courses.index("数据库")) # 1len() 给出元素数量,in 判断是否存在,count() 统计出现次数。index() 返回第一次出现的位置,但找不到时会抛出 ValueError。如果“找不到”本来就是正常分支,先用 in 判断通常更易读。
嵌套列表要分清层级:
records = [
["林晓", 92],
["周宁", 88],
]
print(records[0]) # ['林晓', 92]
print(records[0][1]) # 92第一个索引先取出一条记录,也就是内层列表;第二个索引再从该记录中取成绩。只写 records[1] 得到的是完整的第二条记录,不是所有记录的第二列。实际项目里,当位置越来越多、看到 record[3] 已经猜不出含义时,应该考虑后续章节的字典,而不是继续堆索引。
列表切片还能直接替换一段内容:
courses = ["Python", "旧课程A", "旧课程B", "网络"]
courses[1:3] = ["数据库"]
print(courses) # ['Python', '数据库', '网络']这是对原列表的修改,别名也会看到结果。右侧元素数量可以与被替换区间不同,所以列表长度可能变化。初学时如果这让代码变得难追踪,使用清楚的删除和插入步骤也完全可以,简短不是唯一目标。
Python 序列从索引 0 开始。对五个元素的列表来说,合法的非负索引是 0 到 4:
courses = ["Python", "数据库", "网络", "操作系统", "算法"]
print(courses[0]) # Python
print(courses[4]) # 算法
print(courses[-1]) # 算法
print(courses[-2]) # 操作系统负索引从末尾往前数,-1 是最后一个元素。它很方便,但空列表没有最后一个元素:
courses = []
# print(courses[-1]) # IndexError所以,在列表可能为空时先判断:
if courses:
print(courses[-1])
else:
print("还没有选择课程")切片写成 sequence[start:stop:step]。起点包含,终点不包含:
courses = ["Python", "数据库", "网络", "操作系统", "算法"]
print(courses[1:4]) # ['数据库', '网络', '操作系统']
print(courses[:3]) # 前三个
print(courses[2:]) # 从索引 2 到末尾
print(courses[-2:]) # 最后两个
print(courses[::2]) # 每隔一个取一个
print为什么终点不包含?因为 courses[:3] 恰好取得三个元素,courses[1:4] 的长度恰好是 4 - 1。相邻区间也能无缝拼接:courses[:2] 和 courses[2:] 不重复、不遗漏。

切片会创建新的外层列表;如果元素本身是可变对象,内层对象仍可能被两个列表共享。
items = ["A", "B", "C"]
# print(items[10]) # IndexError
print(items[:10]) # ['A', 'B', 'C']单个索引明确要求“给我这个位置的元素”,位置不存在就报错。切片表达“尽量取这个范围”,边界超过序列长度时会收缩到有效范围。这不是前后矛盾,而是两种操作的语义不同。
元组 tuple 也是有顺序的序列,也支持索引、切片和遍历。它与列表最显眼的区别是:元组不能替换、添加或删除元素。
screen_size = (1920, 1080)
point = (120, 80)
print(screen_size[0])
print(point[-1])一个元素的元组必须带逗号:
not_a_tuple = (5)
one_item_tuple = (5,)
print(type(not_a_tuple)) # <class 'int'>
print(type(one_item_tuple)) # <class 'tuple'>真正构成元组的是逗号,圆括号主要帮助分组和阅读。空元组则写作 ()。
下面这个细节很容易超出“元组不能改”的简单记忆:
profile = ("林晓", ["Python", "数据库"])
profile[1].append("网络")
print(profile)输出:
('林晓', ['Python', '数据库', '网络'])元组的两个位置仍然指向原来的字符串对象和列表对象,我们没有替换元组元素。发生变化的是第二个位置指向的列表内容。元组保证的是自身元素引用不能被替换,不会把里面的可变对象“冻住”。
# 下面才是在替换元组的元素,会报 TypeError
# profile[1] = ["算法"]什么时候选元组?当一组位置有固定结构,且你希望代码明确表达“不要增删这些位置”时,可以使用元组,例如二维坐标、颜色的 RGB 三个分量。需要频繁添加、删除和排序时,列表更自然。
假设你要根据原课程清单做一份可修改的推荐清单。直觉上可能这样写:
original_courses = ["Python", "数据库"]
recommended_courses = original_courses
recommended_courses.append("算法")
print(original_courses)结果原清单也出现了“算法”。原因已经见过:赋值只增加名字,没有创建新列表。
original_courses = ["Python", "数据库"]
recommended_courses = original_courses[:]
recommended_courses.append("算法")
print(original_courses) # ['Python', '数据库']
print(recommended_courses) # ['Python', '数据库', '算法']也可以写:
recommended_courses = original_courses.copy()现在外层列表是两个不同对象,所以给推荐清单添加元素不会改变原清单。

两个名称绑定同一个列表时,从任一名称修改列表,另一边都会看到变化。
这个工作台会同时展示原列表、别名和切片副本。先拖动起止刻度生成切片,再分别修改三者;比只看静态输出更容易发现哪一层对象真正被复制。
事情到嵌套列表这里又多了一层:
original = [
["Python", 92],
["数据库", 88],
]
copied = original.copy()
copied[0][1] = 100
print(original)
print(copied)两边第一个成绩都会变成 100。copy() 创建了新的外层列表,但两个外层列表里的元素仍然指向同一批内层列表。这叫浅复制。
我们可以分步骤观察:
original.copy() 先创建一个新的外层列表,所以 original is copied 是 False。
复制时只把两个内层列表的引用放进新外层列表,没有继续复制内层对象,所以 original[0] is copied[0] 是 True。
copied[0][1] = 100 修改的是共享的第一个内层列表,因此从两个外层列表进入,都能看到同一个变化。
如果只是要替换整个内层元素,可以先构造新值;确实需要把嵌套结构独立复制时,再使用标准库 copy 模块的 deepcopy():
from copy import deepcopy
copied = deepcopy(original)
copied[0][1] = 100深复制不是越多越好。它会递归复制结构,数据量大时更费时间和内存,某些包含文件、连接或自定义对象的结构也不适合盲目深复制。先确认是否真的需要完全独立,往往比直接套 deepcopy() 更重要。
rows = [[]] * 3
rows[0].append("已选")
print(rows)输出:
[['已选'], ['已选'], ['已选']][[]] * 3 重复的是同一个内层列表的引用。想得到三个独立列表,可以逐次创建:
rows = [[] for _ in range(3)]
rows[0].append("已选")
print(rows) # [['已选'], [], []]遇到“改一个,多个一起变”,先不要怀疑 Python 随机出错。优先检查这些对象是不是通过直接赋值、浅复制或容器重复共享了同一个可变对象。
input() 返回的永远是字符串。用户输入 18,拿到的是 "18",不是整数 18:
age_text = input("请输入年龄:")
print(type(age_text)) # <class 'str'>如果马上计算,会遇到 TypeError:
# next_age = age_text + 1更稳妥的流程是“读取 → 清理 → 转换 → 使用”:
用 input() 读取原始文本,暂时把名字写成 age_text,提醒自己它还不是可计算的年龄。
用 strip() 去掉输入两端意外出现的空白,得到清理后的字符串。
用 int() 明确转换为整数。如果内容不符合整数格式,Python 会抛出 ValueError,这正好告诉我们输入内容需要处理。
转换成功后再参与加法,把后续代码建立在稳定的数据类型上。
age_text = input("请输入年龄:").strip()
age = int(age_text)
next_age = age + 1
print(f"明年你就 {next_age} 岁了")print(int("18")) # 18
print(float("18.5")) # 18.5
print(str(18)) # '18'
print(list("中文")) # ['中', '文']
print(tuple([1, 2])) # (1, 2)int() 处理浮点数时会朝零截断,不是四舍五入:
print(int(3.9)) # 3
print(int(-3.9)) # -3下面两个失败看起来很像,异常类型却不同:
# int("十八") # ValueError:对象类型可接受,但内容不是有效整数文本
# int([18]) # TypeError:这种对象类型不符合 int() 的使用方式bool() 也不是在解析自然语言:
print(bool("False")) # True因为任何非空字符串都是真值。如果输入只有 yes/no、是/否 等选项,应该自己定义允许的文本:
answer = input("是否继续(是/否):").strip()
should_continue = answer == "是"
外部输入通常先是文本;转换成功后才能参与计算,失败时应给出明确提示。
在下面的“类型转换诊所”里选择或输入一段文本,再尝试 str、int、float 与 bool。重点观察:同一份内容为什么有的转换成功,有的得到 ValueError,以及非空字符串为什么会被 bool() 判为真。
本章先不展开完整的异常处理语法,但你应该知道错误在告诉你什么:
TypeError:这个类型不支持当前操作,例如字符串与整数直接相加;ValueError:类型方向没错,但具体内容不符合要求,例如 int("十八");IndexError:序列中没有你要求的位置,例如空列表的 items[-1];NameError:使用了尚未绑定的名称,常见原因是拼写错误或赋值语句没有执行。不要只盯着 Traceback 的第一行。先看最后一行的异常类型和说明,再往上找自己代码中最后出现的位置,通常更快。
拿到错误信息后,可以按一个固定顺序缩小范围。先确认出错行两边对象的实际类型,必要时临时打印 type();再确认变量来自哪里,尤其留意 input()、文件或网络返回的文本;接着检查索引和切片边界;最后回看之前有没有原地修改列表或把方法返回的 None 覆盖到原变量。
例如下面的错误表面发生在加法处:
course_count = input("课程数:")
total = course_count + 1真正要修的不是加号,而是输入边界。让数据进入程序时就转换,后面逻辑会更干净:
course_count_text = input("课程数:").strip()
course_count = int(course_count_text)
total = course_count + 1再比如 IndexError 出现在 courses[0],不要把索引从 0 随手改成 -1。如果列表为空,两个位置都不存在。应该回头判断列表为什么可能为空,以及空列表时程序应显示提示、跳过处理还是要求重新输入。
错误类型是一条线索,不是一份自动修复指令。看懂对象类型和数据来源之后再改,通常能同时修掉一整类问题,而不只是让当前这一行勉强运行。
刚学循环时,很多人会先想到下标:从 0 开始,不断给 courses[i]。Python 的 for 更直接,它会从可迭代对象中逐个取得元素:
courses = ["Python", "数据库", "计算机网络"]
for course in courses:
print(f"已选择:{course}")每轮开始时,名称 course 会绑定到这一次取出的元素。列表遍历完,循环自然结束。字符串、元组、range 等也可以迭代:
for char in "Python":
print(char)for number in range(3):
print(number)range(3) 依次产生 0、1、2,不包含终点 3。它不是先创建一个完整列表,而是用紧凑的信息表示一个整数序列,遍历时再逐个给出值。

循环通过统一的迭代规则逐个取得元素,因此列表、字符串和文件都可以被遍历。
如果要显示序号,新手常写:
for index in range(len(courses)):
print(index + 1, courses[index])这能运行,但 enumerate() 更贴近“同时拿序号和值”的意图:
for number, course in enumerate(courses, start=1):
print(f"{number}. {course}")输出:
1. Python
2. 数据库
3. 计算机网络for course in courses:
print(f"正在登记:{course}")
print("登记完成")
print("全部课程处理完毕")循环体里两行都会对每门课执行一次,最后一行没有缩进,只在循环结束后执行一次。Python 用缩进表达代码块,建议每层使用四个空格,不要混用 Tab 和空格。
numbers = [1, 2, 2, 3]
for number in numbers:
if number == 2:
numbers.remove(number)
print(numbers)你可能期待两个 2 都被删掉,实际却可能留下一个。循环正在沿着位置向前走,删除元素会让后面的元素左移,于是某个元素被跳过。
简单场景可以遍历副本:
for number in numbers.copy():
if number == 2:
numbers.remove(number)更常见的思路是构造一个新列表:
numbers = [1, 2, 2, 3]
kept_numbers = []
for number in numbers:
if number != 2:
kept_numbers.append(number)
print(kept_numbers) # [1, 3]遍历的同时改变同一个列表的长度,索引移动会让程序很难推理。初学阶段优先遍历副本或构造新列表,等你确实理解迭代过程后,再考虑是否需要原地修改。
循环变量只是每一轮绑定到当前元素的名称。给这个名称重新绑定,通常不会替换列表中的对应位置:
scores = [80, 90, 100]
for score in scores:
score = score + 5
print(scores) # [80, 90, 100]整数不可变,score + 5 得到新整数,循环变量 score 指向它;列表里的元素引用没有被替换。想得到加分后的新列表,可以明确构造:
updated_scores = []
for score in scores:
updated_scores.append(score + 5)如果列表元素本身是可变对象,通过循环变量修改该对象时,原列表就能看到:
records = [["林晓"], ["周宁"]]
for record in records:
record.append("已确认")
print(records)这里没有给 record 重新绑定,而是对它指向的内层列表执行 append()。这正好把本章主线串了起来:循环每轮建立绑定,元素类型决定操作是否原地发生,共享引用决定变化从哪里可见。
for course in courses:
print(course)单数名称 course 表示当前一门课,复数名称 courses 表示整份课程列表。若两边都写成 course 或都写成 data,程序也许能运行,阅读时却很容易混淆当前元素和整个容器。
嵌套列表遍历可以直接解包固定结构:
records = [
("林晓", 92),
("周宁", 88),
]
for name, score in records:
print(f"{name}:{score} 分")每个二元组正好拆成姓名和成绩。如果某条记录缺少成绩或多出字段,解包会报 ValueError,提醒我们数据结构不一致。这个报错比默默把错位数据继续算下去更有用。
下面把本章知识连起来:读取学生姓名和年龄,维护课程列表,再输出确认信息。先不加入复杂异常处理,重点看数据从文本输入到稳定对象的过程。
student_name = input("请输入姓名:").strip()
age_text = input("请输入年龄:").strip()
age = int(age_text)
courses = ["Python基础", "数据处理"]
courses.append("项目实践")
print("\n报名信息")
print(f"姓名:{student_name}")
print
这里每个名字都说明了数据所处的阶段:age_text 是原始文本,age 是可参与计算的整数;courses 是会扩展的列表;is_adult 是比较得到的布尔值。程序不靠读者猜类型,而是用命名和转换把过程摆在明面上。
如果年龄可能输成空白或“十八”,int(age_text) 会抛出 ValueError。这并不是坏事,它暴露了输入边界。等学到异常处理后,可以在输入处捕获错误并让用户重新输入;在此之前,至少不要把转换藏到后面某个算式里,否则报错位置会离真正的问题越来越远。
先看名称:有没有拼写不一致,有没有用 list、str 等内置名称保存普通数据,每个名字能否说明含义。
再看输入:来自 input() 的内容是否仍是字符串,需要计算的部分有没有先清理并转换。
接着看可变对象:列表是否被多个名称共享,复制操作是不是只复制了外层,原地方法的返回值有没有被误赋回变量。
最后看边界:空列表是否会被索引,切片终点是否按“不包含”理解,遍历时有没有修改同一列表的长度。
这些练习不追求花哨,重点是让你亲手验证对象、类型和序列操作。建议先在解释器里预测结果,再运行代码核对。
阅读下面的代码,写出两行输出:
first = ["Python", "数据库"]
second = first
second.append("网络")
print(first)
print(first is second)下面的代码会报 TypeError,请在不改变输入方式的前提下修好它:
age_text = "18"
next_age = age_text + 1
print(next_age)不用逐个写元素,请用一次切片从下面列表中取得 "B"、"C"、"D":
letters = ["A", "B", "C", "D", "E"]先别运行,判断 backup 会输出什么:
records = [["林晓", 90], ["周宁", 85]]
backup = records.copy()
records[0][1] = 95
print(backup)请创建一个课程列表,至少包含三门课;用 enumerate(..., start=1) 输出带序号的课程;最后输出课程总数。不要在循环中手动维护序号。
你不必一次记住所有方法,先把下面四句话带走:
下次遇到 TypeError、IndexError 或“两个列表一起变”,先沿着这四句话排查。多数问题都能落到一个具体动作上:该转换就先转换,该复制就明确复制,该判断边界就先判断边界。
enumerate() 同时给出序号和值,避免用 range(len(courses)) 再取索引。len() 返回列表元素数量。