小叶用AI写了两个问卷处理程序。一个统计每种选项出现多少次,另一个整理逐条答案,准备交给后续导出步骤。两个程序都需要清理选项文字,于是各自复制了一段函数。后来发现大小写不一致,小叶修好了统计程序,却忘记另一份。统计表已经把yes和YES算作同一类,逐条清单里却仍保留两种写法。
这里的问题不在于小叶不会调用函数。他已经会给函数传入数据,也知道怎样使用返回值。新的困难是:同一项规则分散在几个文件里,改动怎样才能真正到达所有使用它的位置?如果AI把代码拆成了许多文件,怎样知道它们是在协作,还是只是把重复内容藏得更分散?
模块提供了一种把定义放在一个地方、从别处导入使用的办法。它把复用从“再复制一段代码”,推进到“让调用者共同依赖这份实现”。但文件分开以后,名称从哪里来、导入时会运行什么、当前程序用的是哪个版本,也都需要说清。本章围绕这些实际关系展开。
以下是独立的虚构教学资料,不沿用上一章的标题记录。小叶已经取得三条答案,都是文本。本问卷只用YES和NO表示两种选项,前后空白与英文字母大小写不影响选项含义;本组资料没有缺失、额外选项或其他编码形式。
| 原始答案 | 按本问卷约定应整理成 |
|---|---|
" yes " |
"YES" |
"YES" |
"YES" |
" no " |
"NO" |
最终需要两种内存结果:一份统计表,YES两条、NO一条;一份保持原顺序的答案列表,依次为YES、YES、NO。这里的“待导出”只说明后续用途,本章还没有写入文件。我们也不通过这三条虚构资料判断真实问卷质量。
一、复制了函数,不等于以后会共同使用一份规则
先看复制为什么容易产生分歧。假如两个文件都各自定义normalize_option,即使它们今天逐字相同,也只是两份分别保存的源码。小叶在统计程序里改了一份,并不会让另一份文件自动变化。下一次运行两个程序时,各自执行的仍是自己那份实现。
代码很短时,这种差别容易被忽略。去掉首尾空白也许只要一行,统一大写也只多一个处理动作,看起来顺手复制比专门安排文件更省事。可是后续要记住的已经不只是“规则是什么”,还有“规则被复制到哪些位置、哪些已经修改、哪些仍然保留旧版本”。重复带来的负担往往出现在变更时。
模块化首先改变这项维护关系:把共同规则放到一个可明确定位的模块里,两个程序都通过导入取得它。以后修复同一条规则时,修改位置集中。调用者仍保留自己的任务,一个负责计数,一个负责生成列表;共同规则集中,并不要求两个程序最后交出相同形状的结果。
这也说明该复用什么。统计表和逐条清单的工作目标不同,不能只因为都在处理问卷,就把它们强行压成一项含糊的函数。两者真正共同的,是如何把一条原始答案转换成规范选项。先找出这项稳定关系,再决定放到哪里,比先要求AI“拆成五个模块”更容易得到可检查的程序。
一个只用一次、极短且没有独立含义的动作,不一定值得另开文件。拆分之后,读者要来回查找、理解导入名称,也有成本。本章的例子值得拆,是因为两项任务都需要相同的清理规则,而规则变化已经使两份实现产生分歧。拆分回应了正在发生的困难。
对AI协作,可以把要求说成:“把选项规范化放在共同模块中,统计与逐条整理都调用它;保留两项各自的输入输出。”这比只要求“结构规范、易于维护”具体得多。后者很容易得到漂亮的目录,却未必消除导致分歧的重复实现。
二、先建立一个确实能跨文件调用的小工程
在本章最简单的形式中,模块就是一个包含Python定义和语句的.py文件。程序通过导入取得这个模块,再访问其中的函数或其他名称。Python还支持其他形式的模块,我们先用能直接看到内容的源码文件建立理解。
小叶在一个新的survey_demo文件夹里建立三个文件,先保持它们在同一层。下面的目录树表示文件位置,不是需要写入Python的代码:
survey_demo/
choice_rules.py
count_answers.py
prepare_answers.py
这一步先把共同规则集中,暂时保留只去首尾空白的旧实现,好让后面的一次修改有明确起点。choice_rules.py的完整内容为:
def normalize_option(raw):
return raw.strip()
count_answers.py负责汇总,每次调用新建一张统计表。它的完整内容为:
import choice_rules
def count_answers(answers):
counts = {}
for raw in answers:
option = choice_rules.normalize_option(raw)
counts[option] = counts.get(option, 0) + 1
return counts
answers = [" yes ", "YES", " no "]
print(count_answers(answers))
prepare_answers.py不计数,而是保持顺序生成清理后的文本列表:
import choice_rules
def prepare_answers(answers):
prepared = []
for raw in answers:
prepared.append(choice_rules.normalize_option(raw))
return prepared
answers = [" yes ", "YES", " no "]
print(prepare_answers(answers))
两个文件都写了import choice_rules。后面的choice_rules.normalize_option表示从这个模块取得normalize_option函数,再用括号调用。点号在这里指出函数来自哪个模块,调用者不需要把函数源码复制进自己的文件。
小叶在终端进入survey_demo后,分别用能运行本地Python的命令执行两个脚本。这里以python count_answers.py和python prepare_answers.py表示这两次启动,每次都启动一个新的解释器进程。实际环境若使用python3或明确的解释器路径,应把命令开头换成对应入口。
当前统计结果为{'yes': 1, 'YES': 1, 'no': 1},逐条清单为['yes', 'YES', 'no']。这不是本问卷最终需要的结果,因为旧规则还没有统一大小写。可是两个程序已经确实从共同模块取规则,下一步只需要在这个共同位置修复规范化,再检查两个入口。
这里要分开两项成果:共同依赖已经建立,业务规则还未修好。若仅凭目录里多了choice_rules.py就宣布整个问卷处理正确,仍然跳过了实际结果。模块解决的是规则怎样被共同使用,规则本身仍需满足任务要求。

