freeCodeCamp Python 每日挑战实战:S P A C E J A M 字符串空格变换(Challenge 4)详解
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本文基于 freeCodeCamp 课程库中的 Python 每日编程挑战第 4 题「S P A C E J A M」挑战文档展开,完整讲解这道字符串变换题的题目要求、全部测试用例与官方解法,并结合仓库源码剖析challengeType: 29(dailyChallengePy)挑战是如何被定义、打包进课程区块、最终在浏览器中通过 Pyodide Worker 执行和测试验证的整条链路。读完你可以独立复现该题解法、理解 freeCodeCamp Python 挑战文件的 Markdown 结构化格式(description / hints / seed / solutions 四段式),以及测试代码中runPython沙箱的执行机制。
题目背景:daily-coding-challenges-python 区块中的第 4 题
该挑战位于课程仓库 daily-coding-challenges-python 挑战文件,其 Frontmatter 元数据为:
id: 681cb1afdab50c87ddb2e515 title: "Challenge 4: S P A C E J A M" challengeType: 29 dashedName: challenge-4区块编排文件 daily-coding-challenges-python.json 中可以看到该挑战的登记位置:
{ "id": "681cb1afdab50c87ddb2e515", "title": "Challenge 4: S P A C E J A M" }该区块整体配置了"helpCategory": "Python"、"usesMultifileEditor": true与"blockLayout": "legacy-challenge-list",说明它属于 Python 帮助分类、在多文件编辑器中呈现的每日挑战列表。
关于challengeType: 29,在共享包 challenge-types.ts 中有明确定义:
const dailyChallengePy = 29;同一文件中还声明了该类型的配套属性:[dailyChallengePy]: 'classic'(展示样式)、[dailyChallengePy]: 'tests'(测试面板形态)、[challengeTypes.dailyChallengePy]: 'python'(语言映射)。也就是说,challengeType: 29是 freeCodeCamp 专属的「Python 每日挑战」类型,与常规 Python 课程挑战(challengeTypes.python)在运行时走相同的执行路径,但在页面与进度逻辑上独立区分。
题目要求:三步字符串变换
题目原文(description 段)的要求是:
给定一个字符串,移除字符串中的所有空格,在每两个字符之间插入两个空格,将所有字母转换为大写,并返回结果。
- 非字母字符保持不变(空格除外)。
拆解一下这条规则链,它实际上规定了三个必须按序执行的操作:
- 删空格:移除输入中的全部空白字符(包括连续多个空格),这是唯一的“删除”动作;
- 加空格:在剩余相邻字符之间插入两个空格作为分隔符;
- 转大写:仅字母字符变大写,标点、数字、
@、$、&等非字母字符保持原样(大小写本身就不受影响)。
顺序有讲究:如果先插入空格再删空格,插入的空格会被误删;如果先转大写,结果虽然等价但不符合题目给出的操作顺序。官方解法采用的正是「先删、再插、最后大写」的顺序。
种子代码(seed 段)
挑战文档的 seed 段给学习者的初始代码是一个直接原样返回的存根函数:
def space_jam(s): return s学习者需要在space_jam内部实现上述三步变换并return结果。
全部五个测试用例(hints 段)
挑战文档的 hints 段给出了 5 组断言,它们是本题的完整验收标准。这些测试通过runPython在沙箱中执行 Python 代码,调用unittest.TestCase().assertEqual校验返回值:
用例 1:常规混合大小写字符串
space_jam("freeCodeCamp")应返回"F R E E C O D E C A M P"
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(space_jam("freeCodeCamp"), "F R E E C O D E C A M P")`) }})用例 2:含大量前导、内部、尾随空格的字符串
space_jam(" free Code Camp ")应返回"F R E E C O D E C A M P"
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(space_jam(" free Code Camp "), "F R E E C O D E C A M P")`) }})注意:这个用例和用例 1 的期望输出完全相同——它专门验证「所有连续空格(无论出现在哪)都必须被彻底移除」这一规则。
用例 3:句尾标点
space_jam("Hello World?!")应返回"H E L L O W O R L D ? !"
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(space_jam("Hello World?!"), "H E L L O W O R L D ? !")`) }})用例 4:符号、数字与空格混合
space_jam("C@t$ & D0g$")应返回"C @ T $ & D 0 G $"
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(space_jam("C@t$ & D0g$"), "C @ T $ & D 0 G $")`) }})这个用例覆盖了@、$、&、数字0等非字母字符:它们不参与大小写转换,但作为普通字符参与「两空格分隔」。注意&两侧的原始空格被删除后,&自己也成为一个被分隔的字符。
用例 5:短语
space_jam("allyourbase")应返回"A L L Y O U R B A S E"
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(space_jam("all your base"), "A L L Y O U R B A S E")`) }})这里有一个值得注意的细节:文档的说明文字写的是输入"allyourbase"(无空格),而实际断言代码传入的是"all your base"(含两个空格)。两者经过第一步删空格后都归一化为allyourbase,因此结果一致——这也从侧面印证了「先删空格」是整个变换正确性的关键前提。
官方解法(solutions 段)逐行剖析
挑战文档 solutions 段给出的官方解法只有三行:
def space_jam(s): s = s.replace(" ", "") s = " ".join(s) return s.upper()逐行对应题目三步:
| 代码 | 对应操作 | 说明 |
|---|---|---|
s.replace(" ", "") | 移除所有空格 | str.replace无第三个参数时会替换所有出现位置,一次清空前导、内部、尾随及连续空格 |
" ".join(s) | 插入两个空格分隔 | 对字符串迭代时join遍历的是单个字符,因此for循环式的" ".join("abc")得到"a b c" |
s.upper() | 全部字母转大写 | str.upper只影响字母字符,@、$、?、!、数字等原样保留,精确满足「非字母字符保持不变」 |
以C@t$ & D0g$为例走一遍执行过程:
- 删空格后:
C@t$&D0g$ join后:C @ T...(中间过程C @ t $ & D 0 g $)upper()后:C @ T $ & D 0 G $,与用例 4 期望值一致。
从源码结构看,该解法没有用到任何第三方库,纯标准库三件套(replace/join/upper),时间复杂度为 O(n),n 为字符串长度——对这道每日挑战而言是最直接的实现路径。学习者也可以先upper()再变换,结果等价;测试只校验最终字符串,不约束内部步骤顺序。
边界行为推断
结合五组测试用例,可以推断出该题对边界输入的行为约定:
- 空字符串
"":三步操作均为恒等,期望返回""; - 纯空格字符串
" ":第一步删空后变为"",最终返回""(用例 2 中前导尾随空格被删除的行为推广到极端情况); - 连续多空格:与单个空格同等处理,全部删除,不会在结果中留下空位。
需要说明的是,这些行为是由题目规则与官方解法自然推导出来的,挑战文档本身只显式覆盖了上述五个断言。
挑战文件的四段式结构
这道题的 Markdown 文件展示了 freeCodeCamp Python 挑战的标准结构,对想理解课程仓库格式的人很有参考价值:
- Frontmatter:
id(24 位十六进制,与文件名一致)、title、challengeType、dashedName; # --description--:题目正文,渲染为学习者看到的题目描述;# --hints--:提示区,混排自然语言提示与内嵌的 JS 测试块({test: ...}形式,供测试面板执行);# --seed--→## --seed-contents--:编辑器初始代码;# --solutions--:通过全部测试后的参考解法。
hints 中的{test: () => { runPython(...) }}块是 JS 代码,运行在挑战测试沙箱里;runPython是沙箱暴露给测试的全局函数,把一段 Python 代码字符串送入 Pyodide 解释器执行。在客户端源码 frame.ts 中可以看到其实现入口:
export const runPythonInFrame = function ( ... ) { void contentDocument?.__runPython(code); }即最终调用 iframe 文档内部的__runPython,与学习者预览区共用同一套 Python 运行时。
运行时链路:从 challengeType 29 到浏览器里的 Pyodide
runPython之所以能在浏览器里直接跑 Python,依赖仓库中的一条完整执行链路:
挑战识别:execute-challenge-saga.js 的
updatePreviewSaga会检查挑战类型,dailyChallengePy(即 29)与python、multifilePythonCertProject、pyLab一样走 Python 分支:if ( challengeData.challengeType === challengeTypes.python || challengeData.challengeType === challengeTypes.multifilePythonCertProject || challengeData.challengeType === challengeTypes.pyLab || challengeData.challengeType === challengeTypes.dailyChallengePy ) { yield updatePython(challengeData); }代码投递:同文件的
updatePython(L409-L421)先buildChallengeData组装源码,再调用 python-worker-handler.ts 中的runPythonCode,把{ type: 'run', code }消息 post 给 Web Worker:export function runPythonCode(code: { contents: string; editableContents: string; }): void { lastCodeMessage = code; getPythonWorker().postMessage({ type: 'run', code }); }该处理器还维护了
lastCodeMessage:Worker 因忙碌或重置而停止监听后,客户端收到stopped消息会自动重放最后一次代码(L61-L68),保证学习者改动代码不会丢失执行。Worker 端执行:Worker 的源码是 python-worker.ts,它通过
loadPyodide加载 Pyodide(WASM 版 CPython),并用Object.freeze(self)冻结 Worker 全局对象以防止学习者代码破坏运行时状态。每次run请求到达后,initRunPython会创建全新的 globals dict、注册自定义print/input(通过pyodide.registerJsModule('jscustom', ...)),然后在隔离命名空间里pyodide.runPython(pyCode, { globals })执行学习者代码。挑战文档里那些runPython(\from unittest import TestCase ...`)` 测试代码,最终就是在这套沙箱里被解释执行的。
这条链路说明:daily-coding-challenges-python 区块的每一个挑战(包括本文的 S P A C E J A M),其 hints 中的runPython断言都是在浏览器内真实执行的 Pyodide Python 环境里完成的,无需后端服务器参与测试。
小结
- 本题的规则链是「删全部空格 → 相邻字符间插两个空格 → 字母转大写」,官方三行解法
replace+join+upper精确对应三步; - 五个测试用例共同覆盖了:混合大小写、连续/首尾空格、句尾标点、符号数字混合、短语输入五类场景,其中用例 2 与用例 5 都隐式验证了「内部空格必须全部删除」这一关键规则;
challengeType: 29在 challenge-types.ts 中定义为dailyChallengePy,使该挑战在运行时与常规 Python 挑战共用 Pyodide 执行链路(python-worker.ts + python-worker-handler.ts),而在课程编排上作为独立的每日挑战列表区块呈现;- 挑战 Markdown 的「Frontmatter + description + hints + seed + solutions」四段式结构,是 freeCodeCamp 课程库挑战文件的标准写法,hints 中的 JS 测试块与
runPython沙箱共同构成了挑战的验收机制。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考