Python 字符串为什么不可变:从 replace 到变量重绑定

Python 字符串为什么不可变:从 replace 到变量重绑定 副标题: replace 看起来修改了字符串,实际发生的是创建结果并让变量改指向它。理解这一区别,才能看懂字符串方法、别名行为和循环拼接的成本。 适读人群: 刚开始学习 Python 字符串、变量与对象关系的读者 阅读时间: 6 分钟 从一段括号消除代码说起 下面的函数反复删除成对的括号,直到字符串不再变化: def is_valid_by_elimination(s: str) -> bool: while True: previous = s s = s.replace("()", "").replace("[]", "").replace("{}", "") if len(s) == len(previous): return s == "" 这里容易产生一个疑问: previous = s s = s.replace("()", "") 既然 previous 和 s 原来指向同一个字符串,第二行为什么不会同时改变 previous? 答案是:Python 字符串是不可变对象。replace 没有修改原字符串,而是计算出一个 结果,随后赋值语句让变量 s 改为指向这个结果。previous 仍然指向原字符串。 “不可变”到底是什么意思 字符串不可变,指的是: 一个 str 对象创建以后,它所表示的字符序列不能被原地修改。 例如: text = "cat" text[0] = "b" Python 会抛出异常: TypeError: 'str' object does not support item assignment 不能把已有字符串中的 c 原地改成 b。如果需要 "bat",必须得到另一个字符串值: ...

2026年8月28日 · 3 分钟 · map[name:Jeanphilo]

先做信息对等,再谈模型能力:Agent 失败时该优化系统还是模型

Agent 做错判断时,不要先凭感觉升级模型或增加工作流。先冻结模型实际可见的输入,再用同输入可解性、信息增量和输出唯一性三项测试定位问题究竟在推理侧、系统侧还是任务契约。

2026年8月28日 · 3 分钟 · map[name:Jeanphilo]

从 Python/Linux 到 C#/.NET:为 Windows Runner 建立第一套心智模型

面向熟悉 Python/Linux、但第一次接触 C# 的开发者,从 Windows Runner 场景出发解释 .NET、C#、CLR、SDK、NuGet 和 csproj,并在 WSL 中完成一个最小控制台项目。

2026年8月26日 · 5 分钟 · map[name:Jeanphilo]

管线只负责一次运行:文档索引系统不该默认共享跨 Run 节点

一次 RAG 与知识图谱独立运行模式的架构讨论,让我重新分清了 Pipeline、Run、动态 Target 和跨请求 Singleflight。我的结论是:文档索引系统更适合让每次 Run 独立拥有状态和节点,同一文件只保留一个活动 Run,并通过持久化产物复用结果,而不是共享正在执行的节点任务。

2026年8月26日 · 4 分钟 · map[name:Jeanphilo]

Speculative Decoding 为什么能加速:从并行验证到 GPU Memory-Bound

副标题 / 摘要 Speculative decoding 并不是让大模型先生成一遍,再与小模型逐个比较;它让小模型先提供未来 token 的候选路径,使大模型能在一次 forward 中并行验证多个位置。它的本质,是用更多、甚至部分无效的并行计算,换取更少的串行 Target forward。 预计阅读时长:12~16 分钟 目标读者:了解 Transformer 基础,希望理解 LLM 推理性能、vLLM 或 speculative decoding 的读者 核心问题:为什么一次验证多个 token 会比逐个生成更快?如果中间猜错,后面的计算是否全浪费了? 先给结论 理解 speculative decoding,只需要抓住四件事: 自回归生成慢在串行依赖:不知道前一个 token,就无法确定下一个 token 的条件分布。 Draft 模型先猜出一条候选路径后,Target 模型就能在一次 forward 中并行计算这条路径上多个位置的条件分布。 第一个未被接受的 token 之后,验证结果确实全部作废;这是用并行冗余换串行轮次的代价。 加速是否成立,取决于接受长度、Draft 成本、Target verify 成本、batch 大小和 KV Cache 流量,而不是只看一次 draft 了多少个 token。 一句话概括: Draft 模型最重要的作用,不只是“帮大模型猜答案”,而是暂时补上未知的未来输入,让 Target 能跨过原本必须串行执行的多个解码位置。 普通自回归生成为什么必须串行 假设当前上下文是: I love Target 模型准备生成: New York City very much 普通 decode 只能这样执行: ...

2026年8月26日 · 6 分钟 · map[name:Jeanphilo]

LeetCode 155:最小栈,如何让最小值跟着栈一起变化

题目要求 设计一个栈 MinStack,支持以下操作: MinStack():初始化栈。 push(value):把 value 压入栈顶。 pop():删除栈顶元素。 top():返回栈顶元素。 getMin():返回栈中的最小元素。 题目要求每个操作的时间复杂度都是 O(1)。 pop、top 和 getMin 只会在栈非空时调用,因此不需要为这些方法设计额外的空栈返回值。 示例 输入操作: ["MinStack", "push", "push", "push", "getMin", "pop", "top", "getMin"] 输入参数: [[], [-2], [0], [-3], [], [], [], []] 输出: [null, null, null, null, -3, null, 0, -2] 对应的执行过程是: min_stack = MinStack() min_stack.push(-2) min_stack.push(0) min_stack.push(-3) min_stack.getMin() # -3 min_stack.pop() min_stack.top() # 0 min_stack.getMin() # -2 约束 -2^31 <= value <= 2^31 - 1 最多调用 3 * 10^4 次 push、pop、top 和 getMin pop、top 和 getMin 调用时栈一定非空 Step 1:弹出最小值后,前一个最小值从哪里回来 普通栈已经能保存值,并按照后进先出的顺序执行 push、pop 和 top。当前 baseline 是: ...