图中两条箭头指向同一份规则源码。它表达的是实现的共同来源;小叶分别启动两个程序时,各自进程中仍有各自加载的模块对象,并不是所有程序都在共享同一块运行内存。后面讨论导入缓存时会用到这个范围。
三、导入方式决定调用处实际得到哪些名字
名称空间可以理解为一组名称及它们所绑定对象的对应关系。模块有自己的名称空间,使用它的程序也有自己的名称空间。分开这些对应关系,可以减少不同文件里恰好同名的变量互相干扰。
在第二节,import choice_rules把模块绑定到当前程序的choice_rules这个名字。normalize_option仍在模块的名称空间中,所以调用时写出模块名再接函数名。当前程序不因此自动多出一个可以直接使用的normalize_option名字。
小叶若只写normalize_option(" yes "),又没有在当前文件定义或导入这个名字,就无法用该写法找到函数。函数已经存在于另一个模块,与当前位置能直接用哪一个名称,是两个相关但不同的问题。错误不能只靠重复执行同样的import来解决。
另一种写法是从模块中直接导入某个名称。下面把两个写法放在一个新的check_imports.py中;它和第二节三个文件位于同一目录,choice_rules此时仍使用只strip的版本:
import choice_rules
from choice_rules import normalize_option
import choice_rules as rules
print(choice_rules.normalize_option(" yes "))
print(normalize_option(" yes "))
print(rules.normalize_option(" yes "))
三行都显示yes。第一行通过完整模块名访问;第二行使用直接导入的函数名;第三行使用别名rules访问同一个模块。as改变的是本文件用来访问对象的名称,没有因此建立一份独立规则,也没有把源码文件改名。
若某个文件只写from choice_rules import normalize_option,它取得的是normalize_option这个名称,并不会顺带在该文件建立choice_rules这个模块名。上面的check_imports两者都能用,是因为它分别执行了两种导入,不能把这个组合例误读为任何一种写法都会提供全部名称。
使用哪种写法,应看阅读时需要什么信息。模块名能让来源显眼,读到rules.normalize_option就知道这是共同规则;直接函数名较短,适合名称含义清楚、来源不容易混淆的地方。不存在靠某一种拼写就保证代码结构良好的办法。
from choice_rules import *这种星号导入会把一批名称带进当前位置。它可能让读者难以追踪某个名字从哪里来,也可能与本文件已有名称冲突。初学时优先写明要用的模块或名称,通常能减少这种追踪负担;这里不需要先记住星号导入的全部细节。
对AI生成的多文件代码,导入部分是一张很实用的线索表。某函数报名字未定义时,可以先核对它实际在哪里定义、当前位置导入的是模块还是函数、调用拼写是否对应。不要急着让AI在当前位置再复制一个同名函数,否则很可能把原先想消除的重复重新引进来。
四、导入模块,也会执行模块顶层的语句
把函数放进文件以后,小叶可能顺手在文件末尾加一行演示,看看清理结果。这里容易出现一个误会:以为只有直接运行文件时才会执行这些语句,别的程序import它只会取得函数名称。
实际上,一个普通模块第一次被导入时,Python需要执行它的顶层代码,才能建立函数定义、变量等内容。这里的顶层是指直接位于模块中的语句,不在某个函数体内。函数定义本身会被执行以建立函数对象,函数体则仍要等到实际调用才运行。
用一个独立的import_demo目录观察。它只放下面两个文件,不替换survey_demo里的共同规则。demo_rules.py为:
print("正在加载规则模块")
def normalize_option(raw):
return raw.strip().upper()
print(normalize_option(" yes "))
入口observe_import.py为:
print("入口准备导入")
import demo_rules
print("入口导入完成")
从这个目录新启动python observe_import.py,屏幕依次显示:入口准备导入、正在加载规则模块、YES、入口导入完成。中间两项不是入口显式调用normalize_option产生的,而是导入时执行了demo_rules的两条顶层打印语句;其中后一条打印还先调用了整理函数。
因此,“没有在入口文件里写函数调用”并不足以说明某项处理没发生。入口导入了一个模块,而那个模块的顶层语句可以继续调用函数。追踪执行过程时,要把这段导入期间发生的工作也算进去,才能解释为什么一启动就出现额外输出。
演示打印看起来只是多了几行文字。如果顶层代码换成读取用户输入、启动一批任务或写入结果,导入就可能在调用者还没准备好时触发这些动作。小叶想复用清理规则,并不一定想顺便运行模块作者留下的整套试跑安排。
这不是说模块顶层不该有任何语句。定义函数、建立固定常量、导入所需模块,本来就属于常见的准备工作。需要分开的是“使用这个模块所需的准备”和“只有主动启动本次任务时才想做的事”。前者可以属于导入,后者应当有明确入口。
五、让直接试跑与被别人导入,各有清楚的入口
Python为模块提供一个名叫__name__的特殊变量。一个文件作为普通模块被导入时,它通常取模块名称;这个文件作为本次程序的入口直接运行时,它取"__main__"。小叶可以据此决定是否执行末尾的演示任务。
仍在刚才的独立import_demo中,把demo_rules.py完整改成以下内容,再启动一个新的进程观察:
def normalize_option(raw):
return raw.strip().upper()
def main():
print("正在试跑规则")
print(normalize_option(" yes "))
if __name__ == "__main__":
main()
现在直接运行python demo_rules.py,会显示正在试跑规则和YES。执行到末尾条件时,__name__为"__main__",所以调用main。main只是这里为入口处理选择的普通函数名,不是因为名字叫main就会自动启动;真正启动它的是条件内的那次调用。
再新启动python observe_import.py,屏幕只有入口准备导入、入口导入完成。demo_rules仍会建立两个函数,但此时它的__name__是"demo_rules",末尾条件不成立,试跑就没有发生。导入者已经可以使用规范化函数,无需先承受一轮演示输出。
这段写法常称为入口保护。它保护的是条件块里安排的动作,不是给整个文件套上一层“导入时绝不执行任何事”的屏障。假如小叶又在条件块外写一条print,那条顶层print仍会在首次普通导入时执行。应检查语句实际放在哪里,而不只是搜索文件中有没有__main__。

