
不少初学者跑来问我Python入门到底学什么才能写出真正能用的程序我的答案几乎每次都一样先把函数、列表、元组这三块吃透。很多人在学习过程中会特别迷恋高级特性可实际上 Python 日常开发里用得最多的就是这三种语法它们决定了你能不能把代码组织得清楚能不能处理好一批数据以及在写函数接口的时候能不能站稳脚跟。这篇文章就用我实际踩过的坑、实际写项目时的体会把这三块核心用法逐一说透适合那些已经看完基础语法、却还不太会组织代码的初学者。先说一个我观察到的现象很多人在网上刷了一堆 Python 语法今天学了个 lambda明天学了个装饰器但回过头来让他写一个把文件里每行内容转成元组并按第二列排序的需求他愣是不知道怎么组装这些语法。原因就在于函数、列表、元组这三样东西是 Python 的积木它们单独看不难难的是怎么把它们拼起来解决实际问题。这篇文章不聊虚的全部围绕真实使用场景展开。1. 函数把重复代码关进盒子里1.1 def 声明背后干了什么很多教材讲函数只会说封装代码提高复用这两个词太抽象了。我换个说法函数就是一个有输入、有输出的加工车间。你把原材料参数扔进去车间里的流水线函数体处理完把成品返回值交给你。def greet(name): 向指定用户打招呼 return fHello, {name}! print(greet(小明))这段代码里greet是函数名name是形参小明是实参return fHello, {name}!是输出。我特别想强调一下那个文档字符串就是引号里那句话这玩意儿看起来不起眼但等你项目写大了别人要调用你的函数时help(greet)就能看到这段说明。我见过太多人写函数不加 docstring过两周自己都忘了这个函数是干嘛的。还有一点新手经常搞不明白为什么 Python 里函数要用缩进来表示代码块因为 Python 的设计哲学是可读性优先。你用{}或者end来结尾写起来确实快但读起来很容易迷失在嵌套结构里。Python 用统一的四空格缩进强行逼你把代码排得整整齐齐这点刚开始觉得烦长期写下来反而觉得舒服。1.2 参数传递形参、实参与默认值函数参数这块我见过最混乱的代码就是参数设计完全凭感觉函数里用了十来个参数调用的时候根本记不住顺序。其实参数传递就四种情况一次理清楚位置参数——最直观调用时按定义顺序传值def person_info(name, age, city): print(f{name} 今年 {age} 岁住在 {city}) person_info(张三, 25, 上海)关键字参数——调用时指定参数名不用记顺序person_info(city上海, name张三, age25)关键字参数写起来啰嗦但在参数多、可读性要求高的场景下非常香。默认参数——给某些参数一个兜底值def connect(host, port, timeout30): # 不传 timeout 时默认等 30 秒 pass这里有个全网都在讲的坑默认参数不要用可变对象比如def add_item(item, lst[])这种写法函数被多次调用时列表会一直累积数据。后面第五章我会详细说这个坑的排查过程这里先记住结论凡是默认参数要使用列表、字典这类对象一律用None来占位def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst可变参数——*args和**kwargs这俩是让函数能多收几个参数用的def sum_all(*args): return sum(args) def print_kwargs(**kwargs): for k, v in kwargs.items(): print(f{k}: {v})*args会把传进来的位置参数打包成元组**kwargs会把关键字参数打包成字典。记住这一点很关键后面第四章讲函数和元组配合时还会用到。1.3 作用域与命名空间为什么函数里改不了外部变量新手第一次写这样的代码大概率会懵count 0 def increment(): count 1 # 报错UnboundLocalError increment()报错原因不是 Python 不允许函数读取全局变量而是count 1这个写法让 Python 认为count是这个函数内部的局部变量——它检测到你在函数内给count赋值了于是认定这是一个新变量而新变量在赋值之前就被拿来用了所以报错。这里就牵扯到 Python 的LEGB 规则变量查找顺序是 Local局部→ Enclosing外层→ Global全局→ Built-in内置。如果函数内部没有给某个变量赋值它会自动往上一层找一旦赋值了就认为它是局部变量。那确实想在函数里改全局变量怎么办用global声明count 0 def increment(): global count count 1但我的实操建议是尽量少用 global。函数之间通过参数和返回值传递数据逻辑才清晰。全局变量改来改去一旦项目复杂起来排查 bug 会非常痛苦。1.4 lambda、内置函数与回调的实用姿势lambda 是 Python 里创建匿名函数的写法语法很简单# 普通函数写法 def square(x): return x * x # lambda 写法 square lambda x: x * x注意lambda 适合写那种一句话就能搞定的简单逻辑比如作为sorted的排序规则data [(apple, 5), (banana, 2), (cherry, 8)] data.sort(keylambda item: item[1]) print(data) # 按第二列升序排序这段代码在真实项目中太常见了——把一组元组数据按某个字段排序。lambda 的作用就是告诉排序函数按元组里哪个位置的值来排。内置函数这块map、filter这两个在 Python 3 里返回的是迭代器不是列表很多新手在这里踩坑nums [1, 2, 3, 4, 5] result map(lambda x: x * 2, nums) print(result) # 输出的是一个 map 对象不是 [2, 4, 6, 8] # 要看到结果需要转一下 print(list(result)) # [2, 4, 6, 8]至于回调函数这个概念其实不神秘。回调和普通函数调用唯一的区别就是它不是一个动作的起点而是某个事件发生后被调用的后手。比如列表排序时传入的keylambda item: item[1]就是一种回调——排序算法会在需要比较两个元素时回头调用这个函数来获取比较规则。理解了这一点后面学 Tkinter 按钮的commandmy_func、Flask 路由的app.route()装饰器都会顺畅很多。2. 列表Python 中最灵活的数据容器2.1 索引和切片Python 处理数据的神技列表的索引从 0 开始负数表示从尾部倒着数这个基础到不能再基础。真正厉害的是切片一个方括号就能搞定很多操作。my_list [10, 20, 30, 40, 50, 60] # 取前三个 print(my_list[:3]) # [10, 20, 30] # 取后三个 print(my_list[-3:]) # [40, 50, 60] # 从索引 1 到 4步长为 2 print(my_list[1:5:2]) # [20, 40] # 经典反转技巧 print(my_list[::-1]) # [60, 50, 40, 30, 20, 10]切片语法是[start:stop:step]其中start和stop可以省略省略时默认从头到尾。我当初学切片时有个理解误区以为[:3]是取前 3 个后来才想明白它其实是取索引 0 到 2 的元素3 是结束位置不包含 3 本身。Python 这种左闭右开的设计初看别扭但用久了你会觉得非常合理——因为它天然支持len(mylist) 1的边界而不用单独处理减一。切片有个关键特性切片会生成一个新列表。这就意味着a [1, 2, 3] b a[:] # b 是 a 的副本修改 b 不影响 a这是复制列表最常用、最轻量的方式之一。2.2 增删改查列表方法的完整用法列表的方法很多但真正高频的就那么几个我把它们按增删改查分类整理一下增append(x)在末尾添加一个元素。注意是一个元素如果你传一个列表它会把整个列表当成一个元素塞进去。extend(iterable)把可迭代对象的元素拆开逐个追加到末尾。insert(index, x)在指定位置插入元素插入后原位置及后面的元素会依次后移。cart [apple] cart.append(banana) # [apple, banana] cart.extend([orange, grape]) # [apple, banana, orange, grape] cart.insert(1, watermelon) # [apple, watermelon, banana, orange, grape]append和extend的区别我每带一个新人就要强调一次两者看起来差不多但用错会导致列表嵌套结构混乱。比如有个需求是把一批学生姓名加入名单如果学生名单本身是一个列表用append就会得到[a, b, [c, d]]这种脏数据。删remove(x)删除第一个匹配到的元素如果元素不存在会抛ValueError。pop(index-1)删除并返回指定位置的元素默认删除末尾。这个返回值特性在实现栈结构时很有用。clear()清空整个列表。改直接通过索引赋值就可以mylist[2] new_value。查index(x)返回第一个匹配元素的索引。count(x)统计元素出现次数。in操作符判断元素是否存在这个用得最多时间复杂度是 O(n)。排序有两种方式mylist.sort()是原地排序直接修改原列表sorted(mylist)是返回一个新列表原列表保持不变。看场景选如果原列表之后还要用那就选sorted。nums [3, 1, 4, 1, 5, 9, 2, 6] sorted_nums sorted(nums) # nums 还是原样 nums.sort() # nums 直接变化把列表当栈用是 Python 开发里的常见操作append压栈pop出栈。但注意 Python 的list.pop()默认弹出最后一个元素栈是后进先出刚好匹配。2.3 列表推导式一行解决循环列表推导式是 Python 里我最喜欢的语法没有之一。它把循环 条件 生成新列表压缩成一行# 传统写法 squares [] for i in range(10): squares.append(i * i) # 列表推导式 squares [i * i for i in range(10)]带条件的推导式更爽# 取 1 到 100 中能被 2 和 3 同时整除的数 result [n for n in range(1, 101) if n % 2 0 and n % 3 0]我见过不少初学者一开始不愿意用推导式觉得循环写得明明白白为什么要压缩。但实际操作中当你需要从一个列表生成另一个列表、从一堆数据里过滤出子集时推导式不仅能少打字可读性其实更高——一行代码就是一个完整的转换规则。比如从用户列表中提取所有年龄大于 18 的姓名names [user[name] for user in users if user[age] 18]这句代码读起来几乎和自然语言一样顺畅。但要注意别写三层以上的嵌套推导式不然谁也看不懂那就违背了可读性优先的原则。2.4 列表复制的三重坑逐个拆给你看这是一个我教过的几乎所有学生都踩过的坑值得专门拿出来讲。第一重b a不是在复制a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4] a 也被改了原因很简单b a只是让b也指向了a所在的地址两个变量是同一个列表的两个名字。修改任意一个另一个跟着变。如果只想复制内容用b a[:]或b a.copy()。第二重a[:]和copy()是浅拷贝什么意思如果列表里存的是可变对象比如嵌套列表a[:]复制出来的新列表里的嵌套列表还是同一个引用a [[1, 2], [3, 4]] b a[:] b[0][0] 99 print(a) # [[99, 2], [3, 4]] a 又被改了b[0]和a[0]指向同一个列表。这时候要用copy.deepcopy(a)做深拷贝把每个层级都复制一份。第三重对象里的列表属性同理class Student: def __init__(self, scores): self.scores scores s1 Student([90, 95]) s2 s1 # 这俩是同一个学生解决方法和上面完全一样要么自己手动复制要么用copy模块。明白了这三重坑你才算真正理解了 Python 里引用和拷贝的区别。3. 元组不可变的价值3.1 为什么有了列表还要有元组很多初学者第一次接触元组都会问列表这么强大要元组干嘛这个问题问得非常好。元组和列表的核心区别就一条元组是不可变的创建之后不能再修改。而恰恰是不可变这个特性赋予了元组三条独特价值第一保证数据安全。比如一个函数返回了坐标点信息(x, y)如果用列表返回调用方可能不小心修改它导致后面的逻辑出错。用元组想改都改不了从语法层面杜绝误操作。第二可以被哈希。Python 里列表这种可变类型不能作为字典的键因为键必须是可哈希的但元组可以# 用元组做字典键存坐标到名字的映射 locations { (31.2304, 121.4737): 上海, (39.9042, 116.4074): 北京, }第三性能更好。元组没有增删改查的方法结构更简单创建和访问的速度会比列表快那么一点点。数据量大的时候这个差距能感受到。用一句话总结凡是创建之后不应该被修改的数据优先用元组。这在写代码时是一种自我约束也是一种给他人的暗示。3.2 元组解包把一个元组拆成多个变量元组解包是 Python 非常优雅的一个特性。原先你要取元组里的每个元素需要挨个用索引取point (3, 5) x point[0] y point[1]有了解包一行搞定x, y point解包还能用在循环和多个返回值上下面这个例子你大概率会在项目里用到# 遍历一个包含元组的列表 students [(张三, 25, 上海), (李四, 22, 北京)] for name, age, city in students: print(f{name} 住在 {city}今年 {age} 岁)这个语法让遍历并拆解数据结构变得极其自然。更高级一点的玩法是用*来实现不确定数量的解包first, *middle, last [1, 2, 3, 4, 5] print(first) # 1 print(middle) # [2, 3, 4] print(last) # 5Python 3 还支持在函数调用时用*将元组拆开作为位置参数def add(x, y): return x y points (3, 5) print(add(*points)) # 8这个技巧在你调用一些库函数需要把接收到的元组参数展开传进去时非常有用。3.3 元组与列表的选择什么时候用哪个我整理了实战中判断用列表还是元组的方法一个表格就能说清楚对比维度列表元组可变性可变不可变常用方法append、extend、remove、pop 等几乎没有可哈希否是性能相对慢相对快典型场景动态收集数据、待排序数据、队列固定结构的数据、字典键、函数返回多值语义含义一组可以变化的数据一个固定的数据组合顺便说一句namedtuple是元组的一个有趣延伸它给元组元素起了名字访问时既可以用索引也可以用属性名from collections import namedtuple Point namedtuple(Point, [x, y]) p Point(3, 5) print(p.x, p.y) # 3 5如果元组使用频率很高又希望代码可读性强namedtuple可以了解一下。4. 函数联动列表与元组让数据流动起来4.1 函数返回多个值本质是返回元组这是初学者最高频的一个疑问Python 函数能不能返回多个值能但底层原理是返回一个元组。def get_min_max(nums): return min(nums), max(nums) result get_min_max([4, 1, 9, 3]) print(result) # (1, 9) print(type(result)) # class tuple # 用元组解包直接接收 min_val, max_val get_min_max([4, 1, 9, 3])这里就体现了 3.2 节元组解包和函数返回值的巧妙配合。你可以没有多个返回值这个特殊语法但只要返回元组 解包接收效果就和多返回值一样。在实际项目中这个模式最常见的使用场景是什么我举个例子从数据库里取出一条记录函数返回(id, name, created_at)调用方直接user_id, name, created_at fetch_user(123)每个变量名一看就知道是什么比用result[0]、result[1]这种索引访问清晰得多。4.2 列表当函数参数引用传递的副作用函数参数传列表时和 2.4 节说的引用问题是同一回事。看代码def remove_all_zero(nums): # 这样写会直接修改原列表 for n in nums: if n 0: nums.remove(n)这个函数本来只是想处理并返回一个新列表结果把原列表也改了调用方如果没意识到排查 bug 时会相当痛苦。我的习惯是函数体内部如果没有特殊需求尽量不要修改传入的可变对象。有两种方案方案一复制后操作def process(nums): nums nums[:] # 先复制 # 再处理不会影响外部 return nums方案二函数内部只读返回新对象def get_even(nums): return [n for n in nums if n % 2 0]这一节其实是把函数和列表两个知识点做了交叉验证——你越熟悉 Python 的引用/拷贝模型越能避免写出改了别人的数据而不自知的函数。4.3 实战场景综合函数筛选数据并排序为了把函数、列表、元组串起来我分享一个实际需求给出一批学生成绩数据姓名班级分数要求取出分数及格的学生按分数降序排列并输出姓名和对应的班级。# 模拟原始数据列表包元组 students [ (张三, A班, 78), (李四, B班, 52), (王五, A班, 91), (赵六, B班, 64), (孙七, C班, 45), ] def filter_passed(students, threshold60): 筛选出分数大于等于 threshold 的学生并按分数降序排序 # 用列表推导式过滤保留元组结构 passed [s for s in students if s[2] threshold] # 按第三列分数降序排列 passed.sort(keylambda s: s[2], reverseTrue) return passed result filter_passed(students) for name, cls, score in result: # 元组解包循环 print(f{name}{cls}: {score})输出王五A班: 91 张三A班: 78 赵六B班: 64把这段代码拆开看三个核心语法刚好完美配合列表推导式负责过滤lambda 负责指定排序键元组解包负责循环读取。这就是我前面说的积木拼接——单独学每个语法时都感觉挺简单组合起来才能解决实际问题。把这段代码往这个框架里一放函数、列表、元组三个概念的衔接就非常自然了。5. 入门阶段最容易踩的坑5.1 环境配置为什么命令提示符不认pip这部分本来和语法关系不大但我见过太多 Python 入门者卡在第一步所以必须单独说。你在命令行里敲pip install requests时如果系统弹出类似这样的报错pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这说明系统根本不知道 pip 在哪里。命令行本身只是个发号施令的地方它不知道 pip 的完整路径自然找不到这个命令。解决办法是让操作系统记录下 Python 和 pip 的安装目录这就是配置环境变量PATH。最简单的处理方式有两种如果你安装 Python 时勾选了Add Python to PATH重新打开一个命令窗口一般就好了如果没勾选需要手动把 Python 的安装路径比如C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\Scripts加到系统的 PATH 环境变量里。这个坑可以扩展到任何命令行工具git、npm、claude 命令甚至你自己写的一个非常实用的小脚本凡是提示不是内部或外部命令或无法识别原因大多都是路径没配好。配一次以后所有命令行工具都受用。5.2 默认参数用可变对象的隐患一次赋值次次积累这个坑我前面提过这里完整展开一下。假设你要写一个函数往某个列表里添加一条日志def log_message(msg, log_list[]): log_list.append(msg) return log_list print(log_message(hello)) print(log_message(world)) print(log_message(again))你预期每次调用都返回一个[hello]、[world]之类的单元素列表实际输出是[hello] [hello, world] [hello, world, again]原因在于默认参数[]只在函数定义时创建一次之后每次调用都复用同一个列表。这属于 Python 面试中的经典问题但现实里很多写了好几年代码的人也偶尔会被它坑到。标准解法就是我之前说过的None占位法def log_message(msg, log_listNone): if log_list is None: log_list [] log_list.append(msg) return log_list这种写法的含义是如果调用者没有传日志列表就新建一个空的传了就用传进来的那个。非常直观。5.3 元组只有一个元素时别忘了逗号另一个很隐蔽的坑创建只有一个元素的元组时光用括号是不行的。a (5) print(type(a)) # class int b (5,) print(type(b)) # class tuple(5)在 Python 里只是一个数学表达式结果就是整数 5要让它是元组必须加一个逗号。这个细节在函数返回值、解包时都很容易出问题。比如你想返回一个只包含一个文件的元组(filename,)结果写成了(filename)返回的就不是元组而是一个字符串调用方如果用它来做解包或者传给需要元组的函数就会报TypeError或者得到完全意想不到的结果。5.4 变量命名别和内置函数或关键字撞车Python 里有一些内置函数和关键词比如list、str、dict、sum、type、input它们是可以被普通变量覆盖的。很多新手写着写着就写出这种代码list [1, 2, 3] # 用 list 做变量名把内置 list 函数覆盖了 str hello # str 也被覆盖了当时看着没什么问题但后面一旦代码里需要调用list(abc)或str(123)就会得到奇奇怪怪的错误因为list和str已经被变成了普通变量不再是内置函数。排查这种 bug 需要花很长时间因为报错信息往往只在很后面的代码才出现。我的建议是凡是 Python 里你能想到的类型名称或内置函数名都不要用作变量名。想描述一个列表就用my_list、items、records想描述字符串就用text、content、message。命名上多打几个字符排查错误时能省下几个小时。写在最后函数、列表、元组这三个语法点说它们是 Python 入门的三根柱子一点都不夸张。函数帮你把逻辑结构化列表让你能灵活处理一批数据元组帮你保护不该变的数据而三者组合起来基本就能覆盖日常脚本开发里八成以上的需求。我在实际项目里写的函数十有八九要么接收列表或元组作为参数要么用元组作为返回值。如果你刚学完 Python 基础我建议别急着钻那些花哨的高阶语法先把这三大块练熟。方法也简单找几个真实场景比如读取一个 CSV 文件并按条件过滤数据、批量处理字符串并统计结果逼自己用函数 列表 元组写出可读性好的代码。这样练上一周你对 Python 的理解会比刷一百个知识点都扎实。