2026年8月21日 · 4 分钟 · map[name:Jeanphilo]

LeetCode 20:有效的括号,为什么数量相同仍然无效

题目要求 给定一个只包含以下六种字符的字符串 s: ( ) { } [ ] 判断这个字符串是否有效。有效字符串必须同时满足三个条件: 每个左括号都由相同类型的右括号闭合。 左括号必须按照正确顺序闭合。 每个右括号都有一个对应的同类型左括号。 满足全部条件时返回 True,否则返回 False。 示例 输入 输出 "()" True "()[]{}" True "(]" False "([])" True "([)]" False 约束 1 <= s.length <= 10^4 s 只包含 ()[]{} 中的字符 LeetCode 提供的方法签名是: class Solution: def isValid(self, s: str) -> bool: pass Step 1:数量相同为什么仍然无效 先看两个字符串: ()[]{} ([)] 它们都有一个 ( 和一个 )、一个 [ 和一个 ]。如果只分别统计三种左括号和右括号的数量,这两个字符串都会通过检查。 当前 baseline 是: 分别统计每种左括号和右括号;数量全部相同就认为字符串有效。 这个 baseline 会在 ([)] 上给出错误答案。问题不在括号数量,而在闭合顺序:读到一个右括号时,它必须闭合最近遇到、但还没有被闭合的左括号。 把这个规则用于 ([)]: ...

2026年8月21日 · 3 分钟 · map[name:Jeanphilo]

LeetCode 394:字符串解码,如何保存并恢复嵌套上下文

题目要求 给定一个编码字符串 s,返回它解码后的字符串。 编码规则是: k[encoded_string] 方括号中的 encoded_string 需要连续重复 k 次,其中 k 是正整数。编码可以嵌套,也可以与普通小写字母相邻。 题目保证: 输入字符串始终有效,方括号完整配对且没有多余空格。 原始文本不包含数字,数字只表示重复次数。 不会出现 3a 或 2[4] 这类不符合编码规则的输入。 解码后的字符串长度不会超过 10^5。 示例 输入 输出 "3[a]2[bc]" "aaabcbc" "3[a2[c]]" "accaccacc" "2[abc]3[cd]ef" "abcabccdcdcdef" 约束 1 <= s.length <= 30 s 只包含小写英文字母、数字和 [] 所有重复次数都在 [1, 300] 范围内 LeetCode 提供的方法签名是: class Solution: def decodeString(self, s: str) -> str: pass Step 1:进入内层以后,外层信息去了哪里 先从没有嵌套的输入开始: 3[a] 读到 3 后知道下一段需要重复三次;读完方括号中的 a,得到: a * 3 = aaa 当前 baseline 是: 读出一个重复次数,再收集后续方括号中的文本,遇到 ] 时执行重复。 这个 baseline 可以处理一层 3[a],却会在 3[a2[c]] 上中断。外层已经读到重复次数 3 和普通字母 a,此时又遇到了内层编码 2[c]。如果直接把当前次数改成 2、当前文本改成 c,外层的 3 和 a 就丢失了。 ...

2026年8月21日 · 4 分钟 · map[name:Jeanphilo]

从复合意图到可执行查询:面向实体的多源证据编排

当一个自然语言要求同时涉及多个主体、结构化事实、附件和原文时,可以先将复合意图编译为带主体的原子查询,再完成实体绑定、多源执行与按主体归并。知识图谱只是可选的主体解析工具,不是这套模式的本体。

2026年8月17日 · 4 分钟 · map[name:Jeanphilo]

二分查找区间怎么选:从候选集合到边界更新

写二分查找时,我们经常先看到这样的初始化: left = 0 right = len(nums) - 1 本文讨论的输入是按非递减顺序排列、允许重复值的 list[int]。精确查找要返回某个等于 target 的实际下标,不存在时返回 -1;左边界查找要返回 [0, n] 中第一个满足 nums[i] >= target 的位置,如果不存在这样的数组元素,就返回 n。 它看起来像是在声明一个闭区间,但只看这两行还不够。真正决定区间含义的是下面四件事是否一致: left 和 right 是候选下标、未分类边界,还是哨兵; 循环在什么条件下说明仍有实际元素需要检查; 检查 mid 后,更新是否正确排除了它; 循环结束时,哪个位置承载最终答案。 最可靠的判断方法不是背代码,而是先问一句: 当前哪些实际下标还没有被排除或分类,答案位置又被限制在哪个范围内? 这些持续成立的事实就是二分查找的循环不变式。区间符号、初始化和更新规则都应该从它推出来。 闭区间 [left, right] 当 left 和 right 都指向尚未排除的实际数组下标时,候选集合是闭区间: left = 0 right = len(nums) - 1 此时两个端点都可能是答案,所以只要 left <= right,区间就仍然非空: while left <= right: 如果 mid 不是答案,下一轮必须把它排除: if nums[mid] < target: left = mid + 1 else: right = mid - 1 因此,这套约定是: 项目 规则 候选集合 [left, right] 初始化 left = 0, right = n - 1 非空条件 left <= right 排除 mid left = mid + 1 或 right = mid - 1 空区间 left > right 它很适合“找到任意一个等于 target 的位置”:命中时直接返回,区间为空时返回不存在。 ...

2026年8月14日 · 4 分钟 · map[name:Jeanphilo]