1. 先把"基本用法"这件事想清楚:你真正需要的不是语法清单
很多人来找我聊Python,第一句话往往是"我想学Python,但不知道从哪儿开始",第二句话往往是"基础语法我看过好几遍了,list、dict、if、for我都认识,但真让我写一个脚本就脑袋空白"。
这个问题我太熟悉了。因为大多数教程把"Python的基本用法"讲成了"Python的语法词典"——变量是什么、运算符有多少种、for循环怎么写、函数怎么定义,一条一条列出来,看得时候觉得都会,合上书还是不会用。
我自己的经验是:基本用法不是知识点,而是"高频动作的组合"。你日常写脚本、处理数据、写小工具,翻来覆去就用那二十几个操作。真正要掌握的是"拿到一个任务,怎么把它拆成变量、流程、函数、数据结构,最后用文件读写和异常处理把它包成一个能跑的东西"。
所以这篇东西不讲大全,也不讲偏门技巧,就讲一个有过实操经验的人,每天真正在用的那一套Python基本功。适合两类人看:一是刚学完语法但还没写过完整程序的人;二是能写能跑但总觉得自己代码"不够Python"的人。
我先说一个最简单也最容易被忽略的判断标准:一段代码是否算"会用Python",不是看它用了多少高级特性,而是看它能不能在10分钟内写完、5分钟内被另一个人看懂、以及出问题时能不能快速定位。后面讲的所有东西,都奔着这三件事去。
2. 环境和运行方式:先把地基夯实,后面少折腾
2.1 选对Python版本和管理工具
现在还会有新人直接去官网下载一个Python装完就开写。能用,但你早晚会踩"版本混乱"的坑。我的建议是走一套很常规但很省心的组合:
- 用Python 3.x的最新稳定版本(具体小版本不重要,但别用2.7,行业里早就没人维护了);
- 装一个虚拟环境工具,我常用的是Python自带的
venv,轻量、零负担; - 编辑器随便,VSCode配Python插件就够了。不要在这个阶段纠结"哪个编辑器最强",能跑代码、有语法高亮、能看调试信息,就完成任务了。
为什么不建议直接往系统Python里pip装包?因为你会同时做好几个小项目,A项目需要某个库的1.0版本,B项目需要同一个库的2.0版本,装在一起就是互相覆盖、互相拆台。虚拟环境相当于给每个项目开一个独立的"库仓库",互不干扰。这属于"基本用法"里最值得先建立的习惯。
# 建一个项目目录,进去之后创建虚拟环境 python -m venv venv # Windows激活 venv\Scripts\activate # macOS / Linux激活 source venv/bin/activate激活之后,命令行前面会出现(venv)字样,这时候你再pip install任何东西,都只会进当前环境。项目做完,直接删掉这个目录都行,不影响全局环境。
2.2 也要从第一行就理解"脚本"和"程序"的区别
用python xxx.py跑一个脚本和写一个可以被导入的模块,是两种不同的思维。我见过很多初学者把逻辑全写在一个文件里,从上到下跑完拉倒。在小练习里没问题,可一旦你要复用功能、写测试、让别的脚本调用,立刻就会撞墙。
所以从一开始就养成一个习惯:把每个文件当成一个可以独立运行、也可以被其他地方import的模块来写。下面这个写法算是个基线:
def main(): # 你要干的事都放在这里 print("Hello, Python") if __name__ == "__main__": main()if __name__ == "__main__"这行看着啰嗦,但它的意思是"只有当我是被直接运行时才执行main"。这个文件被别人import时,不会莫名其妙弹出一堆执行结果。这个习惯越早养成,后面写项目越顺。
3. 变量与对象的关系:搞懂这个,很多坑自然就消失了
3.1 Python变量其实是"标签",不是"盒子"
很多教程说变量像是装东西的盒子,这在Python里是个有害类比。Python的变量实质上是贴在对象上的标签,一个对象可以有好几个标签,同一个标签在任意时刻只贴在一个对象上。
a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4],你没看错这里b = a不是复制了一份,而是给同一个列表对象贴了个新标签。你通过b往列表里加东西,a这个标签指着的还是同一个列表,所以看到的结果自然也被改了。
这个特性在日常编程里太容易出问题。尤其是把列表、字典作为参数传给函数的时候,函数内部一改,外面的变量跟着变。不是所有场景都要防御这个,但你至少得知道"改的人是谁"。
如果你确实要复制一份独立的数据,用copy模块的deepcopy,或者对列表用切片a[:]。字典就用d.copy()。注意这里都是浅层复制,如果数据里还嵌套了列表、字典,就得用copy.deepcopy。
3.2 不可变类型和可变类型的边界
整数、浮点数、字符串、元组是不可变的;列表、字典、集合是可变的。不可变的意思是:你不能"原地修改"一个字符串或数字,你只能重新造一个新的。所以x = x + 1其实是在原来的值上操作完,生成了新对象,再把x这个标签贴过去。
这个认知直接影响你的编码习惯。字符串拼接用+在少数几次场景下没问题,但循环里拼几百上千次,效率会非常难看。因为每次拼接都在创建新字符串。工程上多用''.join(...)或者格式化字符串。这不是炫技,是在数据量上来之前就避开一个明显的性能坑。
常见的一个新手困惑是:元组既然不可变,为什么元组里还能放列表?答案是:元组不可变的是它存的那份"引用关系",而不是引用指向的那个对象本身。你没法给元组增删元素,但元组里的某个元素如果是列表,你还是可以给那个列表执行append。理解到这一层,很多"为什么报错/为什么不报错"的疑问都会迎刃而解。
4. 控制流和函数:把代码写成"读起来像说话"
4.1 条件与循环的高频写法
if、elif、else是个人都会写,我提三个容易被忽略的细节。
一是条件里的真值判断。Python里空列表、空字典、空字符串、0、None在布尔上下文里都是False。所以判断一个列表是否为空,别写if len(my_list) != 0:,直接写if my_list:就行。这不仅是少打几个字的问题,更是读代码的人一秒就能捕捉到意图。
二是循环里别手动改索引。很多从C语言转过来的人习惯写for i in range(len(some_list)):然后some_list[i],这在Python里通常是多余的。直接for item in some_list:就完了。如果你既需要元素又需要下标,用for i, item in enumerate(some_list):。如果要在两个列表里配对,用zip。
三是**while循环一定要有明确的退出条件**。我见过最多的死循环事故都是"忘了某条更新语句"。宁可在while块的末尾加个计数器自增,也别让循环靠break硬撑。
4.2 函数设计:小、短、单一职责
判断一个函数写得好不好,最简单的标准是:能不能只看函数名和参数,就知道它大概在干什么。一个300行的函数也许能跑,但它已经是"程序的地狱模式"了,调试时你根本无从下手。
我在实际工作中会把函数拆得很碎。比如某个处理任务,拆成"读原始数据""清洗数据""计算指标""生成报告"四个函数,主流程在这里:
def main(): data = load_data(path) cleaned = clean_data(data) result = calculate(cleaned) build_report(result)每个函数短小、单一,任何一步出错,只要看栈信息就知道是哪个环节挂了。调试成本成倍下降。
参数这块,要注意区分位置参数、关键字参数、默认参数。有一个惨痛教训我提一下:默认参数千万别用可变对象。比如def add_item(item, lst=[])这个写法,每次调用都会共用同一个列表,第二次调用时上次的数据还在。这是Python面试题里最经典的坑,也是实际代码里真会踩的雷。正确姿势是写def add_item(item, lst=None),函数内部在lst is None时新建一个列表。
4.3 关于lambda、生成器和"Pythonic"的一点提醒
Lambda表达式很酷,短短的,像是函数界的速写。但它不是万能的,只适合那种逻辑极简、直接作为参数传递的场合,比如key=lambda x: x[1]这样的排序键。一旦逻辑复杂,老老实实写def函数,可读性远胜于一行绕来绕去的lambda。
生成器表达式和列表推导式是Python的特色,大部分时候我推荐用。[x * x for x in range(10) if x % 2 == 0]这种写法,一行顶三行,而且非常直观。但注意:当推导式嵌套超过两层,或者里面带一堆条件时,它就会从"简洁"变成"晦涩"。判断标准还是那句话:如果这段逻辑将来自己读都要想一会儿,就别图这一行完成。
5. 高频数据结构实战:列表、字典、集合和元组怎么用才顺手
5.1 列表的常用操作,别靠背,靠组合
日常操作里,列表的增删改查、切片、排序、去重,每个都有很快捷的写法。
# 在头部加一个元素(不常用但要知道) lst.insert(0, "x") # 在尾部加多个元素 lst.extend(["a", "b", "c"]) # 切片取倒数几个 tail = lst[-3:] # 原地排序 lst.sort() # 排序但不改动原列表 new_lst = sorted(lst, reverse=True) # 列表去重,保持顺序(3.7+ dict保证插入顺序) uniq = list(dict.fromkeys(lst))特别注意sort和sorted的区别。前者原地修改列表返回None,后者返回新列表。很多人栽在"我排序完怎么得到个None"上,原因就是把.sort()的结果赋给变量了。
5.2 字典:Python里最常用的"万能结构"
字典在Python里的地位怎么强调都不过分。JSON、配置、参数传递、数据聚合,全都靠它。我建议每个人把字典的这几个姿势练熟:
- 用
d.get(key, default)而不是d[key]去取值,因为前者在键不存在时不会抛异常; - 用
d.setdefault(key, default)实现"有就用旧值,没有就塞默认值"; - 更新多个键值对时用
d.update(other_dict); - 遍历时用
for key, value in d.items()。
一个非常有用的场景是按某个字段聚合数据。比如有一堆学生信息,你想按班级分组,通常的写法是:
students_by_class = {} for student in students: cls = student["class_name"] students_by_class.setdefault(cls, []).append(student)这样循环下来,每个班级名对应一个学生列表。这个套路我几乎天天用。比你先判断if cls not in dict再初始化列表要干净得多。
5.3 元组和集合的功能定位
元组最常被我把作"一个不可变的、有名字的字段集合"。函数返回多个值时,本质上也是在返回一个元组。命名元组namedtuple和dataclass适合更正式的结构,但基础阶段注意"元组是不可变、轻量"这一点就够了。
集合的核心价值是去重和集合运算。去重前面说过了,集合运算则是set的隐藏技能:交集&、并集|、差集-、对称差集^。比如你要找"在读列表里但不在已读完列表里的书",直接set(reading) - set(finished),一行解决。
6. 文件读写与异常处理:让程序体面地处理意外
6.1 用with管理资源,别再写裸的open和close
文件操作是几乎所有脚本都绕不开的。老式写法是:
f = open("data.txt", "r", encoding="utf-8") content = f.read() f.close()现在统一用with open(...) as f:。好处是无论代码执行正常还是中途抛异常,文件都会被自动关闭。你少管一件事,就少一个出错点。
with open("data.txt", "r", encoding="utf-8") as f: lines = f.readlines()这里我再强调一次encoding="utf-8"。Windows下默认编码经常不是UTF-8,读写中文时只要有一边不一致,就会出乱码。凡是文本文件,我都建议显式指定编码,省得换台机器就翻车。
写入文件时同理:
with open("output.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2)ensure_ascii=False的意思是让JSON里的中文直接显示成中文,而不是一堆\u开头的转义序列。这个细节我是在某个项目交付时被对方问"为什么JSON里全是\u开头"才注意到的,后来一直保留这个习惯。
6.2 try/except不是用来"掩耳盗铃"的
新手很容易走向两个极端:要么完全不用try,让程序一碰到小异常就崩;要么大包大揽,写一个try: ... except Exception: pass,把错误全吞掉。
正确的做法是只捕获你能处理的异常,并且让它留下痕迹。比如读配置文件时,文件找不到是可能发生的,那就捕获FileNotFoundError,然后给个默认配置;但如果程序内部逻辑出错导致数据格式不对,这种问题应该让它抛出来,方便你定位。
import logging logging.basicConfig(level=logging.INFO) try: with open("config.json", "r", encoding="utf-8") as f: config = json.load(f) except FileNotFoundError: logging.warning("配置文件不存在,使用默认配置") config = {} except json.JSONDecodeError: logging.error("配置文件内容不是合法JSON,请检查格式") raise注意最后一行raise,它把异常继续往上抛,表示"我处理不了这个情况,程序应该停在这里让开发者看到"。用日志记录错误,而不是只吞掉或只打印,是程序"体面"的分水岭。
还有一个实用点:如果你要释放某个资源、或者在异常后做清理,用finally;如果你希望在try成功时才执行某段逻辑,用else,放在except之后。这俩一个兜底,一个锦上添花,但都比你硬把清理塞进try末尾要优雅。
7. 模块、导入和虚拟环境:从单文件迈向真实项目的关键一跃
7.1 import的机制和常见的坑
import的本质是执行一次整个模块文件。所以模块顶层的代码不应该有任何"副作用"——不要一import就打印东西、连数据库、跑主流程。这也是前面强调if __name__ == "__main__"的原因。
导入时,Python按这个顺序搜索模块:当前目录、PYTHONPATH、标准库、site-packages。如果你把自己的脚本文件命名为random.py、json.py之类,就会覆盖同名标准库,属于classic级的翻车现场。我见过有同学把文件命名为email.py,然后自己的程序里再import email时,成功导入了自己,直接报出一堆奇怪错误。
7.2 包结构的最小实践
当你的代码超过一个文件,就面临"怎么组织包"的问题。一个最简单的、长期好用的目录结构是这样:
project/ main.py utils/ __init__.py date_helper.py text_clean.py requirements.txt venv/__init__.py在Python 3里可以空着,但它是让目录变成"包"的标志。从main.py里导入时写from utils.text_clean import clean_text。这样,utils目录里的每个工具模块职责清楚,后期扩展、加测试、被人调用都有条有理。
依赖管理上,requirements.txt就是项目的地基清单。每当环境里装好一个新库,就执行一次pip freeze > requirements.txt,把你的依赖固定下来。别人拿到项目后,一条pip install -r requirements.txt就能复现环境。
7.3 不要迷信"一行流",但也不要拒绝标准库
很多人一上来就想学某个第三方库铅笔、某个爬虫库、某个数据分析框架。我不是反对化学业,我反对的是在基础不稳时直接上框架。当你能熟练用csv、json、re、datetime、pathlib、collections这些标准库解决八成日常需求时,再上框架,你的理解会完全不同。
比如路径拼接,还在用字符串+ "/" +吗?试试pathlib:
from pathlib import Path data_dir = Path("data") output_file = data_dir / "processed" / "result.csv" output_file.parent.mkdir(parents=True, exist_ok=True)用/运算符拼接路径,跨平台、可读性好、还能顺手建目录。这类标准库里的"宝藏操作",属于不系统看根本发现不了,但一旦用上就回不去的体验。
8. 一个我从"能跑"到"像话"的检查清单
最后给你一份我自己的代码自查清单。每次写完脚本,我按这个过一遍,质量基本不会差的:
- 文件内部有没有顶层副作用代码?是不是都收进
main()里了? - 读文件、连外部资源的地方,有没有用
with? - 异常捕获有没有至少做打日志,而不是
pass一句带过? - 列表、字典作为默认参数了吗?函数里修改了入参吗?
- 变量名和函数名是不是一眼能懂?是不是用了
data、x这种名字糊弄过去? - 有没有重复三次以上的代码块?有就抽成函数。
- 路径拼接是不是走了
pathlib? - 类、函数、模块之间的职责是不是拆开了?
有一次我拿这个清单去复盘一个自己写过的小工具,改完的版本比原版短了四分之一,但可读性提升了一倍。所以基础用法这件事,最后拼的不是知道了多少,而是你愿不愿意把"好读""好改""能跑很久"当成标准。
Python最可爱的地方也在这里——它不要求你一开始就写出完美的代码,但只要你愿意往前进一步,它提供的工具足够你走得很远。把这套基础动作练成肌肉记忆,后面不管是顺手写个自动化脚本,还是转去做数据分析、后端开发,你都不会觉得是在从零起步。