问题与目标
环境稳定之后,我很快遇到第二个问题:会写几行赋值和 print(),不等于会解决需求。
真实程序拿到的数据并不只有一个字符串。它可能有用户问题、文档列表、模型参数、开关状态和暂时缺失的结果。程序还要根据条件选择分支,对一批数据重复处理,并把稳定的处理步骤封装起来。
这正是数据类型、流程控制和函数各自解决的问题:
数据类型:信息用什么形式表示
流程控制:代码接下来走哪条路
函数:一段逻辑如何被命名和复用
我一开始把它们当成三个独立章节,后来才发现,一段稍微完整的业务逻辑几乎总会同时用到三者。
核心理解
程序不是在“处理变量”,而是在处理有业务含义的数据。
用户问题适合用字符串表示,候选文档适合用列表保存,一篇文档的标题和正文适合放在字典里,是否启用某项能力适合用布尔值表示。类型选对后,后面的判断和处理会自然很多。
函数也不只是为了少写几行代码。一个命名清楚、输入输出明确的函数,相当于把需求中的一个动作固定下来:
原始文档 -> 筛选相关文档 -> 拼接上下文 -> 得到 Prompt
这已经是最小的数据处理流水线。
完成标准
这一篇结束后,应当能够:
- 根据业务含义选择字符串、数字、布尔值或
None。 - 使用列表、元组、集合和字典组织一组数据。
- 完成常用的查询、增加、修改、删除和遍历操作。
- 使用条件判断表达互斥规则。
- 使用
for和while完成重复处理。 - 正确使用
break、continue和range()。 - 定义具有明确参数和返回值的函数。
- 理解默认参数、可变参数和变量作用域的基础边界。
核心概念
常用基础类型
第一阶段最常遇到的是:
| 类型 | 示例 | 常见用途 |
|---|---|---|
str | "如何创建虚拟环境" | 文本、路径、Prompt |
int | 3 | 次数、索引、数量 |
float | 0.2 | 比例、分数、模型参数 |
bool | True | 功能开关、判断结果 |
None | None | 暂无结果、缺失值 |
input() 得到的永远是字符串。需要参与数值计算时,要明确转换:
top_k_text = "3"
top_k = int(top_k_text)
类型转换可能失败,因此来自用户、文件或接口的数据不能默认可靠。
数字类型常用操作包括:
document_count = 10
success_rate = 0.86
print(document_count + 2) # 12
print(document_count // 3) # 3
print(document_count % 3) # 1
print(round(success_rate, 1)) # 0.9
浮点数采用有限精度表示,不适合用 == 判断所有计算结果是否绝对相等:
import math
result = 0.1 + 0.2
print(result) # 0.30000000000000004
print(math.isclose(result, 0.3)) # True
布尔值只有 True 和 False。None 表示“当前没有值”,判断时优先使用 is:
answer = None
if answer is None:
print("暂时没有模型回答")
字符串的查询、切片和清洗
字符串是不可变序列。索引从 0 开始,负索引从末尾开始:
text = "Python AI"
print(text[0]) # P
print(text[-1]) # I
print(text[0:6]) # Python
print(text[:6]) # Python
print(text[7:]) # AI
切片遵循“包含开始、不包含结束”。对字符串执行方法会返回新字符串,不会原地修改原值:
raw_question = " Python 如何处理文本?\n"
question = raw_question.strip()
print(question.lower())
print(question.replace("Python", "Python 3"))
print(question.startswith("Python"))
print("文本" in question)
常见文本操作还有:
tags = "python,rag,agent".split(",")
tag_text = " | ".join(tags)
print(tags)
print(tag_text)
split() 把字符串拆成列表,join() 用指定分隔符连接一组字符串。这两个操作在文档清洗和 Prompt 拼接中非常常见。
常用容器类型
容器用于把多个值组织在一起:
list:有顺序、可修改,适合文档列表和对话消息。tuple:有顺序、通常表达不应修改的一组值。dict:用键找到值,适合一条结构化记录或配置。set:元素不重复,适合去重和集合判断。
例如一条文档记录可以写成:
document = {
"title": "虚拟环境",
"content": "虚拟环境用于隔离项目依赖。",
}
这里真正重要的不是记住花括号,而是知道 title 和 content 组成了一条有结构的文档数据。
列表:保存有顺序、会变化的数据
documents = ["python.md", "rag.md"]
documents.append("agent.md")
documents.insert(1, "linux.md")
documents[0] = "python-basics.md"
removed = documents.pop()
print(documents)
print(removed)
列表支持索引、切片、成员判断和长度统计:
print(documents[0])
print(documents[:2])
print("rag.md" in documents)
print(len(documents))
元组:表达一组相对固定的值
model_config = ("demo-model", 0.2, 512)
model_name, temperature, max_tokens = model_config
元组不能像列表一样修改元素。它适合表达坐标、固定配置组合或函数返回的多个结果。只有一个元素的元组必须保留逗号:
single_item = ("python",)
集合:去重和集合关系
source_tags = {"python", "rag", "python"}
required_tags = {"python", "agent"}
print(source_tags) # 重复项被去除
print(source_tags & required_tags) # 交集
print(source_tags | required_tags) # 并集
print(required_tags - source_tags) # 差集
集合不适合依赖位置索引。需要保持输入顺序时,要明确设计去重方式,不能简单把列表转换成集合后再转回来。
字典:通过键组织结构化数据
document = {
"title": "Python 基础",
"content": "变量用于给对象绑定名称。",
}
print(document["title"])
print(document.get("source", "unknown"))
document["source"] = "local"
document["title"] = "Python 变量"
removed_content = document.pop("content")
document["missing"] 在键不存在时会抛出 KeyError;get() 可以提供默认值。来自外部的数据适合先验证,内部已经保证结构的数据则可以直接访问,让真正的结构错误尽早暴露。
遍历字典时可以同时取得键和值:
for key, value in document.items():
print(f"{key}={value}")
可变与不可变影响函数行为
字符串、数字和元组通常是不可变对象;列表、字典和集合是可变对象。
original = ["python"]
alias = original
copy_items = original.copy()
alias.append("rag")
print(original) # ['python', 'rag']
print(copy_items) # ['python']
赋值只让另一个名字指向同一对象,不会自动复制。浅拷贝可以复制第一层容器,但内部的嵌套对象仍然可能共享:
import copy
original = [{"title": "Python", "tags": ["basic"]}]
shallow = original.copy()
deep = copy.deepcopy(original)
shallow[0]["tags"].append("syntax")
print(original[0]["tags"]) # ['basic', 'syntax']
print(deep[0]["tags"]) # ['basic']
深拷贝会递归复制嵌套对象,代价也更高。项目中应先问清“是否真的需要一份完全独立的数据”,不应把 deepcopy() 当成默认解法。
推导式、zip() 和排序
推导式用于从已有可迭代对象构造新容器。下面把非空字符串清洗后放进新列表:
raw_texts = [" Python ", "", " RAG "]
cleaned_texts = [text.strip() for text in raw_texts if text.strip()]
print(cleaned_texts) # ['Python', 'RAG']
阅读顺序可以拆成三部分:
要放入结果的值
for 当前元素 in 原始数据
if 可选过滤条件
集合推导式适合去重,字典推导式适合构造键值关系:
tags = ["Python", "python", "RAG"]
normalized_tags = {tag.lower() for tag in tags}
scores = [0.9, 0.7]
score_map = {index: score for index, score in enumerate(scores, start=1)}
推导式适合清楚、短小的转换。条件嵌套太多时,普通循环更容易读懂和调试。
zip() 可以并行遍历多组数据:
titles = ["Python", "RAG"]
contents = ["Python 基础", "检索增强生成"]
documents = [
{"title": title, "content": content}
for title, content in zip(titles, contents)
]
默认情况下,zip() 会在最短的输入耗尽时停止。两组数据长度必须一致时,可以在 Python 3.10 及以上使用 strict=True,让长度不一致直接报错:
pairs = list(zip(titles, contents, strict=True))
sorted() 返回新列表,列表的 sort() 则原地排序:
documents = [
{"title": "Python", "score": 0.8},
{"title": "RAG", "score": 0.95},
]
ranked = sorted(
documents,
key=lambda item: item["score"],
reverse=True,
)
lambda item: item["score"] 是一个只包含单个表达式的匿名函数:接收 item,返回它的 score。它适合作为排序键这类短小回调;逻辑稍复杂时,应该改成有名称的普通函数。
条件判断让程序做选择
if question.strip():
print("开始处理")
else:
print("问题不能为空")
条件最终会被解释为真或假。空字符串、空列表、空字典、数值 0 和 None 在条件中都视为假,但不必为了追求简短把所有判断写得隐晦。涉及业务规则时,明确往往比聪明更重要。
互斥条件可以使用 if、elif、else:
score = 0.82
if score >= 0.9:
level = "high"
elif score >= 0.7:
level = "medium"
else:
level = "low"
只有简单的二选一赋值适合条件表达式:
status = "有结果" if score > 0 else "无结果"
分支逻辑一旦包含多个动作,就应恢复为普通 if,不要为了压缩行数牺牲可读性。
Python 3.10 及以上可以使用 match/case 匹配一组离散结构:
command = "search"
match command:
case "search":
action = "执行检索"
case "summarize":
action = "生成摘要"
case _:
action = "未知命令"
简单范围判断仍然适合 if/elif;match/case 更适合明确的命令、状态或结构匹配。
循环让同一种处理作用于多条数据
for document in documents:
print(document["title"])
for 适合遍历已有集合;while 适合“不知道要重复多少次,只知道何时结束”的场景。处理文档、消息和检索结果时,for 更常见。
range() 常用于生成整数序列:
for index in range(1, 4):
print(index) # 1、2、3
遍历时需要位置和值,可以使用 enumerate():
documents = ["python.md", "rag.md"]
for index, name in enumerate(documents, start=1):
print(f"第 {index} 篇:{name}")
continue 跳过本轮剩余逻辑,break 结束整个循环:
results = ["", "Python 基础", "RAG 原理", "Agent 设计"]
for result in results:
if not result:
continue
if result == "RAG 原理":
print("找到目标")
break
while 必须保证条件最终能够变为假:
retry_count = 0
max_retries = 3
while retry_count < max_retries:
retry_count += 1
print(f"第 {retry_count} 次尝试")
忘记更新循环条件会形成无限循环。
函数要有清楚的输入和输出
def normalize_question(question: str) -> str:
return question.strip()
类型标注不会自动拦住所有错误,但它把函数的约定写得更清楚:输入一个字符串,返回一个字符串。
判断一个基础函数是否清楚,主要看四点:
- 函数名是否表达动作。
- 参数是否只保留必要输入。
- 返回值是否稳定。
- 函数是否只承担一个主要职责。
函数可以使用位置参数或关键字参数:
def build_message(role: str, content: str) -> dict[str, str]:
return {"role": role, "content": content}
user_message = build_message("user", "什么是 RAG?")
system_message = build_message(
role="system",
content="请根据资料回答。",
)
默认参数用于提供常用配置:
def truncate_text(text: str, limit: int = 100) -> str:
return text[:limit]
数量不固定的位置参数会被收集为元组,关键字参数会被收集为字典:
def merge_contexts(*contexts: str, separator: str = "\n") -> str:
return separator.join(contexts)
def create_config(**options: object) -> dict[str, object]:
return options
context = merge_contexts("资料一", "资料二", separator="\n---\n")
config = create_config(model="demo-model", temperature=0.2)
第一阶段不需要为了使用 *args 和 **kwargs 而使用它们。参数名称固定时,明确列出往往更容易维护。
调用函数时,* 可以解包序列,** 可以解包字典:
def build_message(role: str, content: str) -> dict[str, str]:
return {"role": role, "content": content}
position_values = ("user", "什么是 Python?")
keyword_values = {"role": "system", "content": "请简洁回答。"}
user_message = build_message(*position_values)
system_message = build_message(**keyword_values)
解包后的元素数量和键名必须符合函数签名,否则会触发 TypeError。
参数列表中的 * 可以要求后面的参数必须通过名称传入:
def search_documents(
question: str,
*,
limit: int = 3,
min_score: float = 0.0,
) -> list[str]:
return [f"{question} | limit={limit} | min_score={min_score}"]
results = search_documents(
"Python 环境",
limit=5,
min_score=0.7,
)
对于 limit、min_score 这类容易混淆的配置,强制使用关键字参数能让调用位置更清楚。
变量作用域决定名字在哪里可见:
DEFAULT_LIMIT = 3
def select_items(items: list[str]) -> list[str]:
selected = items[:DEFAULT_LIMIT]
return selected
DEFAULT_LIMIT 是模块级名称,selected 只存在于函数调用内部。函数优先通过参数接收需要变化的数据,减少在函数内部直接修改全局变量。
Python 查找名称时可以先记住 LEGB 顺序:局部作用域、闭包作用域、全局作用域、内置作用域。global 用来声明要重新绑定模块级名称,nonlocal 用来重新绑定最近一层闭包中的名称:
def build_counter():
count = 0
def increase() -> int:
nonlocal count
count += 1
return count
return increase
counter = build_counter()
print(counter()) # 1
print(counter()) # 2
这个例子用于说明作用域,不代表所有计数器都应写成闭包。大量修改全局状态会让调用关系难以追踪,更常见的做法仍然是通过参数和返回值传递数据。
函数开头的字符串可以作为文档字符串,记录函数约定:
def normalize_question(question: str) -> str:
"""去掉问题首尾空白,返回清洗后的字符串。"""
return question.strip()
递归也属于函数调用,但它的重点是问题拆分、终止条件和调用栈,将在 03 阶段结合树与算法统一展开。
可运行实现:从文档列表构造一个 Prompt
输入是用户问题和三条文档;处理过程会过滤无关文档、限制数量并拼接上下文;输出是一段可以交给模型的 Prompt。
def select_documents(
question: str,
documents: list[dict[str, str]],
limit: int = 2,
) -> list[dict[str, str]]:
keywords = set(question.lower().split())
selected = []
for document in documents:
content = document["content"].lower()
if any(keyword in content for keyword in keywords):
selected.append(document)
if len(selected) >= limit:
break
return selected
def build_prompt(question: str, documents: list[dict[str, str]]) -> str:
clean_question = question.strip()
if not clean_question:
raise ValueError("问题不能为空")
selected = select_documents(clean_question, documents)
if not selected:
context = "没有找到相关资料。"
else:
context = "\n".join(
f"[{item['title']}] {item['content']}" for item in selected
)
return f"请只根据上下文回答。\n\n上下文:\n{context}\n\n问题:{clean_question}"
documents = [
{"title": "虚拟环境", "content": "venv 可以隔离不同项目的 Python 依赖。"},
{"title": "文件编码", "content": "读取文本文件时应明确使用 UTF-8 编码。"},
{"title": "异常处理", "content": "异常处理用于应对运行期间的可预期失败。"},
]
prompt = build_prompt("venv 如何隔离依赖", documents)
print(prompt)
预期结果会包含“虚拟环境”文档,而不会把所有资料都塞进上下文。
这个例子使用了:
- 字符串保存问题和正文。
- 列表保存多条文档。
- 字典表示每条文档。
- 集合保存去重后的关键词。
if处理空输入和无结果情况。for遍历文档。- 函数拆分筛选和 Prompt 构造两个动作。
它不是一个真正的语义检索器,但已经把数据如何经过程序流转表达清楚了。
常见问题与排查
把 print() 当成 return
print() 只是把内容显示到控制台,return 才会把结果交回调用位置:
def wrong_add(a: int, b: int) -> None:
print(a + b)
result = wrong_add(1, 2)
print(result) # None
如果函数的结果还要进入下一步,就应该返回它。
忘记容器是可变的
列表和字典可以原地修改。同一个对象被多个变量引用时,一处修改可能影响另一处:
messages = ["你好"]
backup = messages
backup.append("请介绍 Python")
print(messages) # 两条消息
需要独立副本时,应根据数据结构选择浅拷贝或深拷贝,而不是只换一个变量名。
边遍历边删除列表元素
循环期间修改正在遍历的列表,容易跳过元素。更清楚的方式通常是构造新列表:
cleaned = [text for text in texts if text.strip()]
函数参数默认值使用可变对象
不要写成:
def add_message(message: str, history: list[str] = []):
history.append(message)
return history
这个列表会在多次调用之间共享。更安全的写法是默认使用 None,在函数内部创建新列表。
变量名遮住内置函数
把变量命名为 list、str、sum,后面再调用同名内置函数会遇到麻烦。变量名应该表达业务含义,例如 documents、total_score。
小结
这一篇让我从“会写单独语句”走到了“能表达一段完整处理逻辑”。
现在遇到需求时,可以先判断:数据是什么形状,程序有哪些分支,哪些步骤值得成为函数。这个顺序比见到需求就开始堆代码更可靠。
本篇检查清单:
- 能说明
str、数字、bool和None的用途。 - 能根据顺序、可变性、去重和键值关系选择容器。
- 能完成字符串与四类容器的常用操作。
- 能读写列表、集合和字典推导式。
- 能使用
zip()对齐数据,并使用sorted()指定排序键。 - 能写出包含多个分支的
if。 - 能判断条件表达式和
match/case是否适合当前分支。 - 能正确选择
for或while。 - 能解释
break与continue的区别。 - 能定义包含参数、默认值和返回值的函数。
- 能解释调用时的
*、**解包和仅限关键字参数。 - 知道可变对象和作用域可能带来的副作用。
不过,当函数和数据越来越多,全部放在一个文件里又会产生新的问题。下一篇要解决的,就是如何用模块和包拆开代码,并让文件读取失败时,程序能够给出可理解的反馈。
License: CC BY-NC 4.0
Updated 3 hours ago
Was this article helpful? Give it a like.
0 comments


