ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python基础第二周:数据类型、控制流与函数避坑指南

Python基础第二周:数据类型、控制流与函数避坑指南

这几天把Python基础啃掉了第二块,从变量和简单计算往深走了一步,才发现写代码真正有意思的地方在于:同样的语法,不同人写出了截然不同的代码。这篇笔记想记录一下第二周的学习内容,核心是数据类型、控制流和函数三大块。适合刚学完环境和基础语法的朋友,也就是那种能跑通print("hello")、但还不太理解列表和元组到底差在哪的阶段的读者。

我不打算面面俱到讲官方文档,那样跟翻手册没区别。我想以我这周实际遇到困惑和查资料的经历为线索,把关键知识点串起来,顺便把踩过的坑写出来,方便以后再翻。

1. 数据类型:先搞清楚Python里到底存的是什么

1.1 整数与浮点数:除法带来的第一个认知冲击

之前我一直觉得数字就是数字,做加减乘除还能有什么花样。直到我用Python算了个简单的除法:7 / 2,结果不是3,是3.5,这没问题;但当我写7 // 2的时候,结果是3,地板除直接把小数部分砍掉了。刚看到这个结果确实愣了一下,后来才明白这跟Python 3的除法设计有关系——单斜杠是真正意义上的除法,返回浮点数;双斜杠是整除,向下取整。

我试着把所有组合列了出来,才发现这里面的门道比想象中多:

表达式结果类型
10 / 42.5float
10 // 42int
-10 // 4-3int(注意是向下取整)
10 % 42int
-10 % 42int

-10 // 4等于 -3 这件事我一开始怎么都想不明白,明明 -2.5 向下取整怎么会是 -3?后来才理解,floor的意思是"往更小的方向取",-3比-2.5小,所以取 -3。这个特性在写算法、处理循环索引时非常容易踩坑,比如倒序遍历数组时,range(-1, -len(arr)-1, -1)这种写法一旦搞混取整方向就会出bug。

数字类型还有一个很容易忽略的细节:整数在Python里可以无限大,只要内存放得下。我当时测试了一下2 ** 1000,结果直接输出一个300多位的大整数,这在C语言里早就溢出了。Python自动处理大整数溢出这件事确实省心,但也会让初学者低估数据类型本身的边界问题,等以后玩底层库(比如numpy)时,才真正体会到类型转换的痛。

符号+=、*=这类复合赋值我也专门练了练,它跟a = a + 1完全等价,只是写法更简洁。不过有一点要注意:对于不可变类型(比如整数、字符串),+=实际上是创建新对象再重新绑定;而对于可变类型(比如列表),它可能是在原地修改。理解了这一层,后面学函数传参、默认参数才不会莫名其妙出错。

1.2 字符串:不可变背后的含义

字符串这块内容量比我想象中大得多。先记住一个结论:字符串是不可变对象。

什么叫不可变?就是一旦创建,就不能修改里面的某个字符。比如:

s = "hello" s[0] = "H" # 报错:'str' object does not support item assignment

我第一次看到这个报错时挺蒙的,不是经常看到别人给字符串重新赋值吗?后来才理解,重新赋值是让变量指向一个新字符串,而不是改动原字符串本身。就好比你换了个新手机,但旧手机本身没被改造。这个特性对安全性和多线程很重要——多个地方同时引用同一个字符串时,谁也不担心它被改掉。

字符串的切片操作真是学Python以来觉得最爽的语法:

s = "python学习笔记" print(s[0]) # p print(s[-1]) # 记 print(s[0:6]) # python print(s[::-1]) # 记笔习学nohtyp

[::-1]直接反转字符串,一行代码搞定,我一开始看到这种写法根本反应不过来,后来把原理搞清楚就顺手了:[start:stop:step],step为-1就是倒着走。这个语法很值得多写几遍,因为列表、元组、字典这些容器类型全都遵循同一套索引规则,学会了字符串切片,就等于学会了所有序列类型的切片。

字符串拼接有三个办法:+、join()、f-string。我用多了之后强烈推荐f-string(格式化字符串)。Python 3.6以后支持,写法就是:

name = "张三" score = 92.5 print(f"{name}本次得分{score:.1f}分")

直接在字符串里嵌变量,后面还能加格式控制符,比如.1f表示保留一位小数。对比一下传统方式:

print(name + "本次得分" + str(score) + "分") # 每个地方都要手动转换类型 print("{}本次得分{:.1f}分".format(name, score)) # format也挺好,但写起来略烦

