
看到import random后面跟着def generate_book_title()这个片段我第一反应就是兄弟你这不是在写代码你是在给灵感枯竭做备胎。这个标题往深了挖其实就是用 Python 随机数生成器去做命名引擎而它踩中的恰好是 Python 入门到进阶之间最实用的一批知识点模块导入、random 库、def 函数封装、列表推导式、字符串拼接或 f-string 格式化。今天我就把这个项目从头到尾拆开顺着为什么这么写、还能怎么改、踩过哪些坑的顺序把整个实现过程讲透。这篇文章适合刚学完 Python 基础语法、想拿小项目练手的人也适合写过不少业务代码但没认真研究过 random 玩法的老手拿来当工具模板直接抄也行。1. 为什么有人会写一个随机生成书名的脚本1.1 这个片段背后是什么import random和def generate_book_title():这两行放在一起基本已经交代了全部意图导入随机数模块定义一个生成书名的函数。后面那个them大概率是theme主题或者then然后被打断的半截单词也就是说作者真正想写的是从一组主题词里随机抽取、拼装成一个像样书名的生成器。这类项目的本质是一个基于词库的随机组合器。它不需要神经网络不需要大模型核心逻辑就三步准备若干组候选词用random.choice()从中抽取再按模板拼接输出。十几行代码就能搭出雏形但真正把它做好里面涉及的细节其实挺多——词库怎么分类、模板怎么设计才能不像咒语生成器、随机结果怎么保证可复现这些才是这个项目真正的价值所在。1.2 哪些场景真的用得上别觉得生成书名是纯娱乐。我见过有人拿它做写作灵感辅助半夜写不出题目的时候跑一下蹦出一个蝴蝶与雾码头马上有画面了也有人把它做成投稿标题测试工具批量生成几十个备选标题做 AB 测试还有做新媒体运营的朋友把词库换成行业热词生成文章标题效率比人肉凑词高不少。从工程角度讲这个小项目覆盖的知识点非常典型import语句的运行机制以及random模块到底导入了什么函数的定义、参数设计、返回值约定列表list的切片、拼接、列表推导式字符串格式化的几种方式随机种子的作用、去重、边界条件处理所以别看它小把这一套吃透你后面写爬虫、写数据处理脚本、写自动化工具用到的基础能力全都在这了。2. 核心三件套import、random、def2.1 import random 真正做了什么很多教程告诉你import 就是把模块导入进来但没说清楚它到底发生了什么。当你写import random的时候Python 解释器会做三件事先在sys.path记录的目录列表里找有没有random.py或random包找到后把它编译成字节码并执行模块顶层的代码最后把模块对象绑定到当前命名空间的random这个名字上。这里有个容易忽略的细节import random并不会把random里所有名字都倒进你的全局命名空间你调用的时候仍然要写random.choice()。如果你嫌麻烦可以写from random import choice直接拿到choice这个函数但这样做的代价是命名空间里可能发生名字冲突——比如你恰好定义了一个叫choice的变量后面再调用就会莫名其妙报错。我的习惯是短脚本、频率低用import random可读性强一眼知道随机函数来自哪里循环内高频调用用from random import choice省去每次属性查找的开销虽然差异很小但养成习惯没坏处团队项目明确禁止from random import *这种写法会让代码审查的人血压升高因为你根本不知道导入了哪些名字顺带说一句py 的模块搜索路径问题也是新手高频踩坑点。有时候你明明装了库import却报ModuleNotFoundError十有八九是sys.path里没有当前项目目录或者你的脚本文件名和库名撞了。自己建了个random.py放项目根目录然后死活import random进的是自己那份这种乌龙我见过不止一次。提示排查 import 问题时先打印print(sys.path)确认搜索路径再检查项目里有没有跟标准库同名的文件这两个检查能解决掉七成以上的导入报错。2.2 random 库里的常用函数怎么选random模块的核心是基于梅森旋转算法的伪随机数生成器PRNG它不是真随机但对绝大多数应用场景足够。模块里函数很多写这种生成器项目你真正高频用到的其实就这几个函数作用典型返回值适用场景random.choice(seq)从序列中随机取一个元素雾从词库抽词random.sample(seq, k)不重复地抽取 k 个元素[雾, 码头]抽取多个不同词根random.shuffle(seq)原地打乱列表顺序无返回值列表被打乱批量生成时打乱输出顺序random.randint(a, b)返回 [a, b] 闭区间内的整数3随机决定拼接长度random.seed(n)设置随机种子无让随机结果可复现这里要特别提醒一个容易误用的函数random.sample如果k大于序列长度会直接抛ValueError。比如你词库只有 5 个词却写random.sample(words, 8)程序立刻崩。所以如果你的模板需要抽取的个数是动态的一定要先判断k len(seq)或者用random.choices(seq, kn)允许重复抽取——注意choices带 s是允许重复的很多人第一次都写错成choice。2.3 def 封装的边界def generate_book_title()这种命名方式在 Python 里属于动词开头、小写加下划线的标准函数命名风格。封装成函数而不是把逻辑裸写在脚本里好处是明显的可复用、可测试、可替换内部实现。但设计函数时有个问题很多人没想清楚参数怎么定。最开始的版本可能是无参数的def generate_book_title(): # 从全局词库里抽词 return f{random.choice(adjectives)}与{random.choice(nouns)}这么写跑通没问题但后面你会发现所有词库都依赖模块级全局变量一旦词库变大函数就像个长了触手到处抓数据的章鱼难维护。我建议从第二版开始就把词库传进去def generate_book_title(adjectives: list, nouns: list, template: str {}与{}) - str: left random.choice(adjectives) right random.choice(nouns) return template.format(left, right)参数化之后你的函数就不再关心词库从哪来是硬编码的列表、从 JSON 文件读的、还是从数据库查的都不影响调用。返回值类型也直接用- str标注清楚你在其他地方接收返回值时就知道该拿它当字符串处理。注意函数默认参数不要设成可变对象比如不要写def generate_book_title(words[])。因为默认参数只在函数定义时创建一次多次调用如果修改了这个列表状态会跨调用累积这是 Python 面试题级别的经典坑。3. 手把手实现一个书名生成器3.1 词库设计好书名靠词根随机生成的书名像不像样词库质量决定九成。语法模板只是皮词根才是血肉。我自己的经验是词库至少要分成两组以上才有组合感比如意象词 A雾、海、孤岛、雨季、长夜、诗、少年、远山、车站、街灯意象词 B鲸、码头、月亮、野马、风筝、邮差、糖果、勋章、白衬衫、地图模板走A与B结构就会得到雾与鲸孤岛与邮差这类书名。这种命名法的好处是两个意象之间留了空白读者自己会去脑补关联反而比硬写的爱与成长更有味道。如果你想更进一步可以增加第三组修饰词比如无声的漫长的最后的三月的让模板变成修饰词 A 与 B或者A 之 修饰词 B。这样组合空间会从 n×n 膨胀到 n×m×n但要注意修饰词和意象词的搭配协调性否则会出现三月的鲸与五月的码头这种造句感太强的结果。词库存放在哪也有讲究。项目初期直接写在 Python 文件里是合理的方便调试但词库过百条之后我强烈建议拆到独立的JSON文件里写一个加载函数读取这样编辑词库不需要动代码你甚至可以把这个功能做成一个简单的随机起名 SaaS后端词库挂数据库前端点按钮调接口。3.2 模板引擎组合出句式的变化如果模板永远只有一种生成器很快就会让人审美疲劳。一个实用的做法是准备多个模板字符串运行时随机选一个templates [ {a}与{b}, 那个{adj}的{a}和{b}, {a}之{b}, 在{adj}的{a}里等{b}, ] def apply_template(template: str, words: dict) - str: return template.format( arandom.choice(words[atoms_a]), brandom.choice(words[atoms_b]), adjrandom.choice(words[adjectives]), )这里用str.format做简单替换就够了。有人问要不要引入 Jinja2 这种重型模板引擎我说不用杀鸡焉用牛刀。format自带的花括号语法在这个规模下足够而且零依赖、易调试。模板的选择不是完全随机最好。我实测下来给不同模板设置权重会更自然。用random.choices(templates, weights[3, 1, 2, 1], k1)让A与B这种简洁结构更常出现文艺过头的长模板偶尔才触发一次整体观感立刻改善。choices带权重参数这个特性是替代choice的高频场景值得记住。3.3 完整代码与一次运行实录把上面的设计合起来一个可直接运行的脚本长这样import random import json from pathlib import Path def load_words(path: str) - dict: 从 JSON 文件加载词库返回 dict。 with open(path, r, encodingutf-8) as f: return json.load(f) def apply_template(template: str, words: dict) - str: 把词库中的随机词填入模板。 return template.format( arandom.choice(words[atoms_a]), brandom.choice(words[atoms_b]), adjrandom.choice(words[adjectives]), ) def generate_book_title(words: dict) - str: 根据模板权重随机生成一个书名。 templates [ {a}与{b}, 那个{adj}的{a}和{b}, {a}之{b}, 在{adj}的{a}里等{b}, ] weights [3, 1, 2, 1] template random.choices(templates, weightsweights, k1)[0] return apply_template(template, words) def main(): words load_words(words.json) for _ in range(5): print(generate_book_title(words)) if __name__ __main__: main()对应的words.json片段{ atoms_a: [雾, 孤岛, 雨季, 长夜, 远山], atoms_b: [鲸, 码头, 月亮, 邮差, 地图], adjectives: [无声的, 漫长的, 最后的, 三月的] }我本地跑了一次输出是这样的雾与鲸 那个漫长的孤岛和邮差 远山之月亮 在无声的雨季里等码头 最后的远山与地图第三次运行又得到一批完全不同的结果。如果你觉得某些组合闹太套直接删对应词根或调整模板权重就行改词库比改逻辑快得多。这也正是数据与逻辑分离设计的好处。3.4 加一个可复现随机参数随机项目最容易被问到的需求是刚才那批标题我觉得挺好的怎么找回同样的一批这时候就要用random.seed()了。给函数加一个可选参数def generate_book_title(words: dict, seed: int | None None) - str: if seed is not None: random.seed(seed) # ...后续逻辑不变只要种子固定随机序列就固定你调用一次generate_book_title(words, seed42)得到的一定是雾与鲸下次再传 42 还是它。这功能在调试和做对比实验时极其好用。注意一个细节random.seed()是全局状态如果你在循环里反复调用random.seed()会导致每次循环拿到同样的结果所以正确的姿势是在整个脚本入口设置一次而不是在函数内部每个随机操作前设置。提示Python 3.9 及以上才支持int | None这种类型联合写法。如果你还在用 3.8 及更早版本得改成Optional[int]否则会直接TypeError: unsupported operand type(s) for |。这是新手从新版本代码往旧环境迁移时经常踩的兼容性坑。4. 进阶玩法让生成器更懂行4.1 按题材分流词库基础版跑通之后你会发现一套词库走天下是不够的。写科幻的用不上邮差与糖果写情感文的用不上星舰与粒子加速器。所以第二步我建议给词库加题材维度words { romance: { atoms_a: [重逢, 情书, 夏夜], atoms_b: [旧站台, 白衬衫, 梧桐树], adjectives: [未完的, 错过的, 温柔的] }, sci_fi: { atoms_a: [星舰, 量子, 数据流], atoms_b: [黑洞, 休眠舱, 雾号], adjectives: [失真的, 坍缩的, 半衰期] } }调用时根据题材取子字典即可。实际用下来题材词库 通用模板的组合能让同一套 30 行代码产生完全不同的结果语境。这里还顺带练到了字典嵌套的取值技巧后面你在脚本里处理更复杂的配置信息时这种数据结构是家常便饭。4.2 批量生成并自动去重做标题测试的时候一次生成 20 个书名里面出现两个雾与鲸就很尴尬。所以批量场景下要有去重逻辑。方案有两个简单方案random.sample搭配词库因为 sample 不重复抽取从组合层面就减少重复通用方案生成结果存进集合set数量不够继续循环补def generate_batch(words: dict, count: int) - list[str]: seen set() results [] while len(results) count: title generate_book_title(words) if title not in seen: seen.add(title) results.append(title) return results这里要小心死循环风险如果词库太小、组合空间小于你要求的countwhile 循环会永远跑不完。遇到这种情况要么检查词库规模要么给循环加一个最大尝试次数比如max_attempts count * 50尝试次数用完了就直接返回当前结果避免把服务器跑死。4.3 从脚本到模块让别人 import 你的函数项目做到这里你已经有了load_words、generate_book_title、generate_batch这几个函数。这时你应该把它们从脚本升级为模块把文件命名为title_generator.py把带执行逻辑的main()用if __name__ __main__:保护起来。这样别人或你自己下一个项目就能这样使用from title_generator import generate_book_title, load_words words load_words(words.json) print(generate_book_title(words))这就是脚本和模块的分界线。脚本被 import 时顶层代码会立即执行所以所有可执行语句都应该放进main()函数只在模块底部用if __name__ __main__:去调用它。这样既保留了直接运行的入口又保证了被导入时不产生副作用。这个习惯从第一个练手项目开始养成后面写包、写服务会省很多事。5. 实操中的常见坑与排查记录5.1 import 相关的报错这个项目虽然是标准库项目但import报错问题在真实环境中非常普遍。我挑三种最常见的第一ModuleNotFoundError: No module named xxx。这不一定是没装库更多时候是运行环境和安装环境不一致。终端里pip install requests装到了全局 Python但 VSCode 里选的解释器是虚拟环境自然找不到。排查方法很简单在运行环境里执行pip list看看包在不在或者用python -m pip install xxx确保装到当前解释器。第二ModuleNotFoundError: No module named words.json这种诡异报错。检查你是不是把模块名拼错了或者试图 import 一个文件路径。JSON 文件不是模块不能用 import 加载必须用open()json.load()。有的新手混淆了import 数据和读取文件的概念这个一定要区分开。第三相对导入报错attempted relative import with no known parent package。当你把 generator 放进包的子目录里写了from . import words之类代码但直接按脚本方式运行时会报这个错。原因是你没有以包的方式运行__package__是空的。解决办法是在包的外层用python -m package.module的形式运行而不是python package/module.py。5.2 random 结果不对劲随机结果出问题方向一般指向两个一是结果每次都一样。通常是有人工干预了种子或者循环里反复random.seed()。遇到一直重复先搜代码里的seed关键字全局设一次就够了。二是结果看起来不够随机。特别典型的是短词库下random.choice看起来有规律比如雾出现频率明显高。这其实是样本量太小造成的错觉不是算法问题。你可以跑python -c import random; print([random.randint(0,9) for _ in range(30)])观察大量调用后分布会趋于均匀。如果你真的需要严格统计均匀性要先明确是伪随机布局还是彩票级随机大多数普通场景不涉及后者。另外提醒一个与随机无关但常见的问题random.sample和random.shuffle都要求传入序列如果你传的是字符串random.choice(abc)会返回单个字符而不是把abc当整体。我自己写生成器时就因为词库用了字符串而非列表导致抽出来的是雾字的一个笔画字符调试半天才发现。5.3 中文乱码和文件编码问题用中文词库最容易被坑的就是编码。words.json文件如果是 GBK 编码保存的Python 3 默认 UTF-8 读取就会UnicodeDecodeError。处理思路有两个方向在用编辑器保存时强制选择UTF-8编码在打开文件时显式声明open(path, r, encodingutf-8)我之前遇到过一种更隐蔽的情况文件本身是 UTF-8但前面带了个 BOM 头读取后第一个词开头多了一个\ufeff字符生成的第一个书名莫名其妙多了不可见符号。解决方法是open(path, r, encodingutf-8-sig)这会自动剥掉 BOM。Windows 下用记事本编辑过的 JSON 文件经常踩这个坑Linux 下正常Windows 下就多出字符排查起来特别迷惑。5.4 列表推导式没看懂跟这个项目强相关的热搜里有一个def main(): list1[x for x in range(20) if x%20]的片段这是列表推导式。我顺便说透[x for x in range(20) if x % 2 0]等价于取 0 到 19 之间所有偶数。你在这个项目里用到它的场景是从词库中筛掉某个词、过滤掉过短的词、批量构造模板列表。它比手写 for 循环更紧凑但也别滥用超过两层的推导式可读性会急剧下降。如果你在跑类似代码时得到的不是预期列表重点检查两部分if过滤条件放在for后面表达式放在最前面两个顺序反了会报语法错误。这个语法是 Python 特色C 系背景的人一开始总会写反多练几次就固化了。6. 写在最后几个实测后的心得体会这个生成器项目我前前后后改了三版从最开始的十几行硬编码脚本到现在的词库与逻辑分离、带权重模板、支持批量去重代码量也就七十行上下。我的最大体会是小项目的价值不在代码量而在你往里面塞了多少思考。词库分类、模板权重、随机种子、编码处理任何一个细节单独拎出来都不难但组合在一起就是一个拿得出手的完整工具。最后分享一个小技巧把生成器文件保存好词库 JSON 也保存好之后你写其他项目需要凑名字的时候——不管是变量命名、测试数据生成、还是内容运营标题——直接把generate_batch导入过去就能用。我后来甚至用这套逻辑改造成了一个随机生成诗词意象的版本思路一模一样只是把模板和词根换成了更古典的词汇。你完全可以在这个词库上继续扩充试着自己加一个年代感维度或者把生成结果接入 CLI 命令行交互说白了边界只取决于你想玩多大。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。