图中两种方式都会执行定义,差别落在末尾条件是否调用main。这比记住一段神秘的固定模板更有用:如果AI把读取资料和批量处理放在保护块外,即使末尾保留了这段模板,也没有保护到真正想隔离的工作。
对于主工程,count_answers.py和prepare_answers.py也可能被其他程序导入。后面会把它们自己的示例数据与打印放进入口保护,使计数函数和逐条整理函数也能被进一步复用。复用关系可以向上延伸,不必把“模块”和“能直接运行的脚本”看成永久互斥的文件种类。
Python的入口说明详细解释了这个名称与执行环境的关系。本章先用直接运行.py文件和普通导入两种情形,不需要为了理解这两条路径而展开所有打包入口机制。
六、同一进程里的重复导入,通常使用已经加载的模块
入口保护解决了是否主动试跑的问题。另一个常见误解是把import当作“重新执行一遍文件”或“把模块恢复到最初状态”的命令。对普通导入而言,同一解释器进程中已经加载的同名模块通常会从缓存取得,不会每次重新执行顶层语句。
这里的缓存,就是保存本进程已经加载的模块对象,供后续同名导入继续使用。它让不同位置可以接到同一个模块,也使模块里保存的可变状态可能被这些位置共同看到。这个范围是当前进程,不是电脑上所有Python程序共享一份对象。
再建立一个独立cache_demo目录。audit_state.py只保存一份事件清单并打印加载提示:
print("事件模块已初始化")
events = []
同目录check_cache.py的完整内容为:
import audit_state
audit_state.events.append("收到第一批答案")
import audit_state as again
print(audit_state is again)
print(again.events)
新启动check_cache.py后,初始化提示只出现一次,随后显示True和['收到第一批答案']。is在这里检查两个名称是否指向同一个对象。第二次import给这个已加载模块增加了一个本地访问名称again,没有重新建立events空表,先前追加的事件仍然存在。
如果小叶原本想清空一批任务记录,重新写一行import就没有实现这个目标。需要明确重建本轮结果,或调用按约定重置状态的处理;如果这份记录本来就是多处共同维护的事件清单,则保留它也可能正合用途。判断依旧回到当前工作想保存什么,而不是把缓存简单说成好或坏。
再把check_cache.py作为一个全新进程启动,它会重新加载audit_state,重新建立自己的events,然后执行本次append。因而这次屏幕仍是同样三项,不会因为上一个已结束的进程加过一条,就自动累计成两条。模块对象的内存状态与磁盘上的持久记录不是一回事。
本节说的是普通同名导入的常见路径。Python另有重新加载或改变导入缓存的机制,但它们有自己的使用条件。现在不必用这些机制记忆所有例外;只需先知道:重复import不是可靠的任务重置动作,也不是向所有进程广播一项修改。
七、改了磁盘上的文件,正在运行的程序未必已换规则
有了缓存的理解,再回主工程修正大小写。小叶把survey_demo中的choice_rules.py改为:
def normalize_option(raw):
return raw.strip().upper()
strip负责去掉首尾空白,upper在本题这些英文选项上把字母统一成大写。现在一条yes与一条YES会得到相同的规范文字,可以归到同一个统计键下。这个转换符合开篇给出的问卷约定;不能未经确认便把任何任务中大小写不同的文字都视为同义。
修改以后,小叶结束先前的演示进程,再分别新启动count_answers.py和prepare_answers.py。新进程从这个目录加载已保存的共同规则,统计结果成为{'YES': 2, 'NO': 1},逐条结果成为['YES', 'YES', 'NO']。两个入口的代码都没有复制新规则,只是继续调用共同模块。
这个变化才完成了开篇的修复:统计和逐条结果遵守同一个规范化约定。它不是靠让两张结果长得一样,而是让两种任务对“这条答案属于哪个选项”作相同判断。计数仍交出数量,逐条整理仍保留原顺序和逐项内容。
如果小叶在一个一直没关闭的交互解释器里,早已import过choice_rules,仅仅保存磁盘文件后再次import,不应期待旧模块必然自动变成新实现。当前进程仍可能使用已加载的函数对象;磁盘文件已改、编辑器已显示新代码,与本次调用实际用上新代码,是不同事实。
本章的小实验采用明确的处理方式:保存文件,结束旧解释器或旧程序,重新启动,再运行同样样本。这样可以把“新源码有没有生效”与“规则改得对不对”分别核对。没有必要一开始就把重新加载的细节加入所有示例。
重新启动之后仍得到旧结果,也不能立刻断言Python缓存有问题。还可能启动了另一份工程、导入了另一个目录下的同名模块,或者编辑器里的改动没有保存。接下来的来源检查,就是为了把这些不同原因分开,而不是一遇到不一致就反复修改相同函数。
共同规则的修复也不会自动重写已经生成的旧结果。假如小叶把上一轮统计字典保存在某个变量里,新函数不会主动找出它再改一遍;必须用所需输入重新生成结果。模块复用让后续调用有共同来源,旧结果是否重算,仍由当前任务决定。
八、直接导入的名称,不是持续跟随重绑定的窗口
第三节里,import模块、直接导入函数和使用别名都能成功调用。但遇到模块内的名称后来被重新赋值时,这些访问方式可能表现不同。理解这种区别,可以少一些“明明已经更新设置,为什么这里还是旧值”的困惑。
用一个独立settings_demo目录观察,不改主工程。settings.py中放一段展示标签和一个列表:
label = "初稿"
notes = []
check_settings.py为:
import settings
from settings import label, notes
settings.label = "复核稿"
settings.notes.append("已完成一次检查")
print(label)
print(settings.label)
print(notes)
输出依次是初稿、复核稿,以及包含已完成一次检查的列表。直接导入label时,本地label取得当时那份文本。后来settings.label改指另一份文本,本地label并没有因为名字相同而跟着重新绑定。通过settings.label访问,才会在此时读模块里的那个名称。
notes的情况不同。直接导入以后,本地notes与settings.notes仍指向同一个列表;append修改的正是共同对象,所以本地notes也看见新增内容。不能把这个例子概括成“from导入会复制数据”,也不能概括成“from导入总能实时同步变化”。需要分别看名称重新绑定与对象内部修改。
这与上一章函数参数的关系是连着的。一个对象可以有多个访问名称,修改共同对象会被其他入口看到;让其中一个名称改指别的对象,不会自动重绑所有名称。模块只是把这个关系延伸到了文件之间,并没有另创造一套相反的变量规则。
如果某项配置确实需要在运行中更新,让使用者通过模块名称读取当前配置,或者通过一个明确的查询函数取得当前值,通常更容易说明行为。若希望一次任务始终使用启动时取得的设置,则把当时的值明确传入也可能合适。先决定设置要在哪个时点生效,再选择访问方式。
本章的问卷规范化函数不需要反复改变全局配置,因此主工程保持简单的参数输入和返回值即可。这里增加独立小例,是为了识别AI生成代码里的跨模块状态,而不是要求每个小项目都建立一套配置管理系统。
九、导入成功,还要知道导入的是哪一份文件
当程序执行import时,它需要按导入规则找到相应模块。对本章放在同目录的普通源码文件,直接启动脚本时,脚本所在目录通常就在搜索路径前部,因此可以找到旁边的choice_rules.py。这也是第二节先让三个文件位于同层的原因。
模块搜索路径是一组用来查找模块的位置。Python还可能从内置模块、标准库和已安装包的位置取得代码。这里不需要背诵所有导入步骤,但要知道“电脑某处存在一个文件”,并不等于当前解释器能用这个模块名找到它。
小叶把choice_rules.py随手移到另一个不在搜索位置的文件夹,而没有调整工程安排,原来的import就可能找不到模块。反过来,如果几个搜索位置都有同名文件,程序可能取得比他预想更早找到的那一份。模块名不是整个硬盘上的唯一文件标识。
在主工程中,可以临时加入一个check_location.py来查看实际来源:
import sys
import choice_rules
print(sys.executable)
print(choice_rules.__file__)
第一行输出当前解释器的位置,第二行输出本例这个源码模块关联的文件路径。不同电脑路径会不同,重点是检查它是否属于正在编辑的那份工程,以及解释器是否就是准备运行项目的那个。不能把截图里的某条路径抄成所有人的标准结果。
如果第二行指向另一个survey_demo副本,继续修改眼前文件并不能改变那次调用的实现。此时应先对齐实际启动位置和导入来源,再核对规则。若模块名拼错导致ModuleNotFoundError,则先核对导入名称与文件位置;错误名称表示查找没有成功,不自动意味着必须上网安装一个同名包。
本地文件也可能遮住预期使用的标准库。例如后面要用collections模块,若把自己的练习脚本命名为collections.py,就可能让正常的名称查找先遇到这份本地文件。改一个不冲突的文件名并重新启动,比反复安装标准库更能对应这类原因。
这类检查不要求小叶立刻掌握复杂环境配置。先能回答“我启动的是哪一个Python、实际加载的是哪一份模块”,就已经能区分多种表面相似的故障。Python模块教程给出了搜索路径与本地同名遮蔽的说明,进一步定位时再按需要展开。
十、依赖关系清楚,模块才能顺利组合
当一个模块使用另一个模块提供的功能时,就形成一项依赖关系。比如count_answers需要choice_rules来确定每条答案的规范文字,prepare_answers也需要这份规则。依赖箭头表示“这一处工作需要那一处提供内容”,不只是目录图上看起来相连。
主工程的关系相对清楚:两个上层入口都依赖共同规则,而共同规则不需要知道哪一个入口正在调用它。它接收一条文本并返回规范文字,不需要回头导入统计表,也不需要先执行待导出任务。这使它更容易被另一个新入口使用。
假如统计模块为了复用一些东西导入整理模块,整理模块又导入统计模块,就出现循环导入。循环并非在所有安排下都会立即出错,但初始化尚未完成时,互相读取还没建立的名称,很容易使启动失败。对初学者而言,先找清共同内容是否放错位置,通常比硬调导入顺序更有用。
下面是一个独立cycle_demo里的最小失败例,不要把它加进主工程。文件a.py先导入b中的名称,再定义自己的normalize:
from b import build
def normalize(raw):
return raw.strip()
文件b.py则先要求从a取得normalize:
from a import normalize
def build(raw):
return normalize(raw)
在这个目录的新解释器中执行import a,a开始初始化,第一句要求导入b;b初始化的第一句又要取得a.normalize。可是a还没有执行到normalize的定义,所需名称尚未建立,所以这次导入失败。文件中以后写着一个函数,与当前时点已经能够取得这个函数,仍是两件事。
如果normalize是共同的底层处理,可以把它放入独立rules.py,让a与b按实际职责分别依赖rules,减少彼此初始化时的要求。这里“抽出共同内容”改变的是依赖方向,确实解决了需要对方尚未提供名称的困难;不是把文件再拆小一点就一定有效。
也有一些场景可以通过调整调用时点或局部导入解决具体循环,但那需要了解更完整的运行关系。本章先建立能检查的基本安排:共同规则尽量独立于某个具体入口;上层流程通过参数和返回值组合它。系统规模更大时怎样划分模块,软件工程课程还会进一步讨论。
十一、文件增多以后,用包组织模块的位置
主工程只有三个文件,放在同一层已经够用。如果小叶以后又有选项规则、日期整理、文字校验等多个模块,全部堆在一层,名称与用途会越来越难找。这时可以用包把相关模块组织在一个共同名称之下。
Python包支持用带点号的模块名称表达层次。例如survey_tools.choice_rules,表示从survey_tools这组模块中定位choice_rules。先把它理解成程序能够识别的一种组织关系,而不只是文件夹换了一个名称。
下面另外建立package_demo,演示一种普通包结构。为避免两个副本同时参与本次搜索,这个目录中不再放同层的choice_rules.py:
package_demo/
check_package.py
survey_tools/
__init__.py
choice_rules.py
本例的__init__.py是空文件,survey_tools/choice_rules.py包含第七节那个去空白并转大写的函数。check_package.py完整内容为:
from survey_tools import choice_rules
print(choice_rules.normalize_option(" no "))
从package_demo启动这个脚本,得到NO。调用者通过包的位置取得模块,模块仍通过相同函数完成规范化。把文件放进包,没有自动改变函数接受什么或返回什么;变化的是代码怎样组织、怎样被定位和导入。
在这种普通包结构里,__init__.py可以是空的,也可以放初始化语句。若写入会执行的顶层动作,导入包时仍需要考虑它们的影响,不能以为换成包就绕开了前面的入口问题。Python还有不采用这个文件的命名空间包等机制;当前示例只需要这种明确、常见的小目录结构。
包内模块之间可以按相应规则互相导入,较大项目还会用到相对导入。这里先不铺开所有点号语法。小叶首先应能看懂目录与导入名称怎样对应,并确认运行入口所在的位置;这已经足以把同一项规则整理进一组相关工具,而不需要先完成打包分发课程。
重要的仍是职责,不是文件夹数量。把互相纠缠的函数原封不动搬进不同子目录,并不会自动形成清晰依赖;把原本简单的一项调用绕过许多无实际作用的中间文件,也可能增加理解成本。目录应帮助读者找到共同实现,并看清谁使用它。
十二、使用现成模块,要区分功能复用与环境可用性
复用不只发生在小叶自己写的代码之间。Python标准库已经提供许多常用功能,第三方库还会提供更专门的能力。理解import以后,读到AI生成代码中的外部名称,就可以继续问:这是当前解释器自带的内容、项目自己的文件,还是需要另行安装的依赖?
标准库是随Python提供的一组模块。比如collections中的Counter可以统计一组元素出现的次数。在一个新的小脚本中,使用已经规范化的三条文本:
from collections import Counter
prepared = ["YES", "YES", "NO"]
counts = Counter(prepared)
print(counts["YES"])
print(counts["NO"])
两行分别显示2和1。这里使用现成的计数能力,省去了逐项维护计数字典的代码。不过Counter不会替小叶判断yes与YES在本问卷里是否同义;如果把未规范化的三条文本直接交入,它会按各个实际字符串分别计数。领域规则仍需要由任务约定提供。
Counter官方说明给出了它如何把元素与次数对应。本章只使用最基本的计数,不要求在开始写问卷程序前先学完这个容器的所有操作。现成模块的价值,是接过已经有明确输入输出的通用工作。
第三方依赖则是项目所需、但不是当前Python标准库一部分的软件包。通常需要在所用环境中安装,之后才能按它提供的模块名导入。import主要负责取得当前环境中可找到的代码,不能把一行import理解成自动去网络下载并安装一切所需软件。
这里的运行环境至少包括正在使用的Python解释器,以及它能访问的安装位置。电脑里可能有不止一个Python;在其中一个环境成功安装包,不保证另一个解释器也能找到它。因此前面的sys.executable不仅帮助确认所用解释器,也能帮助排查“已经安装却仍然找不到”的情况。
虚拟环境允许不同项目分别管理第三方包,使这个项目的依赖安装与另一个项目分开安排。小叶不必因为接触模块就立刻搭建复杂基础设施,但在真实项目使用第三方包时,应知道这个包安装在哪个环境里。
Python打包指南说明了pip安装与虚拟环境的对应关系。具体安装命令、包名及版本,应根据项目实际需要和该包文档确定;不是每出现一次ModuleNotFoundError就随手安装同名软件,也不是把一个项目能运行的环境配置直接推广到所有项目。
当AI建议新增依赖时,可以先让它说明承担哪一项工作、标准库或现有模块能否完成、调用者需要怎样接收结果。若只是为了统计这三条答案,引入庞大的外部服务就没有解决新的必要困难。模块复用应该减少重复劳动与维护负担,判断仍要落在实际任务上。
十三、用两个入口的结果,确认一次共同修改真的完成
现在回到survey_demo,把本章建立的关系放在一起。choice_rules.py已采用第七节的规范化规则。两个业务函数继续调用它,不再各自保存一份normalize_option。接下来把两个入口的演示部分也放进保护块,方便别人导入这些业务函数。
count_answers.py保留原有import与count_answers定义,只把文件末尾的示例两行改为:
if __name__ == "__main__":
answers = [" yes ", "YES", " no "]
print(count_answers(answers))
prepare_answers.py同样保留原有import与prepare_answers定义,把末尾改为:
if __name__ == "__main__":
answers = [" yes ", "YES", " no "]
print(prepare_answers(answers))
这里两个代码块只是各文件末尾的替换部分,不是可单独运行的完整文件。前面的函数定义与共同模块仍需保留。明确修改范围,可以避免“把保护块复制过去了,却把要调用的函数删掉”这种与真正目的无关的故障。
现在分别新启动两个脚本,统计仍为YES两条、NO一条,逐条列表仍为YES、YES、NO。再在同一目录的新解释器里仅导入count_answers和prepare_answers,不会自动打印这两份示例结果;需要它们的调用者可以自行提供答案并取得返回值。
小叶可以从三个层次确认这次修复。先看输入约定,三条答案的大小写与首尾空白确实属于允许规范化的变化;再看依赖,两个业务函数实际调用的是同一项目中的choice_rules;最后看运行,已保存新实现、重新启动正确入口,两个结果都符合各自要求。缺少任何一层,都可能得到一个“代码看起来改了,实际工作却未一致”的状态。
如果以后两个任务对规则的要求真的分开了,例如一个需要保留原始大小写作为记录,另一个需要统一选项用于计数,就不能再把不同目标当成重复错误。可以保留原文,再明确产生规范化字段,让各项任务选用相应内容。共同模块应当承载真正共同的规则,不能为了维持复用而悄悄抹掉有价值的区别。
读AI生成的多文件项目时,不必首先统计它拆了多少层。先从一个入口跟进:导入哪个模块,模块初始化时做什么,取得哪项函数,函数的返回值到哪里;修改以后再看这次运行使用了哪份来源。这样的追踪会让目录与代码成为实际工作关系,而不是一串需要背熟的文件名。
本章的问卷问题已经得到具体解决:规则集中在choice_rules,统计与逐条整理共同使用;保存并重新启动后,大小写归并同时进入两种结果;导入业务函数时也不再自动触发示例任务。下一章再把内存里的结果保存成文件,就可以在这个清楚的调用基础上继续安排数据流动。
练习与参考分析
练习一:文件名字一样,为什么修复没有同步
小叶在两个互不依赖的项目文件夹里各放了一份choice_rules.py。两份文件今天内容完全相同。以后他只修改第一份,第二个项目的新进程仍使用第二份文件。能否因为两份模块名相同,就说它们已经实现共同规则复用?要检查什么,才能知道第二个项目为什么没有更新?
**参考分析:**相同模块名没有把两份源码连接成同一维护对象。两个项目分别定位各自目录里的文件,第一份磁盘内容改变,不会自动写入另一份。应该核对第二个项目实际使用的解释器、启动入口和模块文件路径,再确认它是否确实依赖预期的共同实现。
如果目标是共同维护,需要建立明确的共享来源或相应的分发更新安排,而不是只统一名称。若两个项目本来要独立演进,保留两份也可能合理,但应承认它们的修改需要分别管理。判断依据是实际依赖与更新方式,不是文件名看起来相同。
练习二:写了入口保护,为什么导入时仍然提问
一个模块在顶层先执行input,询问使用者要处理哪份资料,然后定义几个函数,最后才写if __name__ == "__main__": main()。小叶从另一个程序导入它时,仍被要求输入。他认为入口保护失效了。应怎样解释这个现象,又怎样把询问资料这项动作放到合适的位置?
**参考分析:**保护块只控制缩进在它里面的动作。前面的input是独立顶层语句,第一次普通导入执行到那里仍会提问,与末尾条件是否成立没有矛盾。应把仅直接启动任务时需要的询问放入main或其他明确入口函数,再由保护块调用;共同处理函数则接收已经准备好的资料参数。
修复后还要沿导入路径检查,是否有其他被导入模块也在顶层询问。目的不是让文件中出现一段标准模板,而是让复用者能够取得功能,并自行决定何时启动有交互的任务。必要的定义与固定初始化可以保留在顶层。
练习三:再次import能不能取到新代码
小叶在交互解释器中导入choice_rules并运行过一次。随后在编辑器里保存了新规则,但没有关闭解释器,只再次执行相同import,再调用函数。他仍得到旧结果。是否已经足以证明新规则写错了?请给出一种适合本章小工程、能分别检查来源和规则的方法。
**参考分析:**还不能据此判断新源码的逻辑。普通同名导入可能直接使用本进程已有的模块对象,没有重新执行磁盘上的新文件。先确认改动已经保存,记录实际模块路径;结束旧解释器,重新从预期工程启动,再核对来源并用同样输入调用。这样才能让新规则真正参与本轮检查。
若新进程仍取得旧结果,要继续分辨是否启动了另一份工程、导入来源是否正确,以及规则本身是否满足输入约定。重新启动是一种明确更新运行起点的方法,不是能修复所有逻辑错误的万能动作;它也不会自动修改已保存的历史结果。
练习四:一个设置变了,另一个名字为何没有变
独立模块settings包含文本label与列表notes。调用者执行from settings import label, notes以后,另一处代码把settings.label重新赋为新文本,并向settings.notes追加一项。本地label与notes一定都保持原样,或者一定都跟随更新吗?请分别说明。
**参考分析:**不能用一个“同步或不同步”的结论覆盖两种动作。模块里的label重新绑定到新文本,没有重绑调用者此前取得的本地label,所以本地名仍指向旧文本。调用者的notes与settings模块中的notes则仍指向同一列表,向这个列表追加会从两处都被看见。
这不表示from自动复制所有对象,也不表示每个导入名都持续查询模块当前绑定。若目标要求每次读取当前设置,应选择能体现这个时点的访问方式;若目标要求任务保留启动时的文本值,也可以明确使用已取得的值。共享状态的安排应服务于任务需要。
练习五:把共同函数再复制一次能否解决循环
两个业务模块互相导入,启动时其中一方要求取得另一方尚未定义的共同清理函数,因而失败。AI建议把这个函数复制进两个模块,再删掉相应互相导入。这个改法可能让眼前程序启动,但对“以后共同维护一份规则”的目标有什么影响?本章还提供了哪种安排?
**参考分析:**复制可以切断当前这项导入要求,但同时重新建立了两份需要同步维护的规则。它是否可接受,要看是否愿意承担这个后果;不能只凭启动成功就宣称原复用目标仍然完整实现。若函数确实属于共同底层逻辑,可以把它放入独立规则模块,让两个业务模块分别依赖它。
这项调整需要看实际内容:共同规则不能又回头依赖两个业务模块中尚未初始化的结果,否则换了文件名仍可能保留原来的循环。应通过参数和返回值交接业务资料,使共同处理能在明确输入下工作,而不是靠复制或随意移动import掩盖依赖关系。
练习六:现成计数模块能否代替业务规则
小叶把[" yes ", "YES", " no "]直接交给Counter,然后据此统计问卷选项。他认为既然使用了标准库,就不再需要自己的规范化模块。这个结论为什么不充分?如果任务确实要求保留每条原始写法,又该怎样看待原始文本和规范选项的关系?
**参考分析:**Counter能正确统计它实际收到的元素,但这三种原始字符串彼此不同。标准库没有被告知本问卷允许忽略大小写及首尾空白,不能自动替小叶建立这样的等价关系。需要先按明确规则产生规范选项,再对规范选项计数,才能得到YES两条、NO一条。
若同时需要保留原始写法,可以保存原文并另生成规范化值,不必为了计数覆盖原记录。原文用于追溯,规范值用于当前分类,两项作用可以同时成立。使用现成模块接过通用计数,与由人明确业务含义,是能够配合的两项工作。