f-string不仅省事,可读性还高了一大截。用了几次之后就回不去了,强烈的建议是:新代码一律用f-string,别用+拼接,也别用format(除非要动态构造格式化模板)。

1.3 布尔值与逻辑运算的直觉陷阱

布尔值只有True和False两个值,表面上简单,但真正容易栽跟头的地方是"哪些东西会被当成False"以及and/or的返回值。

Python里有一个概念叫"真值测试":在条件判断语境下,以下值被视为False:

  • False本身
  • 数字0和0.0
  • 空字符串""
  • 空列表[]、空元组()、空字典{}、空集合set()
  • None

其他一切值,包括负数、"0"这个字符串,都被视为True。

这带来一个很常见的用处:判断列表是否为空可以不用len(arr) == 0,直接写if not arr:,简洁且符合直觉。

但真正奇怪的是and和or的返回值。它们不只是返回True/False,而是返回参与运算的某个值本身:

print(0 and "hello") # 0 print(1 and "hello") # hello print(0 or "hello") # hello print("" or "默认值") # 默认值

原因是Python做了短路求值:A and B,如果A为假,整个结果就是A(不用再算B);如果A为真,结果取决于B。同理A or B,A为真就直接返回A,A为假才返回B。理解这一点之后,很多"为什么结果不是True/False"的困惑都会解开。

于是可以利用这个特性写很简洁的逻辑:

name = input("请输入名字: ").strip() name = name or "匿名用户" # 如果输入为空则使用默认值

当然如果追求代码可读性,这种写法可以适度使用,毕竟不是所有人都知道短路求值的细节。我自己在项目里遇到这种逻辑,更倾向于显式写下if not name: name = "匿名用户",团队协作时这种写法更安全。但个人脚本里,用or设默认值确实省几行代码。

2. 容器类型:列表、元组、字典与集合的选型逻辑

2.1 列表和元组:一个可变、一个不可变

列表是Python里用得最频繁的数据结构,它就像一块可以随时增删改的白板。常用操作我总结了一下:

arr = [1, 2, 3] arr.append(4) # 末尾加元素,变成 [1, 2, 3, 4] arr.insert(0, 0) # 下标0位置插入,变成 [0, 1, 2, 3, 4] arr.pop() # 删除并返回末尾元素,arr变成 [0, 1, 2, 3] arr.remove(1) # 删除第一个值为1的元素 arr.sort() # 原地排序 arr[::-1] # 反转(返回新列表)

元组(tuple)则恰恰相反,它是不可变的:

point = (3, 5) point[0] = 10 # 报错

你可能要问:那为什么要用元组而不全部用列表?最大的理由是"语义约束"。你希望某些数据创建后就不能被改动,用元组等于告诉读代码的人:"这个数据是不可变的,放心用"。比如地理坐标、函数返回的多个结果,用元组天然合适。第二个原因是性能:元组比列表占用内存更小、创建更快,面对大量数据时优势明显。第三个原因藏在字典的键里——列表不能作为字典的键(因为可变对象有哈希失效风险),元组可以。

我在实际练习时感受最深的是解包(unpacking):

a, b = (10, 20) x, y = 1, 2 # 不用括号也能解包 lst = [1, 2] c, d = lst # 列表也能解包

这个特性在交换两个变量时特别好用。以前别的语言交换变量要引入第三个临时变量,Python直接一行:

a, b = b, a

看到这种感觉特别爽。元组的不可变性不只是"保护数据",更重要的是让代码行为可预测,尤其在多线程环境中,不可变对象几乎没有并发风险。

2.2 字典:用"名字"找"值"

字典(dict)是Python里最接近"人脑思维"的数据结构。在别的语言里叫map或哈希表,核心就是键值对。生活中最典型的例子是通讯录:人名是键(Key),电话号码是值(Value)。

基本用法:

user = { "name": "A同学", "age": 18, "city": "某城市" } user["age"] = 19 # 修改值 user["score"] = 88 # 新增键值对 print(user.get("email")) # 不存在时返回None,不会报错 print(user.get("email", "未填写")) # 不存在时返回默认值

需要注意的坑是:直接访问不存在的键user["xxx"]会抛出KeyError,程序直接崩溃;而get()方法返回None或自定义默认值,不会中断流程。这个差异在写交互程序时非常关键。我一开始习惯直接下标访问,后来被KeyError搞烦了,就改成统一用get()了,写代码的心情立刻舒畅不少。

