Python基础笔记-从语法到能写脚本
Python 的笔记之前是按知识点一篇篇记的:类型一篇、容器一篇、排序一篇。真写脚本时才发现缺的是“从需求到代码”那条路,而不是又一个知识点。这篇按写脚本的实际顺序重排:先把数据表达清楚,再让代码动起来,把重复逻辑收进函数,用内置函数和推导式少写循环,最后落到文件、编码和排错。pytest、接口、UI 那些属于自动化测试,放在另一篇。
一、先把数据表达清楚
四种基本类型
| 类型 | 类名 | 要注意的 |
|---|---|---|
| 整型 | int |
不限长度,不用担心溢出 |
| 浮点型 | float |
有精度问题,别直接比相等 |
| 布尔型 | bool |
空值、0、空容器都算 False |
| 字符串 | str |
不可变,单双引号、三引号都行 |
浮点数的坑值得单独记一笔:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
做金额、比例断言时,用 round() 保留位数再比,或者转成 Decimal。
字符串常用操作基本都在方法上:strip、split、join、replace、startswith、find、upper、lower。拼接优先用 f-string:
name = "login"
f"case_{name}_01" # case_login_01
类型转换记住两件事:int("12") 可以,int("12a") 会抛 ValueError;str(3.14) 得到的是 "3.14",但 str(["a", 1]) 得到的是 "['a', 1]",看着像列表,其实已经是字符串了。
一个贯穿全文的例子
后面几节都拿这批测试结果举例,省得每节换一个玩具数据:
results = [
{"name": "登录-正常", "status": "pass", "duration": 0.32},
{"name": "登录-密码错", "status": "fail", "duration": 0.41},
{"name": "下单-库存不足", "status": "skip", "duration": 0.05},
]
二、判断和循环:让代码动起来
分支就 if / elif / else 三种写法:单分支、双分支、多分支。简单赋值可以用三元表达式压成一行:
label = "通过" if r["status"] == "pass" else "未通过"
循环里最常用的是 for,配合 range 控制次数、配合容器做遍历:
for r in results:
if r["status"] == "fail":
print("失败:", r["name"])
while 适合“次数不确定、靠条件退出”的场景,比如重试:
retry = 0
while retry < 3:
if do_request():
break
retry += 1
continue 跳过本次、break 跳出整个循环。写 while True 时一定要在循环体里留出口,否则就是死循环。
遍历字典时默认拿的是 key,要拿值就 d[key] 或者 d.items():
for key, value in config.items():
print(key, value)
三、容器:一批数据怎么放
四种容器怎么选
| 容器 | 写法 | 特点 | 什么时候用 |
|---|---|---|---|
| 列表 list | [1, 2, 3] |
有序、可变、可重复 | 一批用例、一批结果 |
| 元组 tuple | (1, 2) |
有序、不可变 | 固定结构,如坐标、配置项 |
| 字典 dict | {"a": 1} |
键值对、键唯一 | 一条记录、一组参数 |
| 集合 set | {1, 2} |
无序、元素唯一 | 去重、求交集 |
拿上面的 results 举例,常见的几个动作:
results[0]["name"] # 取第一条的名字
len(results) # 一共几条
set(r["status"] for r in results) # 出现过哪些状态 {'pass', 'fail', 'skip'}
列表的方法要分清“改自己”和“返回新的”:append / extend / insert / pop / remove / sort 都是原地改,sorted / 切片 返回新对象。
remove 只删第一个匹配项,删不存在的值会抛 ValueError;pop 按下标删并返回,越界抛 IndexError。
切片
列表和字符串都能切,[起:止:步长],止不含:
names = ["a", "b", "c", "d", "e"]
names[1:-1] # ['b', 'c', 'd']
names[::2] # ['a', 'c', 'e']
names[::-1] # ['e', 'd', 'c', 'b', 'a']
互相转换
| 从 → 到 | 写法 | 备注 |
|---|---|---|
| 列表 → 字符串 | ",".join(str(i) for i in lst) |
元素必须是字符串,先 str() |
| 字符串 → 列表 | s.split(",") 或 list(s) |
按分隔符切,或按字符拆 |
| 两个列表 → 字典 | dict(zip(keys, values)) |
长度取短的 |
| 嵌套列表 → 字典 | dict([["a", 1], ["b", 2]]) |
每项是二元结构 |
| 字典 → 列表 | list(d.items()) |
要 key 或 value 就换 keys() / values() |
| 字符串 → 字典 | json.loads(s) |
仅限 JSON 格式 |
| 元组 ↔ 列表 | list(t) / tuple(lst) |
常规互转 |
eval() 也能把字符串变回 Python 对象,但它会执行任意代码,解析外部数据一律用 json.loads,别图省事。
四、函数与类:把重复收起来
函数
def 定义、return 返回,没有返回值时返回 None。即使没有参数,调用时的括号也不能省。
def pass_rate(results):
if not results:
return 0
passed = len([r for r in results if r["status"] == "pass"])
return passed / len(results)
两个常见的坑:
def add(item, bucket=[]): # 错:默认值只在定义时创建一次
bucket.append(item)
return bucket
可变对象不能当默认参数,改成 bucket=None 再在函数里初始化。另外函数里改列表、字典这类可变对象,会影响到调用方,除非先拷贝一份。
类
类适合“有状态 + 多个方法”的场景,比如把结果和统计逻辑包在一起:
class Report:
def __init__(self, results):
self.results = results
def passed(self):
return [r for r in self.results if r["status"] == "pass"]
def summary(self):
return f"{len(self.passed())}/{len(self.results)} 通过"
__init__ 在实例化时自动执行,self 指向实例本身。如果只是想复用一段逻辑,用函数就够了,不必为了“看起来面向对象”硬造类。
五、少写循环:内置函数与推导式
常用的十几个
| 函数 | 作用 | 例子 |
|---|---|---|
len |
长度 | len(results) |
sum / max / min |
求和、最大、最小 | sum(r["duration"] for r in results) |
any / all |
任一为真 / 全为真 | all(r["status"] != "running" for r in results) |
sorted |
排序,返回新列表 | sorted(results, key=...) |
zip |
并行打包 | list(zip(names, ages)) |
enumerate |
带下标遍历 | for i, r in enumerate(results, 1) |
map / filter |
映射 / 过滤 | list(filter(lambda r: r["status"] == "fail", results)) |
round / abs / divmod |
取整、绝对值、商余 | divmod(7, 2) |
type / isinstance |
类型判断 | isinstance(x, int) |
type() 不考虑继承关系,isinstance() 考虑,判断类型优先用后者。
推导式
“循环 + 往空列表里 append”的写法,九成可以换成推导式:
names = [r["name"] for r in results]
failed = [r["name"] for r in results if r["status"] == "fail"]
by_status = {r["status"]: r["name"] for r in results}
列表、字典、集合都有推导式;把方括号换成圆括号就是生成器表达式,数据量大时省内存。
格式化
rate = 0.6667
f"通过率 {rate:.1%}" # 通过率 66.7%
f"耗时 {1.2345:.2f}s" # 耗时 1.23s
f"{42:>6}" # 右对齐,宽 6
f"{42:06d}" # 左侧补零
print 的 sep 和 end 也常用:print(1, 2, sep=",") 输出 1,2,print(x, end="") 不换行。
六、排序:从手写算法到一行代码
五种排序
| 算法 | 思路 | 平均复杂度 | 稳定性 |
|---|---|---|---|
| 冒泡 | 相邻比较,大的往后换 | O(n²) | 稳定 |
| 选择 | 每轮找最小放前面 | O(n²) | 不稳定 |
| 插入 | 把元素插进已排好的部分 | O(n²) | 稳定 |
| 希尔 | 按增量分组做插入排序 | 约 O(n^1.3) | 不稳定 |
| 快速 | 选基准,小的放左、大的放右 | O(n log n) | 不稳定 |
手写快排的核心就是分区加递归:
def quick_sort(arr):
if len(arr) < 2:
return arr
pivot = arr[0]
less = [x for x in arr[1:] if x < pivot]
equal = [x for x in arr if x == pivot]
greater = [x for x in arr[1:] if x > pivot]
return quick_sort(less) + equal + quick_sort(greater)
实际项目里只用内置的
sorted() 返回新列表,list.sort() 原地排序、返回 None:
sorted(results, key=lambda r: r["duration"])
results.sort(key=lambda r: r["duration"], reverse=True)
要多个排序条件,key 返回元组即可。比如“失败的排前面,同状态按耗时从高到低”:
order = {"fail": 0, "skip": 1, "pass": 2}
sorted(results, key=lambda r: (order[r["status"]], -r["duration"]))
Python 的排序是稳定的,相同 key 保持原顺序,所以也可以先按次要字段排一次,再按主要字段排一次。只要前 10 条时别全排,用 heapq.nlargest(10, rows, key=...)。
七、文件、编码与路径
打开文件
with open("data.json", "r", encoding="utf-8") as f:
text = f.read()
| 模式 | 含义 |
|---|---|
r / w / a |
读 / 覆盖写 / 追加 |
x |
新建,已存在就报错 |
b |
二进制,常组合成 rb、wb |
+ |
可读可写,如 r+ |
with 保证异常时也会关闭文件。读大文件用 for line in f 逐行迭代,别 readlines() 一次全读进来。
编码
Windows 默认编码常是 GBK,读 UTF-8 文件会报:
UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 0
两种解法,优先第一种:
open("a.py", encoding="utf-8") # 显式指定
open("a.py", "rb") # 按字节读,再自己 decode
存一批结果
import json
with open("report.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
ensure_ascii=False 才能让中文原样写出去,否则会变成 \u767b\u5f55 这种。CSV 用 csv.DictWriter 比手拼逗号稳,能处理引号和换行。
路径
字符串拼路径在 Windows 和 macOS 上要处理斜杠,用 pathlib 就没这问题:
from pathlib import Path
for p in Path("data").glob("*.json"):
data = json.loads(p.read_text(encoding="utf-8"))
带 BOM 的文件用 encoding="utf-8-sig" 读,编码不确定时可以 chardet.detect(raw) 先猜一下。
八、出错之后:异常与调试
常见异常
| 异常 | 典型触发 |
|---|---|
IndexError |
列表下标越界,循环写成 <= len |
KeyError |
字典取不存在的键 |
TypeError |
类型不对,如 "1" + 1 |
ValueError |
类型对但值不对,如 int("12a") |
AttributeError |
对象没有这个属性,如 None.strip() |
ZeroDivisionError |
除数为 0 |
FileNotFoundError |
路径不存在 |
读 traceback
报错信息从上往下是“调用链”,最后一行才是真正的异常。看三样:最后一行异常类型和信息、倒数第二行出错的文件和行号、带 ^ 的那行代码。
捕获
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print("解析失败:", e)
else:
print("解析成功")
finally:
print("无论成败都执行")
不要写裸 except:,它会连 Ctrl+C 一起吞掉;至少 except Exception as e 并记下日志。自己抛异常用 raise ValueError("参数不能为空")。
调试
先用 print 打印中间值,简单直接;要看变量和单步执行,在代码里写 breakpoint(),运行到那里会进 pdb,p 变量 看值、n 下一行、c 继续。正式脚本里用 logging 代替 print,至少能分级别、能写文件。
九、练手:用爬虫把前面的东西串一遍
爬虫是个很好的练手项目,容器、循环、函数、文件、异常全用得上。最小闭环就三步:请求列表页、正则取链接、请求详情页存文件。
import re
import requests
html = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}).text
links = re.findall(r'href="(/w/[^"]+)"', html)
for link in links:
detail = requests.get(url + link).text
img = re.search(r'<img id="wallpaper" src="(.*?)"', detail).group(1)
print(img)
写的时候踩过两个坑:re.findall 返回的是列表,下一次匹配前要先 str();响应里的 \uXXXX 转义要先 json.loads,别拿字符串硬替换。
需要工程化抓取时上 scrapy:
scrapy startproject demo
scrapy genspider wall wallhaven.cc
scrapy shell "https://wallhaven.cc/latest"
scrapy crawl wall
scrapy shell 用来验证选择器特别好用。不管用什么工具,抓之前看 robots.txt、控制频率、别碰隐私数据,这条比技术本身重要。requests 的会话、超时、重试这些细节在自动化那篇里展开。
十、写得像工程一点
脚本能跑和能维护是两回事,几个成本很低的习惯:
python -m venv .venv
.venv\Scripts\activate # Windows
pip freeze > requirements.txt
- 每个项目一个虚拟环境,依赖写进
requirements.txt; - 数据用
dataclass或至少用字典统一字段名,别到处row[0]、row[1]; - 函数加类型注解,
def load(path: str) -> list[dict]:,配合mypy能挡掉一部分低级错误; - 用
ruff统一格式和 lint,省得在引号、空行上争论; - 日志用
logging分级,别等到线上排查时才发现只有 print。
十一、还可以往下挖
基础部分真正拉开差距的不是语法,而是这些:
- 生成器与迭代器:处理大文件、大结果集时避免内存爆掉;
- 装饰器:给函数统一加计时、重试、日志,pytest 的 fixture 本质上也是这套思路;
- 上下文管理器:自己写资源类时保证释放;
- 并发:
threading适合 IO 等待,asyncio适合高并发请求; - 数据处理:
pandas做结果统计,sqlalchemy直连数据库造数。

浙公网安备 33010602011771号