freeCodeCamp Python 每日编程挑战解析:Challenge 8 Factorializer(求阶乘)的完整实现与源码机制
【免费下载链接】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 每日编程挑战的 Challenge 8: Factorializer 为主体,完整讲解该题的题目定义、验收测试、官方参考解法及其背后的数学原理,并结合课程仓库的源码(挑战类型定义、Pyodide 测试运行器、每日挑战种子脚本)说明这类"每日挑战"从课程文件到浏览器中执行 Python 单测的完整链路。读完后你可以独立通过该题,并理解 freeCodeCamp 是如何在纯前端环境下运行runPython测试并判定挑战完成的。
题目定义与验收标准
该挑战位于课程块daily-coding-challenges-python(Python 版每日编程挑战),文件为 681cb1b0dab50c87ddb2e519.md。题目原文的核心要求是:
Given an integer from zero to 20, return the factorial of that number. The factorial of a number is the product of all the numbers between 1 and the given number. The factorial of zero is 1.
即:给定一个 0 到 20 之间的整数,返回它的阶乘。一个数的阶乘是 1 到该数之间所有整数的乘积,且特别约定0 的阶乘是 1(这是数学上的空积定义,也是本题第一个验收测试考察的边界情况)。
题目的三个验收断言(hints)在课程文件中直接以runPython测试的形式给出:
| 测试输入 | 期望输出 | 考察点 |
|---|---|---|
factorial(0) | 1 | 边界约定 0! = 1 |
factorial(5) | 120 | 常规计算(1×2×3×4×5) |
factorial(20) | 2432902008176640000 | 上限值,结果为 19 位大整数 |
这三个断言在课程文件中均以unittest的assertEqual形式书写,例如针对factorial(0)的测试:
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(factorial(0), 1)`) }})factorial(20)对应的期望值2432902008176640000要求实现必须使用任意精度整数。Python 的 int 本身就是大整数类型,因此无论用循环还是递归都不会溢出;这一点与许多强类型语言不同,也是 Python 适合承担此类题目的原因。
种子代码与官方解法
挑战文件中的# --seed--部分给学习者提供了初始骨架:
def factorial(n): return n这个种子实现直接返回入参,必然无法通过任何测试——学习者需要把函数体改写为真正的累乘逻辑。挑战文件# --solutions--部分给出的官方参考解法是迭代版本:
def factorial(n): result = 1 for i in range(1, n + 1): result *= i return result逐步拆解这个实现:
result = 1:把累乘器初始化为乘法单位元 1。这个初始值同时天然覆盖了n = 0的分支——当n为 0 时,range(1, 1)是空区间,循环体一次都不执行,函数直接返回 1,恰好满足"0! = 1"的约定,无需单独写if n == 0判断。for i in range(1, n + 1):range右端开区间,所以要写成n + 1才能取到n。循环变量依次取1, 2, ..., n。result *= i:每一步把当前值乘进累乘器。以n = 5为例,result依次变为 1 → 2 → 6 → 24 → 120,与期望输出一致。- 时间复杂度 O(n),空间复杂度 O(1)。对
n ≤ 20的输入规模,迭代实现没有任何性能压力,且避免了递归版本的栈帧开销。
除官方解法外,还存在几种等价的常见写法,读者可以自行验证其同样能通过全部三个断言:
def factorial(n): if n == 0: return 1 return n * factorial(n - 1)递归版本把n! = n × (n-1)!的定义直接翻译为代码,显式处理n == 0的基准情形;也可以用标准库一行完成:
from math import factorial或借助functools.reduce做函数式累乘。但就本挑战的教学目标(练习for循环与累乘器模式)而言,官方给出的迭代实现是最贴切的写法。
课程中的技术上下文:挑战类型、测试运行与数据落地
challengeType 29:dailyChallengePy
挑战 front-matter 中challengeType: 29并非随意编号。在共享配置 challenge-types.ts 中,数值 29 被定义为dailyChallengePy,其旁边的 28 是dailyChallengeJs(JavaScript 版每日挑战):
const dailyChallengeJs = 28; const dailyChallengePy = 29;该文件还给出三个与本挑战直接相关的映射,解释了课程渲染与判题行为:
- 视图类型:
viewTypes中[dailyChallengePy]: 'classic',即该挑战使用classic布局渲染,对应 Challenges/classic 模板目录; - 提交类型:
submitTypes中[dailyChallengePy]: 'tests',表示完成挑战的方式是通过代码测试,而不是提交项目 URL; - 语言判定:
getDailyCodingChallengeLanguage会把类型 29 映射为'python',配合 getIsDailyCodingChallenge 用于区分每日挑战与普通 Python 练习。
此外,hasNoSolution 的无解法类型列表中不包含 29,与本题文件中确实提供了# --solutions--参考解法相印证。
块级元数据 daily-coding-challenges-python.json 声明该块usesMultifileEditor: true、helpCategory: "Python",并把本挑战以"id": "681cb1b0dab50c87ddb2e519", "title": "Challenge 8: Factorializer"登记在challengeOrder第 8 位(前 7 题依次是 Vowel Balance、Base Check、Fibonacci Sequence 等,后接 Sum of Squares、3 Strikes 等),与文件所在目录和标题一一对应。
runPython:Pyodide 在 Web Worker 中执行 Python 单测
hints 中的runPython不是普通的字符串占位符,而是浏览器端的真实测试通道。其执行入口在 python-worker.ts:该脚本把用户代码和测试代码交给 Pyodide(WASM 版 CPython)在 Web Worker 中运行:
- Worker 启动时通过
loadPyodide从 CDN 加载 Pyodide 运行时(indexURL指向cdn.jsdelivr.net/pyodide/...),之后调用postMessage({ type: 'stopped' })通知主线程就绪; - Worker 内部还写入了一个
format_exception.py(内容来自@freecodecamp/curriculum-helpers的formatException),用于把 Python 异常格式化成友好的错误提示; - 脚本特意
Object.freeze(self),"防止学习者把 worker 带进奇怪的状态"(源码注释原话),这体现了测试沙箱的防御性设计。
因此,当你点下"Run Tests",TestCase().assertEqual(factorial(5), 120)这段 Python 测试是在浏览器内真实执行的 CPython 解释器里运行的——本题的三条断言全部通过即判定挑战完成(submitTypes为'tests'的判题方式)。这也意味着参考解法中的大整数2432902008176640000会在 WASM 运行时中按 Python 任意精度整数精确比较,不存在浮点近似问题。
每日挑战的数据来源:从课程文件到数据库
每日挑战的"每日"含义由种子脚本 seed-daily-challenges.ts 实现:它通过 GraphQL 从 dev-playground 超块抓取 JavaScript 与 Python 两个语种的挑战,按相同序号两两配对,写入 MongoDB 的DailyCodingChallenges集合。脚本中有几个值得注意的约束:
EXPECTED_CHALLENGE_COUNT = 365:一年 365 个每日挑战,JavaScript 与 Python 数量必须一致,否则抛错;- 起始日期固定为
2025-08-11T00:00:00.000Z(UTC),第 i 个挑战的日期为起始日期加 i 天;脚本在写入前会用字符串比对校验该日期未被无意修改,并提示"上线后不应更改起始日期"; - 运行方式见 tools/daily-challenges/README.md:复制
sample.env为.env、安装依赖、以显示"upcoming changes"模式启动主客户端(脚本需要 GraphQL 返回挑战数据),然后在tools/daily-challenges目录执行pnpm seed-daily-challenges。
types.ts 定义了入库后的挑战数据结构:id、title、date、description、tests(含testString与text,正对应本文三条runPython断言)以及challengeFiles(含contents与filekey,正对应# --seed--中的初始代码)。可以推断:Factorializer 这道题在课程仓库中以 Markdown 文件维护,经 GraphQL 导出后按日期排入每日挑战序列,学习者打开"当日挑战"页面时看到的题目描述、种子代码与测试,最终都源自这一份课程文件。
小结
- 题目本质:实现
factorial(n)(0 ≤ n ≤ 20),核心考点是累乘器模式与"0! = 1"的边界约定;factorial(20) = 2432902008176640000要求任意精度整数。 - 推荐实现:官方迭代解法
result = 1; for i in range(1, n + 1): result *= i,用result = 1的初值自然覆盖 n = 0 分支,O(n) 时间、O(1) 空间。 - 判定机制:
challengeType: 29(dailyChallengePy)使该题以 classic 视图渲染、以'tests'方式判题;三条runPython断言由 python-worker.ts 中的 Pyodide Worker 在浏览器内真实执行。 - 数据链路:课程 Markdown → GraphQL(dev-playground 超块)→ seed-daily-challenges.ts 配对 JS/Python 挑战并按 2025-08-11 起始日期逐日排布入库,形成 365 天的每日挑战序列。
【免费下载链接】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),仅供参考