还有setdefault()方法也很好用——在键不存在时设置默认值,存在则保持原值不动:

counts = {} counts.setdefault("apple", 0) counts["apple"] += 1

遍历字典有几种姿势:

for key in user: # 只取键 for key, value in user.items(): # 同时取键和值,items()返回键值对列表 for value in user.values(): # 只取值

Python 3.7之后字典会保持插入顺序,这个特性让字典也可以当作有序结构用。但严格说字典的核心优势还是查找速度快——不管存100个还是10万个键值对,查找时间基本不变,哈希表的功劳。

2.3 集合:去重与关系运算

集合(set)的底层也是哈希表,但它只存key不存value,所以有两个天然特性:元素不重复+查找极快。

去重是集合最经典的场景:

scores = [88, 90, 88, 75, 90, 100] unique_scores = list(set(scores)) print(unique_scores) # [88, 90, 75, 100]

一行代码搞定去重,简单粗暴。当然,如果对顺序有要求,去重后列表顺序会打乱,这时可以改用dict.fromkeys(scores)或写循环保持原顺序。

集合的关系运算也很有用:

a = {1, 2, 3, 4} b = {3, 4, 5, 6} print(a & b) # 交集 {3, 4} print(a | b) # 并集 {1, 2, 3, 4, 5, 6} print(a - b) # 差集 {1, 2} print(a ^ b) # 对称差 {1, 2, 5, 6}

我在处理标签数据的练习题时,用了集合并集合并了两个分类的所有标签,代码更直观。比如判断一个集合是否是另一个集合的子集,用a.issubset(b)或者a <= b,在筛选数据时非常方便。

选择容器类型的核心原则我总结成一句话:需要顺序和下标,用列表;需要不可变保护和性能,用元组;需要键值对应快速查找,用字典;需要去重或集合运算,用集合。四种容器各司其职,脱离场景谈优劣没有意义。

3. 控制流:条件判断与循环的节奏感

3.1 if/elif/else:条件判断的优先级博弈

条件判断是让程序"活"起来的第一步。Python的语法里没有switch语句,多重判断用if/elif/else,但写法上有一个非常关键的细节——条件的顺序。

我在一道练习里犯过错:判断学生成绩等级时,我把所有条件都写在if里了,排得又乱,结果分数高的反而输出低等级。后来改成从高到低排列才正常:

score = 85 if score >= 90: grade = "A" elif score >= 80: grade = "B" elif score >= 70: grade = "C" elif score >= 60: grade = "D" else: grade = "F"

为什么必须从高往低写?因为Python按顺序执行条件,一旦命中某个elif就跳过后面的所有判断。如果先写score >= 60,那85分直接命中"D",永远到不了更高等级。条件的顺序本身就是逻辑的一部分,排列不当是新手最常见的逻辑bug来源。

这个细节在查资料时又发现了条件表达式的另一个常见坑:if score > 60 and score <= 80:这种写法自然没问题,但初学者容易写出60 < score <= 80,其实Python支持链式比较,简化了一些,不必写and。

三元表达式x if cond else y也值得掌握,但不要嵌套太深,否则可读性很差,宁可拆成普通if。

3.2 for循环与range:遍历的两种思维

for循环是Python里使用率最高的控制流结构。它的核心逻辑是"遍历一个可迭代对象",跟C语言里那种"计数器从0数到N"非常不一样。

最常见的是配合range():

for i in range(1, 11): print(i) # 输出1到10 for i in range(0, 10, 2): print(i) # 0, 2, 4, 6, 8,步长为2 for i in range(10, 0, -1): print(i) # 10到1倒序

range(start, stop, step)的三个参数分别是起始值(含)、结束值(不含)、步长。**"stop不包含"**这一点必须刻在脑子里,我第一次想打印1到10时写的是range(1, 10),结果少了个10,排查了半天还以为是代码有bug。

除了遍历数字,更常见的是直接遍历列表或字符串:

fruits = ["苹果", "香蕉", "橙子"] for fruit in fruits: print(fruit)

需要下标时,优先用enumerate(),它同时返回位置下标和元素值:

for index, fruit in enumerate(fruits): print(f"第{index + 1}个是{fruit}")

实际做练习题时,我发现一个好用的技巧:列表推导式(list comprehension)配合for循环,能把原本5行的代码压缩成1行:

squares = [x ** 2 for x in range(1, 11)] # 等价于: # squares = [] # for x in range(1, 11): # squares.append(x ** 2)

