资讯动态

用Python random库从零写一个随机书名生成器:import与def实战

发布时间:2026/10/13 13:44:42 来源:尧图企业网站定制
写小说最怕的不是写不出正文而是正文写完了书名还没头绪。上个月帮一个写悬疑小说的朋友解决这个问题我说与其你一个词一个词地憋不如用 Python 写个 generate_book_title 函数把灵感交给 random 库。那大概是我最近做过最实用的小工具之一——import random导入模块def定义函数几个列表一组合书名就批量往外蹦。这个项目看着简单其实非常适合刚学完 Python 基础语法、正愁找不到练手项目的人。它能把import语句的机制、random库的函数选择、def的函数设计还有各种 ImportError 报错排查全部串起来一个项目把入门阶段最容易踩的坑都过一遍。即便你已经有几年经验把这种小工具重构得干净、可扩展也能体会到不少乐趣。这篇文章不讲虚的直接带你从头写一个能用的版本顺带把我在实战里遇到的各种问题都摊开说清楚。1. 项目复盘随机书名生成器到底要设计什么1.1 核心需求拆解书名起不来的本质问题先搞清楚一件事人为什么起名难不是因为词汇量不够而是因为大脑在无压力状态下反而不擅长做限定条件下的组合创新。你脑子里可能有月光、岛屿、告别这些词但让它们两两组合的时候思维反而容易锁死在最熟悉的几组搭配上翻来覆去就是月光下的岛屿这种水平。机器没有这种思维惯性。把这件事交给 Python我们要做的事情就清晰了准备一个词库设计若干组合规则再让random来做选择。一个完整的书名生成器应该包含三块内容词库数据层、随机策略算法层、函数接口交互层。词库解决有什么可选随机策略解决怎么选、选多少、要不要加权函数接口解决调用方怎么传入风格、批量、去重等需求。这三层想清楚之后代码结构自然就出来了。初学者最容易犯的错是一上来就写逻辑词库随便放两个列表就开始拼最后发现风格单一、调用混乱想扩展只能推倒重来。先把需求拆成数据、算法、接口三层后面每一步都顺。1.2 技术选型为什么用 random 库而不是自己造轮子有人问过我随机数嘛我自己用当前时间戳取个模不就行了比如int(time.time()) % 100选词。这在技术上确实能跑但有两个致命问题一是分布不均匀时间戳取模的结果在特定区间内会出现明显偏斜二是周期性很强同一个时间段内生成的结果会大量重复。random库是 Python 官方的标准库底层用 C 语言实现核心算法是梅森旋转Mersenne Twister。它是一种伪随机数生成算法虽然序列是确定的、由种子决定但统计性质非常好分布均匀、周期长达 2^19937 - 1对日常应用来说完全够用。你只需要记住一个结论凡是需要均匀、不可预测、统计上靠谱的随机行为直接用标准库别自己造。那random库里那么多函数怎么选我常用的是这么几个函数作用典型场景random.choice(seq)从序列中均匀随机取一个元素从词库里抽一个词random.choices(seq, weights...)按权重取一个或多个元素让某些风格词出现概率更高random.randint(a, b)返回 [a, b] 闭区间内的随机整数随机页码、随机年份random.uniform(a, b)返回 [a, b] 之间的浮点数模拟连续数值random.sample(seq, k)不放回地抽取 k 个元素选多个不重复的词random.shuffle(seq)原地打乱序列洗稿、乱序展示random.seed(n)固定随机种子让结果可复现方便测试choice和choices的区别值得单独说。choice进去一个序列出来一个元素所有元素概率相等。choices则支持weights参数比如我想让深夜这种词出现概率是其他词的三倍直接给对应索引传一个权重值就行不需要手动构造重复列表效率高得多。1.3 函数封装的边界为什么必须用 def 包一层你可以不定义函数直接把代码写在脚本顶层跑一遍完事。但那样做的话这个生成器就变成一次性脚本了——换个需求就得改源码别人想调用也得复制粘贴。把核心逻辑塞进def generate_book_title()里本质上是在定义一个可复用的接口。函数化之后带来三个直接好处第一可以传参数控制风格和数量调用方不需要关心内部实现第二可以配合循环批量生成每次调用返回独立结果第三能单独测试出一批结果不好问题能快速定位到词库还是随机策略。我见过不少新手写了好几百行代码从头跑到底中间想临时换个词库得全局搜替换那就是封装边界没设计好。另外一个容易忽略的点def后面的函数体只有在被调用时才执行。你写完def generate_book_title(): ...之后运行脚本如果没写generate_book_title()屏幕上什么都不会出现。这个看似幼稚的坑在实际教学里遇到频率非常高后面排雷章节我还会细说。2. import 语句的机制与 random 库使用要点2.1 一句 import random 背后发生了什么很多人把import random当成一句咒语敲上去就完事。但理解它背后的查找机制能让你以后调试各种 ImportError 快得多。Python 执行import random时大概做了三件事第一步在sys.path列出的路径里找random这个模块。查找顺序大致是已经导入过的模块缓存sys.modules、内置模块、标准库目录、第三方site-packages目录以及当前脚本所在目录。第二步找到模块文件后如果之前没有导入过解释器会编译生成__pycache__缓存文件并在一个新命名空间里执行模块代码。第三步把整个模块对象绑定到当前命名空间的名字random上后面的代码才能通过random.xxx访问它的函数。所以import random的完整含义不是把 random 的内容复制过来而是创建一个叫 random 的名字指向这个模块对象。这解释了为什么你可以import random之后用random.choice、random.randint也可以用from random import choice直接把 choice 函数拉到当前命名空间。import random和from random import choice怎么选我的习惯是如果只是偶尔用一两个函数或者担心命名冲突比如自己也定义了choice变量就用import random保持清晰的前缀如果在一个循环里高频调用需要写很多random.前缀就用from random import choice让代码更紧凑。没有绝对对错但保持一致很重要混着用会让代码很难读。还有个小细节模块导入是有缓存的。同一个模块在一个进程里只会真正执行一次初始化代码之后再次import直接命中sys.modules缓存。这意味着你不用太担心多次导入会重复执行模块里的初始化逻辑。但如果模块里有耗时的顶层代码第一次导入时的卡顿仍然没法避免。2.2 random 库常用函数与几个被忽视的坑前面表格列了一圈函数这里挑几个使用频率最高的展开讲讲。random.choice(seq)的底层实现其实很简单本质是seq[random.randrange(len(seq))]所以它要求传入的序列至少有一个元素空序列会直接抛IndexError: list index out of range。这个错误在做随机生成器时极其常见——词库没初始化好就是空列表一调用就崩。random.seed(n)是调试阶段的好朋友。固定种子之后每次运行脚本生成的随机序列完全一样所以你能稳定复现上一次出现的问题。比如我生成了一百个书名其中一个太难听我怀疑是某个词的问题只要random.seed(42)再跑一次结果顺序不变能精确定位。但要注意正式使用生成器、或者每次启动想得到不同结果时千万别手动调用seed否则所有用户拿到的都是同一批书名体验就很糟。还有一个安全边界问题random模块不适合做密码、token、抽奖这类安全敏感的事。因为它在设计上不是密码学安全的随机数生成器序列被观测到足够多输出之后可能被预测。这类需求请直接使用secrets模块这是安全随机数专用库。我在项目里用过一次random生成抽奖码还好只是内部测试后来赶紧换掉了。3. 手写 generate_book_title从简单到能用的三个版本3.1 第一版三个词库直接拼接先来个最朴素的版本让你建立直观感受。核心思路是准备氛围词和名词两组词库通过random.choice各取一个拼接成书名。import random def generate_book_title(): atmosphere [时光, 深夜, 遗失, 沉默, 短暂, 漫长的] objects [岛屿, 列车, 密码, 雨季, 回声, 信笺] suffix [笔记, 手稿, 告别式, 图鉴, 备忘录] word1 random.choice(atmosphere) word2 random.choice(objects) word3 random.choice(suffix) return f{word1}{word2}的{word3} for _ in range(5): print(generate_book_title())运行一次输出大概是这样的深夜列车的手稿 漫长雨季的告别式 时光岛屿的备忘录 短暂回声的图鉴 沉默密码的笔记第一版能跑但问题很明显结果机械感重风格单一全是XX的XX结构读几遍就腻了。另外f{word1}{word2}的{word3}这类拼接有一个隐藏问题——词库之间的搭配没有语义约束会出现漫长的密码的告别式这种读起来呼吸都很困难的书名。所以第一版只能算让流程通了离能用还差得远。3.2 第二版按风格分库 模板句式 参数控制要解决风格单一和搭配生硬得引入两样东西按风格分类的词库和多种句式模板。风格参数让调用方决定生成什么味道的书名句式模板让结果不再是单一结构。我把词库拆成文艺、悬疑、科幻、治愈四个风格每个风格下配备对应的氛围词、名词和动词短语同时准备了几套句式模板用风格参数去选择。import random def generate_book_title(styleliterary): word_pools { literary: { prefix: [月光, 旧梦, 无名的, 迟到的, 旅人], noun: [河流, 灯塔, 山谷, 白衬衫, 编辑部], verb_phrase: [在夜航中, 向远方, 在雨停之前, 穿过麦田] }, suspense: { prefix: [最后的, 第七封, 消失的, 无人区, 暗处的], noun: [证人, 指纹, 直播间, 档案袋, 地铁末班车], verb_phrase: [没有留下任何痕迹, 在午夜重复发生, 指向一个谎言] }, scifi: { prefix: [量子, 星际, 赛博, 时间之外的, 意识上传], noun: [流浪者, 生态舱, 记忆碎片, 引力波, 休眠仓], verb_phrase: [在曲率航行中醒来, 等待下次跃迁, 发出最后的广播] }, healing: { prefix: [今天也要, 在世界的角落, 慢慢来的, 温柔地, 夏天], noun: [拉面店, 向日葵, 猫, 洗衣房, 便利店], verb_phrase: [重新出发, 好好吃饭, 与自己和解, 抬头看云] } } pool word_pools[style] template random.choice([ f{random.choice(pool[prefix])}{random.choice(pool[noun])}, f{random.choice(pool[prefix])}{random.choice(pool[noun])}{random.choice(pool[verb_phrase])}, f关于{random.choice(pool[noun])}的{random.choice(pool[prefix])}故事, ]) return template for style in [literary, suspense, scifi, healing]: print(f[{style}] {generate_book_title(style)})这个版本把style作为参数传入调用方可以指定风格。加上模板句式的多样性生成的观感比第一版好很多。注意我把random.choice直接写在了template的 f-string 里这是有意为之——每次执行到这一行内部会重新调用随机函数保证模板里的每个空位都独立随机而不是三选一后共用同一个下标。这里也体现了def函数的精髓词库是数据模板是逻辑style参数是接口。两者分离之后扩展新风格只需要往字典里加一组词不需要改逻辑代码。新手可以体会一下这个设计点。3.3 第三版批量生成、去重与异常兜底单条生成好办了下一步是批量。如果只是循环调用generate_book_title可能会出现重复。最直接的方案是引入random.sample——它支持不放回抽取但前提是抽取数量不能超过总体数量。我们的书名是组合式生成总体空间极大理论上不容易重复但为了保险我用集合做去重。def generate_many(n, styleliterary, max_attempts100): results set() attempts 0 while len(results) n and attempts max_attempts: results.add(generate_book_title(stylestyle)) attempts 1 return list(results) titles generate_many(10, stylesuspense) for i, title in enumerate(titles, 1): print(f{i}. {title})max_attempts是防止死循环的兜底参数如果词库规模实在太小生成数量超过组合上限程序反复尝试都找不到新组合必须有一个退出条件。这是一种非常重要的工程意识——任何循环都要问自己如果永远不满足条件程序会不会卡死加上兜底参数等于给程序买了份保险。4. 实战排雷ImportError、相对导入与函数执行问题4.1 最常见的 ImportError 到底怎么定位这个项目写多了之后你大概率会碰到各种 ImportError热搜里那句Importerror: cannot import name transforms from albumentations.augmentati就是典型。这类报错的本质是你想从一个模块里导入某个名字但模块里压根没有这个名字。最常见的原因是版本升级后 API 路径变了或者拼写大小写不对。解决办法不是瞎猜而是打开对应模块的__init__.py或官方文档确认当前版本里正确的导入路径是什么。很多人在这一步会反复试错半小时其实花两分钟看一眼模块源码就能定位。另一类高频报错是attempted relative import with no known parent package。比如你写了一个包内部模块用from . import tools这种相对导入结果你直接用python mypackage/my_module.py运行文件。这时 Python 知道当前文件是my_module.py但不知道它的父包是谁所以相对导入立即失败。解法有两种一种是给包目录补上__init__.py然后把运行方式改成python -m mypackage.my_module另一种是把相对导入改成绝对导入同时设置好PYTHONPATH让项目根目录能被找到。热搜里还有一条file /workspace/src/train.py, line 11, in module from src.config import这类报错通常是脚本在项目子目录里直接运行Python 搜索路径sys.path里没有项目根目录。解法是在运行前设置环境变量export PYTHONPATH/workspace或者在脚本开头手动把项目根目录插入sys.path。在训练脚本、数据处理脚本里这个问题几乎是必踩的值得记下来。4.2 函数相关的三个经典坑第一个坑写了函数但没调用。很多新手写完def generate_book_title():之后疯狂往函数体里加代码然后运行脚本发现什么都没输出就开始怀疑函数写错了。其实def只是定义不会执行函数体。你必须主动加一行generate_book_title()或者把它放进print()里代码才会真正跑起来。第二个坑默认参数用了可变对象。比如def generate_book_title(results[])这种写法第一次调用正常第二次调用时函数内的results列表会带着上一次的数据继续累积。Python 在定义函数时就创建了这个列表对象所有调用共享同一个默认列表。正确写法是def generate_book_title(resultsNone)函数内部再if results is None: results []重新初始化。我在写批量生成器时曾被这个坑坑过一晚上最后查资料才反应过来是默认参数共享惹的祸。第三个坑变量遮蔽。有人会在代码里写random [文艺, 悬疑]这种变量直接覆盖了random模块的名字。之后再用random.choice(...)时Python 找到的其实是那个列表而不是模块于是报AttributeError: list object has no attribute choice。排查方法很简单看变量命名项目里尽量不要用模块名做变量名random、json、sys这些都要避开。4.3 随机结果和可复现性排查如果你发现每次运行生成的书名完全一样别急着怀疑 Python 出了问题先检查代码里是不是调用了random.seed()并且传了固定值。遇到这种情况直接注释掉 seed 或者改成随机种子即可。如果生成结果看起来不够随机比如某些词出现频率明显偏高这不一定说明算法有问题可能是你的词库太短权重太集中。建议做一个简单统计跑一万次生成用collections.Counter统计每个词的出现频率跑完你就能直观看到分布情况。这个方法我特别推荐它能把模糊的感觉不对转化成精确的数字证据排查效率高很多。有一点需要特别提醒random模块的随机数生成器是全局共享的在多线程环境下如果多个线程同时调用可能会有并发问题。对于普通练手脚本无所谓但在高并发的服务端代码里建议为每个线程创建一个独立的random.Random()实例避免共享状态。5. 扩展思路把这个小工具玩成完整应用5.1 用 Counter 统计高频词反向优化词库生成器跑久了你会发现有些词出现频率过高有些词沦为摆设。单纯靠感觉调词库不够准我来分享一个数据驱动的做法批量生成一万条书名统计每个词的出现次数然后针对低频词做替换高频词适当降权。这一步其实是把数据分析引入了日常工具开发和热搜里那句python数据分析与可视化对应上了。from collections import Counter import re titles generate_many(1000, styleliterary) text .join(titles) words re.findall(r[\u4e00-\u9fa5], text) freq Counter(words) print(freq.most_common(15))跑完你会看到哪些词被大量使用哪些词几乎没有机会出场。这种基于真实数据反馈的调整比闭门造车地加词库靠谱得多。朋友圈里我跟好几个写手朋友分享过这个技巧他们说最大的收获不是书名本身而是明白了用数据说话这个东西原来这么有用。5.2 导出到 Excel对接更多工作流光在终端打印书名使用场景有限。把结果导出成 Excel就能让非技术背景的协作伙伴也参与筛选。最简单的方式是用csv模块写 CSV 文件Excel 能直接打开不需要额外装包。如果需要更复杂的格式控制再考虑openpyxl或pandas。import csv def export_titles(filename, titles): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([序号, 书名]) for idx, title in enumerate(titles, 1): writer.writerow([idx, title]) titles generate_many(20, stylehealing) export_titles(book_titles.csv, titles)注意这里用了encodingutf-8-sig这是专门给 Excel 写中文时用的编码直接写utf-8有时候在 Excel 里会乱码。这是我踩过好几次坑之后养成的习惯。导出之后你可以把候选书名发给朋友投票也可以在 Excel 里人工批注修改整个起名流程就从独自头疼变成了协作筛选。5.3 对比经典练手题“李白打酒”说说做小项目的意义网上有很多 Python 经典练手题比如热搜里的李白打酒——壶中原来有酒若干每次遇到酒店就加一倍遇到花就喝一斗经过若干次酒店与花之后酒光了问原有多少酒。这类题目锻炼的是数学建模和循环逻辑非常经典但它的缺点是结果固定做完就完缺少开放性。而这个书名生成器不一样它没有标准答案你调词库、换模板、加风格、优化导出每一步都能看到结果变化立刻获得反馈。我个人的体会是学习编程最快的方式就是做这种有用且开放的小工具。它让你愿意反复改而反复改才是提升代码能力的核心。先把import、def、random玩透再去看数据分析、爬虫、自动化基础会扎实得多。我在实际使用中发现这个书名生成器最有趣的地方不在于它能直接给出一个好书名而是它能逼着你去思考什么样的书名算好。同样是随机组合时光列车很多人喜欢沉默的告别式有人觉得沉重。审美这件事没有标准答案但机器可以把所有可能的组合都翻出来把选择权交还给你。后来我把词库换成了自己素材库里积累的意象词每次开新书都能跑上几轮找灵感。这个项目最大的价值不是那个函数本身有多精巧而是让你把import、def、random这些最基础的东西用得顺手顺手到它们成为你思维的一部分。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