推导式是Python的标志性语法,刚学可能不习惯,但一旦用顺手,写起来特别舒服,性能还比循环加append快。后面学了好几个用推导式解决的问题以后,越来越体会到这个特性的设计哲学:表达意图,而非描述步骤。

3.3 while循环与break/continue:什么时候该用循环

while循环和for循环的区别一句话就能说清:for适合"知道要循环多少次"的场景,while适合"不确定多少次、靠条件结束"的场景。

count = 0 while count < 5: print(count) count += 1

写while时最怕的就是死循环——条件永远为真,程序跑不停。我第一天写while就遇到过一次:循环体里忘了写count += 1,条件永远为真,直接在终端里刷屏,只能按Ctrl+C强制中断。打那以后我每次写while都会先检查循环体内有没有能让条件变假的语句。

break和continue是循环的两个控制阀门:

for i in range(1, 20): if i == 10: break # 直接结束整个循环 if i % 2 == 0: continue # 跳过本次循环的剩余部分,进入下一次 print(i)

实际场景里,break特别适合做"查找第一个满足条件的元素",找到了就停下,避免多余的遍历。continue则用来跳过不需要处理的数据项。

while经常和break搭配实现一个很实用的模式:用户输入循环。比如写一个猜数字小游戏:

secret = 7 while True: guess = int(input("猜一个数字: ")) if guess == secret: print("猜对了!") break elif guess < secret: print("小了,继续猜") else: print("大了,继续猜")

while True配合内部的break,等于一个"什么时候退出由流程决定"的循环,比用变量控制条件更直观。但要注意这种写法必须保证循环内一定有break路径,否则就是个死循环。

4. 函数:让代码从"写一遍"变成"用很多遍"

4.1 定义函数与返回值

函数的作用一句话说就是"把代码组织起来复用"。第一次自己写了一个小函数并连续调用三次时,切实感受到"写一次,用多次"的价值,编程思维算是第一次建立起来。

函数的基本结构很清爽:

def add(a, b): """返回两个数的和""" return a + b result = add(3, 5) print(result) # 8

def是关键字,add是函数名,括号里是参数,return返回结果。文档字符串(第一行的"""...""")用来写函数的说明,我在练习时一开始总跳过不写,后来发现写不写区别其实很大——隔几天再看旧代码,瞬间明白每个函数干什么的感觉特别好。

关于return,有个微妙但重要的点:函数没有代码直接return一个值时,它会自动返回None。比如:

def greet(name): print(f"你好,{name}") result = greet("张三") print(result) # None

greet函数只是打印,没有返回值,所以result是None。这种"隐式None"行为容易让人困惑,尤其是你期望函数返回点什么东西的时候。判断一个函数是"做事情"还是"算结果",对理解整个程序的运行逻辑非常有帮助。

4.2 参数传递的灵活玩法

Python函数的参数系统非常灵活,这也是它让新手少犯难的设计之一。主要有四种:

def show(a, b=10, *args, **kwargs): print(a, b) print(args) # 元组,存多余的位置参数 print(kwargs) # 字典,存多余的关键字参数
  • 位置参数:show(1, 2),a=1, b=2
  • 默认参数:show(1),a=1, b=10
  • 可变位置参数:show(1, 2, 3, 4),args=(3, 4)
  • 可变关键字参数:show(1, 2, name="张三", age=18),kwargs={"name": "张三", "age": 18}

*args和**kwargs看起来高深,实际用途非常实在。比如,你写一个函数想接收任意数量的数字求和,用*args就行:

def total(*nums): s = 0 for n in nums: s += n return s print(total(1, 2, 3, 4, 5)) # 15

**kwargs则很适合处理字典形式的配置项。比如一个函数需要接收不确定的配置字段时:

def create_something(**config): print(f"创建对象,配置项有{len(config)}个") create_something(name="测试", size=100, color="red")

默认值参数有个经典的坑是"可变默认值陷阱":

def add_item(item, lst=[]): lst.append(item) return lst print(add_item("a")) # ['a'] print(add_item("b")) # ['a', 'b'],第二次调用没用新的空列表!

原因是默认列表在函数定义时只创建一次,每次调用如果没传lst,用的都是同一个列表对象。解决方法是默认值设为None,内部再新建列表:

def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst

这个坑我踩得印象深刻。调了两次函数,发现第二次返回的列表里居然带着第一次的元素,还以为自己哪里写错了。查了半天才明白是默认参数共享导致的。记住一条:默认参数千万不要用可变对象,一律用None再加判断。

4.3 作用域:函数内部与外部之间

作用域指的是"变量在哪些地方可以被访问"。Python的规则用LEGB原则概括:

  • L(Local):函数内部定义的变量
  • E(Enclosing):嵌套函数中外部函数的变量
  • G(Global):模块级别的全局变量
  • B(Built-in):Python内置的名字,比如len、print

举一个直观的例子:

x = 10 # 全局变量 def func(): x = 5 # 局部变量,跟全局的x互不相干 print("函数内部:", x) func() print("函数外部:", x) # 仍然是10

函数内部的x = 5是重新创建了一个局部变量,不会改变全局的x。这就是我在第一周遇到的一个很意外的现象——在函数里改了一个变量,出了函数一看根本没变。

如果想在函数内部修改全局变量,需要声明global,但除非特殊情况,我基本不用这个关键字。函数设计得越独立,越容易测试和理解。如果你发现函数里要频繁操作全局变量,更合适的做法是把要改的值作为参数传进去,把结果通过return传出来:

def increment(num): return num + 1 count = 0 count = increment(count) # 推荐这种写法

这个模式看起来多写了一行代码,但函数没有对外部环境做任何修改,行为完全可预测,这就是函数式思维的一个雏形。同时给可读性带来的好处是:一个函数的输入输出看起来清清楚楚,测试起来也容易。

嵌套函数那种nonlocal关键字的用法我先放一边了,目前练习里用不上,知道有这个概念就行。等以后学闭包和装饰器时再深挖不迟。

5. 学习过程中的踩坑记录与实用心得

5.1 我踩过的几个具体坑

这周踩坑不少,挑几个印象深刻的记录下来:

第一个坑是字典键不存在。那时候写了个统计字符出现次数的程序:

text = "hello" counts = {} for ch in text: counts[ch] += 1 # KeyError: 'h'

直接报错,因为第一次遇到键h时,字典里根本没有这个键。正确写法是:

for ch in text: counts[ch] = counts.get(ch, 0) + 1

或者用defaultdict,但当时还没学collections模块,用get()已经能解决。

第二个坑是忘记转换数据类型。我写了个加法计算器,输入两个数字后一运行,输出的居然是两个字符串拼在一起。原因很简单:input()返回的是字符串,直接+就是字符串拼接。需要转成int再相加:

a = int(input("第一个数: ")) b = int(input("第二个数: ")) print(a + b)

这个错误让我学到一个习惯:每次接收input()的结果,先问自己"它现在是字符串,我需要它是什么类型"。

第三个坑是函数内部变量访问冲突。我写了一个函数,想在函数内部读全局变量的值,又想改全局变量的值,结果一会儿报错一会儿不报。后来才彻底理解,只要函数内出现了x = ...这样的赋值语句,Python就把x当成局部变量,前面的print(x)也会报"局部变量x在赋值前被引用"。如果确实要改全局变量,就得用global x声明。

5.2 第2周小结:学到什么程度算"学会"

学完数据类型和控制流之后,我对"学会"有了新的定义:不是看懂了书上的例子,而是能独立写出完整的小程序。所以我给自己设定了一道综合题,大概长这样:

# 简易成绩统计系统 scores = [] while True: user_input = input("输入成绩(输入q退出): ") if user_input == "q": break try: score = float(user_input) scores.append(score) except ValueError: print("输入无效,请重新输入") if scores: average = sum(scores) / len(scores) print(f"共{len(scores)}条成绩,平均分{average:.2f}分") above = [s for s in scores if s >= 60] print(f"及格人数:{len(above)}人,及格率:{len(above) / len(scores):.1%}") else: print("没有有效成绩")

这段代码几乎用尽了本周学的内容:while循环、break、try/except、input、float转换、if判断、列表、append、列表推导式、f-string、百分比格式化。如果能不看资料写出来并运行成功,我觉得这周就算过关了。

还有一点想说的是,这个阶段看文档和查资料一定会很频繁,这完全正常。我自己遇到忘记某个方法时就直接打开交互式解释器测试,help()和dir()这两个内置函数我一天能用到好几次。尤其是dir(),输入dir(list)就能列出列表的所有方法名,加上help(list.append)看具体说明,相当于随身带了一个API手册。

逐步思考,逐步验证,不懂就试,多写多跑,学习过程的思路应该就是这样。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进