小叶请AI帮忙整理几条活动须知。程序已经能去掉标题前后的空白,也能给标题加上“提示:”。但把这两段工作接起来时,出现了一个奇怪现象:第一段明明在屏幕上显示了正确标题,第二段却说没有取得可用文字。另一次,他只是把一份列表传进去处理,原来的列表也跟着变了。
这两种情况都与函数的输入输出有关。一个函数里可以发生很多事:取得传入的数据、建立自己的临时名字、计算一个值、修改共享对象、打印内容,以及把结果交还给调用者。它们并不是同一个动作。看见一项动作发生,不能自动认定其他动作也完成。
本章要让小叶能够顺着一次函数调用,看清东西从哪里进入、在哪里处理、从哪里交出。这样阅读AI生成的程序时,他就能判断:这段函数真的提供了下一步所需的东西吗?它是否还改变了调用者原本想保留的资料?几个单独看起来正确的函数,能不能组成一条有效的处理链?
以下使用另一组虚构教学资料,不沿用上一章的运行现场。小叶准备整理三条须知标题,整份列表已经取得,编号互不重复。原文字段只可能是文本或None;None表示还没有取得原文,而只含空白的文本表示已经取得一份内容为空的输入。
| 编号 | 原文 | 当前知道的情况 |
|---|---|---|
| R21 | " 预约须知 " |
有标题,首尾带空白 |
| R22 | None |
原文尚未取得 |
| R23 | " \t " |
输入只有空格和制表符,没有标题文字 |
小叶需要清理首尾空白,把可以排版的标题加上前缀,同时分别保留缺原文、内容空白两种原因。结果暂时只放在内存里;标题文字的事实核验、文件保存和实际发布是另外的工作。这里故意采用很小的处理动作,便于把注意力放在交接关系上。
一、把固定的处理步骤与每次变化的资料分开
函数是一段可以通过名称调用的处理过程。它可以接收参数,执行自己的语句,并把一个结果返回给调用它的地方。先抓住这个具体作用,比把函数理解成“提高代码档次的语法”更有用。
小叶每次整理标题,都要取得一份原文、去掉首尾空白,再把整理结果交给后续。如果把这些语句复制到很多位置,每处都可能略有不同:有的只去掉左侧空白,有的直接覆盖原文,有的只打印出来。后来决定统一处理方式时,还要逐处找齐。
把这项过程写成函数,是把稳定的处理规则集中起来,让每次变化的原文作为输入进入。同一个整理函数可以处理预约须知,也可以处理归还方式。原文换了,并不要求复制一份新的函数;规则变了,也不应依靠修改每一次调用中的细节来维持一致。
这里的“集中”不意味着函数越长越好。若把取得文件、访问网络、清理文本、发消息和统计费用全塞进一个函数,调用者反而很难判断它会产生哪些影响。函数的边界应当对应一项能说清的工作。本章先把标题整理与结果保存分开,前者交出文字,后者决定放在哪里。
也不必把每一行代码都拆成一个新函数。只有一处简单赋值,单独包装后还要跳到别处才能看懂,未必减少理解负担。值得单独形成函数的,常是一个有明确输入与结果、会被调用,或者需要单独检查的处理单元。判断依据是工作关系,不是单纯数行数。
对AI协作而言,这个边界还有一层作用。小叶可以说“请写一个接收原文、返回清理后标题的函数”,然后针对这项结果核对。如果只说“把整个程序做得更规范”,AI即使生成许多函数,也未必把真正需要交接的东西分清。函数数量增加,不会自动带来更明确的职责。
二、定义函数与调用函数,发生在不同的时刻
定义函数是在建立一个以后可以调用的过程;调用函数才让函数体对这次输入执行。小叶先看一个只接受文本的简单版本。这里先演示带有标题文字的输入;None和纯空白文本的处理,后面会分别说明。
def clean_title(raw):
cleaned = raw.strip()
return cleaned
print("整理函数已定义")
title = clean_title(" 预约须知 ")
print(title)
输出依次为“整理函数已定义”和“预约须知”。def引入函数定义,后面是函数名称,以及括号内的参数名raw。冒号之后缩进的两句组成函数体。执行到这个普通定义时,程序先建立函数对象,并把名称clean_title绑定到它;这两句标题处理还没有针对某条原文运行。
真正的处理发生在clean_title(" 预约须知 ")这里。调用先把这份文本交给函数内部的raw,随后执行strip,把清理后的文本绑定到cleaned。遇到return,函数将这个文本交回。外面的赋值再用title接住它。
可以把这次交接看成三个位置:调用处拿出原文,函数内部处理,调用处接收返回值。title不是函数偷偷在外面建立的名字,而是调用者自己选择的接收位置。下一次调用者也可以把同类结果存入另一个变量或字典条目。

图中的返回箭头很重要。函数运行结束以后,程序继续执行调用处后面的安排,并不是从此一直留在函数里。另一方面,只在文件中看到def,也不足以证明这项处理已经发生。若小叶删去末尾的调用赋值和print(title)这两行,只保留函数定义及定义完成的打印说明,程序就只会显示“整理函数已定义”,不会执行标题清理。
函数名称本身与调用也要分开。写下clean_title是在引用这个函数对象;在它后面加上括号并提供相应参数,才构成这里的调用。这个区别在把函数交给其他处理过程时会有用途,本章先记住:有一份处理规则,与已经用它处理某份输入,是两种不同事实。
Python函数教程说明了定义、参数和返回的基本执行关系。本章先使用普通函数体;后面讲默认参数时,还会看到函数定义本身也可能求出一些值,因此不能把“函数体尚未运行”扩大为“定义时绝不会发生任何计算”。
三、参数把调用者的资料接到函数内部
函数定义中的raw称为形式参数,日常通常简称形参;调用时实际交进去的文本称为实际参数,简称实参。两者的关系,可以理解为一个是过程为输入预留的名称,另一个是本次调用真正提供的对象。
小叶不必把外部变量也叫raw。外面可以用original_title保存一份文本,再把它传给clean_title;进入函数后,这次处理通过raw访问它。两个名字可以不同,因为交接靠的是调用时的参数安排,不是靠变量名恰好一样。
函数需要不止一项输入时,还应看清每项的含义。假设小叶另写一个只负责加前缀的函数,text已经是可排版标题,prefix是想显示在前面的提示文字:
def format_title(text, prefix):
return prefix + ":" + text
first = format_title("预约须知", "提示")
second = format_title(prefix="注意", text="归还方式")
print(first)
print(second)
输出“提示:预约须知”和“注意:归还方式”。第一处按位置传入:第一个对象交给text,第二个交给prefix。第二处使用关键字参数,在调用中写明每项交给哪个形参,因此这里可以把prefix写在text前面。
如果把第一处的两个实参对调,程序仍可能运行,因为两项都是文本;但它会拼出“预约须知:提示”。这不是文本类型不合格,而是输入承担的角色放反了。对初学者而言,这类错误比立刻报错更容易遗漏:程序给出了一个合法字符串,却没有完成想要的标题安排。
因此,理解参数时需要同时看类型和含义。两个整数可能分别表示人数与每人份数,两段文本可能分别表示路径与内容。能作为同一语言类型传入,不代表可以互换。关键字调用有时能让角色更容易看见,但名称也需要与实际处理一致,不能把误导性的名字当作正确保证。
输入还可能是表达式的结果。小叶写clean_title(original_title),提供的是调用发生时这个名字指向的对象;若写format_title(clean_title(original_title), "提示"),则先求出整理调用的返回值,再把它作为外层调用的text。内部处理结果需要真的返回,外部才有东西可接。
函数接收参数,并不等于已经检查参数符合要求。上面的format_title把两项当文本相加,若传入None就不能按相同方式拼接。哪些输入被接受、调用者应先保证什么、函数自己检查什么,需要另外说清。后面把缺原文和空白标题分开,正是在建立这样的输入输出约定。
四、打印给人看,与返回给后续用,是两种交付
开篇那个“屏幕显示正确,后面却接不到标题”的问题,常出在把print和return混成一回事。打印把内容送到输出位置,通常供人查看;返回把值交给本次函数调用的使用者,供接下来的表达式或语句使用。
看一个独立例子。这个函数确实整理并显示了标题,却没有写return:
def show_title(raw):
cleaned = raw.strip()
print(cleaned)
received = show_title(" 预约须知 ")
print(received)
屏幕先显示预约须知,再显示None。第一行来自函数内部的print;第二行来自外面的print。函数正常走到末尾而没有显式返回结果时,Python会返回None,因此received接到的是None,不是先前打印的那段标题。
这并不意味着函数没有做事。它做了去空白和打印,只是它没有把整理文本通过返回值交给调用者。如果后续排版需要一个字符串,函数内部已经打印正确,仍然没有满足这项交接要求。继续增加几次print,只会让屏幕多几行,不会把received改成标题。
第二节的clean_title则把cleaned通过return交回。调用者可以选择打印它,也可以把它保存到结果表,或者交给format_title。整理与显示的选择由此分开:同样的整理过程既能服务于屏幕预览,也能服务于批量生成结果,而不必从屏幕文字里再找回数据。
函数也可以同时打印和返回,但应当知道它分别完成什么。例如,为排查问题临时打印正在处理的编号,同时仍返回整理文本,技术上可以成立。只是打印出来的日志不能代替返回值,返回值也不能证明日志已被永久保存。明确两个位置,就不会把调试信息误接到正式结果里。
这里“返回给调用者”也不要求调用者一定接住。写一行clean_title(" 预约须知 ")后不把结果用于任何地方,在普通脚本中不会因为有return就自动打印标题。某些交互环境会显示最后一个表达式的值,那是环境的显示安排,不是return本身变成了print。
使用AI时,可以直接检查这项交接:函数返回什么类型,外面哪一句在使用它,接收位置实际得到什么。比起只看运行窗口有没有出现正确文字,这种检查更能说明几个步骤是否真正连上了。
五、每一条结束路径,都要看它交出了什么
return还有一个与执行过程紧密相关的作用:执行到它时,当前这次函数调用结束,并交回指定结果。它后面的普通语句不会在这次调用中继续顺序执行。因此return放在哪里,会影响哪些资料真正被处理。
先给标题清理增加一个简单约定:输入只可能是文本或None;有可用文字就返回去空白的标题,没有可用文字就返回None。此时“未取得原文”和“取得了空白输入”暂时被合并为同一个返回结果,后面再讨论怎样保留区别。
def usable_title(source):
if source is None:
return None
cleaned = source.strip()
if cleaned == "":
return None
return cleaned
print(usable_title(" 预约须知 "))
print(usable_title(None))
print(usable_title(" \t "))
三次依次输出预约须知、None、None。缺原文时,函数在第一条分支就结束,所以不会继续对None调用strip。取得空白输入时,strip可以正常执行,只是结果为空字符串,函数通过第二个return交回None。普通标题走到最后一条return,才交出清理后的文字。
这种先处理无法继续的情况、再展开正常路径的写法,经常称为提前返回。它的用途是让每条路径的去向清楚,而不是让每个条件后都机械加return。函数是否应该结束,要由本次调用的工作目标决定。
例如,小叶希望整理一整批标题,却把return写在for循环里面,就可能只交出第一条。下面把错误版本和调整后的版本放在同一个独立例子里,输入均为可以清理的文本列表:
def collect_wrong(raw_titles):
result = []
for raw in raw_titles:
result.append(raw.strip())
return result
def collect_all(raw_titles):
result = []
for raw in raw_titles:
result.append(raw.strip())
return result
print(collect_wrong([" 预约须知 ", " 归还方式 "]))
print(collect_wrong([]))
print(collect_all([" 预约须知 ", " 归还方式 "]))
print(collect_all([]))
前两次得到只含预约须知的列表,以及None;后两次得到包含两条标题的列表,以及空列表。错误版本一处理完首条就结束整个函数,第二条没有机会进入处理。空输入时循环一次也不执行,里面的return也到不了,函数走到末尾后返回None。
调整后的return位于循环结束之后,所以先处理完全部标题,再交出列表。即使输入为空,它仍会交出已经建立的空列表。空列表表示“这一批没有结果项”,None则可能表示“这条路径没有提供约定的列表”。如果调用者准备继续遍历结果,这两者就不能不加区分地混用。
这类问题要沿执行路径检查,而不是只搜索函数里有没有return。某个分支有返回,另一个分支可能自然走到末尾;某个return看起来位置接近循环末尾,实际缩进却仍在循环体内。判断标准始终是本次调用完成了哪些工作、从哪条路径结束、交出了什么。
六、函数内部的名字,有自己的使用范围
每次函数调用都会有自己的局部名字。局部变量是绑定在本次调用的局部范围中的名字。本章例子中的形参,以及函数内部赋值建立的名字,都属于局部变量;仅仅在函数体里读取一个名字,并不能据此认定它属于局部。小叶可以把它理解成这次处理过程里的临时记事位置,但不要因此以为所有被这些名字指向的对象都是函数独占的。
先看名字范围。下面是一个独立例子:
prefix = "提示"
def prepare_title(raw):
cleaned = raw.strip()
prefix = "须知"
return prefix + ":" + cleaned
title = prepare_title(" 预约须知 ")
print(title)
print(prefix)
输出为“须知:预约须知”和“提示”。函数内部把局部prefix绑定到须知,用它组成返回文本;外面的prefix仍是提示。这里出现同一个拼写,不代表内外在使用同一份名字绑定。函数内给局部名字重新赋值,没有顺带把外部同名变量也改掉。
cleaned也只在这次调用的局部范围里使用。外部通过title取得了返回文本,却没有因此自动获得一个名叫cleaned的外部变量。在没有其他同名定义的环境里,函数调用后直接写print(cleaned)会遇到名字未定义的问题;这与函数有没有成功返回文本是两件事。
返回文本既然已经交给title,就可以继续使用。不能把“局部名字在调用结束后不能从这里直接访问”,说成“函数一结束,它算出的所有东西都会消失”。对象能否继续被使用,还要看有没有其他引用指向它;返回值正是一种把对象交出去的方式。
同一个函数被调用两次,普通局部变量也分别属于两次调用。第一次的raw可以是预约须知,第二次的raw可以是归还方式。前一次已经返回的文本不会因为后一调用也使用raw这个名字,就自动被后一份文本覆盖。
这使函数能把临时处理收在一个清楚的范围中。调用者不需要知道中间变量叫cleaned还是result,只需要知道接收什么输入、返回什么结果。函数内部换一个临时名称,通常不应迫使所有调用位置跟着修改。
不过,局部名字不是数据隔离的保证。raw可以指向外面传来的对象,另一个局部参数也可能指向共享列表。是否改变原数据,必须继续看函数对那个对象做了什么,而不能只依据它位于def的缩进里面。
七、重新绑定一个名字,与修改共享对象不同
Python调用函数时,会把实参提供的对象交给相应形参访问。可以从“建立一条到这个对象的引用”来理解这次交接。它不会因为进入函数就自动复制整个列表,也不会让局部参数名变成外部变量名的永久别名。
下面的两个函数对同一份标题列表做不同的事。第一个只是让自己的局部titles改指一份新列表;第二个通过titles修改现有列表:
def replace_titles(titles):
titles = ["新的标题"]
def append_title(titles, text):
titles.append(text)
titles = ["预约须知"]
replace_titles(titles)
print(titles)
received = append_title(titles, "归还方式")
print(titles)
print(received)
依次输出只含预约须知的列表、包含预约须知与归还方式的列表,以及None。第一次调用中的赋值改变了局部名字指向哪里,原列表没有被替换;第二次调用的append确实向原列表追加了一项,因此外面也能看见新增内容。
第二个函数没有显式return,received得到None,但它仍然修改了共享列表。这说明函数的影响不只通过返回值发生。对返回之外可观察状态的改变,常称为副作用。这里追加列表就是一种;打印、写文件、发送请求也可以产生各自的外部可观察影响。
副作用并不等于错误。若小叶本来就希望把整理好的标题加入一份共同清单,明确修改这份清单是合理工作。问题在于调用者是否知道它会发生,以及是否与当前目标相符。如果调用者只是想试排一个候选,原清单却被改动,就会影响后续安排。

图的上半部分表示局部参数改指新对象,外面的原引用仍指向旧列表;下半部分表示两处引用还指向同一列表,append改变的正是这个共同对象。判断时应顺着引用看对象,不要把“变量名都叫titles”当作内外联动的原因。
若小叶希望保留原清单,可以让函数建立新列表并返回:
def with_added_title(titles, text):
updated = titles.copy()
updated.append(text)
return updated
original = ["预约须知"]
candidate = with_added_title(original, "归还方式")
print(original)
print(candidate)
现在original仍只含预约须知,candidate包含两条标题。这个例子中的元素都是文本,操作只对新外层列表追加,所以外层复制足以实现目标。若函数还要修改列表内部共享的字典,复制范围就需要另看;不能由本例推出任何复杂对象都会自动隔离。
return本身也不会替人复制对象。若函数直接返回传入的同一个列表,调用者接到的仍可以是原对象;后来经由返回值修改列表,原入口也会看到变化。要判断是不是独立结果,应检查返回的是新建对象还是已有对象,以及后续准备修改哪一层。
关于这些关系,Python参数传递说明从赋值与对象引用解释了局部重绑定、返回结果和修改可变参数的区别。掌握本节两种具体动作,已经足以阅读多数基础处理函数,不必先争论一个没有交代含义的“传值还是传引用”口号。
八、括号里没出现的东西,也可能影响函数结果
局部范围让临时名字更清楚,但函数仍可能读取外部数据。因此,一个函数看起来只有一个参数,不一定只依赖这个参数。小叶判断调用结果时,还要看函数体里是否使用了外面的当前值。
以下独立例子中的render读取外部prefix:
prefix = "提示"
def render(text):
return prefix + ":" + text
first = render("预约须知")
prefix = "注意"
second = render("预约须知")
print(first)
print(second)
两次得到“提示:预约须知”和“注意:预约须知”。函数的text实参一样,结果却不同,因为两次调用读取的外部prefix不同。这里没有随机生成,也没有程序忘记上次规则;只是表面上未写进括号的依赖发生了变化。
把prefix明确列为参数,就能在调用位置看见这种差异。第三节的format_title正采用这种方式:调用者分别交入文本和前缀,函数根据它们组成结果。这样检查一条错误展示文字时,更容易追到是哪次调用提供了哪个前缀。
这并非要求所有外部信息都必须塞进一个很长的参数列表。有的函数合理地使用固定配置,或者由系统约定访问某个存储位置。需要的是让重要依赖可被理解和检查。如果改变外部状态会影响结果,却没有在任何使用说明中体现,调用者就容易把两次不同起点误当成同一输入。
程序中的函数也因此不一定具有“只要这个参数相同,结果就永远相同”的数学函数特征。若它读取时钟、随机源、当前文件或网络回复,结果还依赖那些条件。若它每次都向列表追加内容,即使返回值相同,留下的状态也可能不同。
当一个处理只依赖明确输入、没有修改外部状态时,单独检查通常更容易:小叶拿一份原文进去,就可以比较得到的标题是否符合要求。将这样的计算部分与实际保存、显示等动作适当分开,会让AI生成的程序更容易核对,但不意味着实际应用可以完全没有外部作用。
回到主例,清理与判定函数可以只接收source并返回一个判断结果;批量函数接收records和prefix,负责组织内存结果。取得新资料、补写原文和保存文件分别由调用者或其他步骤安排。这样分开,是为了看清每项依赖和影响具体落在哪里。
九、默认参数还带着定义发生时的信息
默认参数值让调用者可以省略某项输入。例如排版通常使用“提示”作为前缀,就可以把它写成默认值,同时允许个别调用明确改用“注意”。默认值减少重复书写,但仍然参与本次调用的行为。
def format_title(text, prefix="提示"):
return prefix + ":" + text
print(format_title("预约须知"))
print(format_title("归还方式", prefix="注意"))
第一行得到提示:预约须知,第二行得到注意:归还方式。省略prefix时使用默认对象;明确提供prefix时使用这次提供的对象。默认值并不是在文档里写了一句建议,而是程序实际会采用的参数安排。
Python中的默认值在执行函数定义时求出,而不是每次调用时都重新计算一遍。默认是一个不可变文本时,初学者通常不太容易碰到差异;默认是一个会被函数修改的列表时,这个时间关系就会直接影响下一次调用。
看一个独立的有问题的收集函数。小叶希望每次没有提供目标清单时,都得到一份新的单标题清单:
def collect_title(text, collected=[]):
collected.append(text)
return collected
print(collect_title("预约须知"))
print(collect_title("归还方式"))
第一次输出只含预约须知的列表,第二次输出却包含预约须知和归还方式。[]在这次def执行时建立一次;这两次省略collected的调用,使用的是同一个默认列表。第一次追加留下的内容,第二次调用还可以看到。
这与第六节说的“每次调用有自己的局部名字”并不矛盾。每次调用中的collected都属于各自的局部范围,但它们可以先后指向同一个默认对象。名字分别建立,不等于对象分别复制。第七节的引用区别在这里与定义、调用的时间关系接到了一起。
如果目标是每次省略时新建列表,可以用None表示“调用者没有提供清单”,并把新建动作放到函数体内:
def collect_title(text, collected=None):
if collected is None:
collected = []
collected.append(text)
return collected
print(collect_title("预约须知"))
print(collect_title("归还方式"))
shared = []
collect_title("预约须知", shared)
collect_title("归还方式", shared)
print(shared)
前两次分别输出单独的预约须知列表和归还方式列表;最后shared包含两条标题。每次省略参数时,函数内部实际执行一次新建列表;明确传入shared时,则按约定修改这份共同清单。新建与共享分别服务于不同目标,两者都可以有正确用途。
这里用is None检查约定的缺省标记。若改成“清单为空就新建”,函数收到调用者明确提供的空列表时,也会让局部参数改指一份新列表,追加就不会落在调用者那份清单里。空列表已经是一份存在的清单,None在这个函数中才表示需要代为建立清单;不要只凭两者在条件判断中都可表现为假,就把它们当作同一输入。
默认值的更多语法可以之后按需要学习。当前最值得留下的是这个问题:某个值是在定义时建立,还是在每次调用时建立?当一次调用的修改能影响下一次时,顺着这个时间关系检查,往往比单纯增加一个“重置变量”更能找到原因。
十、返回一个值,也可以交出结构清楚的多项信息
第五节的usable_title遇到缺原文或空白输入时,都返回None。如果调用者只问“当前有没有可排版标题”,这个简化可以够用;但主例需要分别列出补原文与检查空白内容的原因,只有None就丢掉了需要的区别。
解决办法不是让调用者再猜一次,而是让返回结果保留它需要的信息。Python函数的一次return可以交出一个字典或元组,里面包含多项内容。“返回一个对象”与“只能表达一项信息”不是同一个限制。
本例使用字典,让字段名称直接说明含义。status表示当前判断,text表示可用时的整理标题;无可用标题时,text为None。原文字段仍按开篇约定,只接收文本或None:
def inspect_title(source):
if source is None:
return {"status": "缺原文", "text": None}
cleaned = source.strip()
if cleaned == "":
return {"status": "空白", "text": None}
return {"status": "可排版", "text": cleaned}
print(inspect_title(" 预约须知 "))
print(inspect_title(None))
print(inspect_title(" \t "))
三次返回的status分别为可排版、缺原文、空白。第一份text为预约须知,后两份text为None。调用者不用把这三个字典看成神秘格式:它们只是把“现在是什么情况”和“有标题时标题是什么”一起交回,让后续可以据此决定去向。
这里选择“可排版”而不是“全部正确”,因为当前函数只检查是否有去空白后仍非空的文字。它没有核对活动地点、日期或其他事实。结果名称若比实际检查强很多,后续就可能把一个局部成功当成完整验收,函数写法再整齐也解决不了这种含义错位。
三个分支都返回相同的字段结构,是为了让调用者能够按统一方式读取。若成功返回文本、失败返回一个字典、缺资料时又什么都不写,调用者就必须先分辨不同类型,再猜它们各自的意思。不是绝对不能返回不同类型,而是这种变化需要明确约定,不能让使用者从偶然结果里反推。
输入输出约定可以先写成一张小表:接受哪些输入,每类输入表示什么,返回字段如何解释,是否修改传入对象。对inspect_title而言,传入的是source字段的文本或None,返回结果有status和text。这次调用不会改写调用者的原记录,也没有进行文件读写。这些已经足够支持本章的小处理链。
这个约定也说明一条边界:若传入一个列表,当前代码并不负责把它自动转成标题。小叶可以在更外层核查输入类型,也可以在需求确实需要时扩充函数。不能一方面只约定文本或None,另一方面因列表没有被智能转换就断言函数必然失效;也不能未经约定就悄悄把任意对象转成字符串,混入展示结果。
结构化返回还有一个实际好处。缺原文的条目可以等待补资料,空白条目可以检查原字段,正常标题继续排版。不同后续动作终于有不同依据,返回值不再只是一个含糊的“失败了”。这是保留信息帮助行动,而不是为了多几个字段显得正式。
十一、把函数接起来,要让前一步结果满足后一步要求
现在把前面的关系用于主例。inspect_title负责判断并清理,format_title负责给已经可用的标题加前缀,build_boards负责遍历整份资料、把可排版结果和需要处理的问题分别汇总。三者有不同输入与输出,却要在同一条数据流上接得起来。
下面给出完整独立程序,重新建立三条原始资料。函数的实现与这次调用所用数据都放在这里,不依赖前面小例留下的名字。build_boards每次建立新的结果表,返回本次完整输入对应的当前结果;它不会向上一轮表格继续追加,也不修改records中的原文。
def inspect_title(source):
if source is None:
return {"status": "缺原文", "text": None}
cleaned = source.strip()
if cleaned == "":
return {"status": "空白", "text": None}
return {"status": "可排版", "text": cleaned}
def format_title(text, prefix="提示"):
return prefix + ":" + text
def build_boards(records, prefix="提示"):
ready = {}
issues = {}
for record in records:
checked = inspect_title(record["source"])
if checked["status"] == "可排版":
ready[record["id"]] = format_title(checked["text"], prefix)
else:
issues[record["id"]] = checked["status"]
return {"ready": ready, "issues": issues}
records = [
{"id": "R21", "source": " 预约须知 "},
{"id": "R22", "source": None},
{"id": "R23", "source": " \t "},
]
first = build_boards(records)
print(first["ready"])
print(first["issues"])
records[1]["source"] = " 归还方式 "
records[2]["source"] = " 开放时间 "
second = build_boards(records)
print(second["ready"])
print(second["issues"])
第一次ready只有R21的提示:预约须知;issues中,R22对应缺原文,R23对应空白。后面两句赋值明确补入了新资料,再次调用后,ready包含三条带前缀的标题,issues成为空字典。新结果来自新输入和新调用,没有靠把旧问题标记改成成功来代替处理。
| 这一轮调用 | 可排版结果 | 仍需处理的问题 |
|---|---|---|
| 原始资料调用 | R21:提示:预约须知 | R22缺原文;R23空白 |
| 补齐两条资料后再调用 | R21:提示:预约须知;R22:提示:归还方式;R23:提示:开放时间 | 无本函数约定的问题 |
沿着R21看一次具体交接:原记录的source传给inspect_title,返回的字典说它可排版,并在text里提供预约须知。build_boards依据status选择路径,把这个text连同prefix交给format_title,再用后者真正返回的字符串填入ready。前一步返回一个字典,不意味着后一步也应该接整个字典;调用者需要取出约定的字段。
沿着R22则是另一条路径。inspect_title返回缺原文和None,build_boards把原因写到issues,没有调用format_title。这个条件保护的实际关系是:排版函数需要可用文本,缺原文时尚无这样的输入。把None强行拼接或只在屏幕上打印“缺原文”,都不能替代把问题原因交到约定位置。
这段组合还说明,调用链是一系列实际的输入输出交接,并不是把几个函数名称写在一起。若inspect_title忘记返回,checked会是None,后续就无法按当前约定读取status;若format_title只打印而不返回,ready里可能存下None。即使屏幕曾出现正确文字,最终结果表也仍不合要求。
再看两次结果的关系。build_boards在每次调用中都新建ready和issues,返回的是这两张新表组成的字典。第二次调用没有修改first里的旧结果;first仍可以用来查看第一轮曾有哪些问题。这里保留的是每轮生成的结果表,不等于自动复制了所有原始资料或保存到磁盘,使用历史时仍要知道究竟保留了什么。
小叶也可以明确传入prefix="注意"来生成另一种展示文字。因为这个输入一路传给format_title,变化会反映在本轮ready中;只修改外面的一个同名变量,不会自动替换这些函数定义里已设置的默认文本。看见参数怎样走过调用链,才能判断某项设置有没有真的接入处理。
十二、用输入、返回与影响,检查AI给出的函数
到这里,小叶不必先把整个程序语言学完,已经可以对AI写出的函数提出一些具体判断。重点不是逐个挑语法名词,而是看一个调用是否完成了它承诺的交接。
首先看这项工作需要什么。对主例,输入不只是“一些标题”,而是完整、编号唯一的记录列表;每条source是文本或None,空白文本与未取得原文不同。prefix表示展示前缀。把这些条件说明,AI才有依据决定哪些情况继续排版、哪些情况返回问题。
然后看结果将怎样被使用。小叶需要一份按编号保存展示文字的ready,以及一份按编号说明原因的issues。若AI只给一段打印日志,日志即使看起来详细,也不能满足调用者读取两张表的要求。若所有问题都返回False,后续也无法区分补资料与检查空白字段。
还要看函数会改变什么。当前build_boards约定每次重建结果、保留输入记录。于是检查时就可以比较调用前后的records,确认原文字段没有被函数顺手替换;也可以检查两次结果是否不合预期地共用某张会继续修改的表。真正准备修改共同记录的函数,则应把这项影响写进职责,而不是假装没有副作用。
阅读一个函数时,可以先看它的名称和参数安排。函数签名概括了参数名称、排列、默认值等调用信息;它能帮助看懂怎样调用,但不会单独说完所有行为。例如build_boards(records, prefix="提示")告诉人有两个输入,却没有完整表达编号唯一、输出字段和是否修改原文。这些仍需要说明或代码证据。
可以在函数开头用文档字符串记录这些含义:它是一段放在函数体开头的字符串,说明用途、输入条件、返回结构和重要影响。文档字符串提供的是使用说明,并不会自动执行字段核验或约束返回值。AI写下“不会修改输入”,仍要看函数体是否真的遵守;写下“返回字典”,也要检查每一条结束路径。
检查几个有代表性的输入,就能看到关键关系是否兑现。给普通标题,应得到可排版的去空白文字;给None,应保留缺原文原因;给纯空白,应保留空白原因;给空记录列表,应得到两张空表。把同一批记录调用两次,如果目标是重建本轮结果,就不应凭空累计上一轮问题。这里的检查来自任务差异,不是随意多换几个数字。
当结果不合用时,修改也应针对造成问题的那项关系。接不到文字,就检查是否实际返回以及调用者读取哪个字段;只处理首条,就检查return与循环的执行位置;原资料被改了,就检查共享对象上的修改;两次无关调用混到一起,就检查默认对象和外部状态;新参数不起作用,就沿调用链看它有没有被继续传入。
这已经是一种能用于实际工作的程序阅读能力。AI可以承担很多语法编写,小叶仍需要知道任务如何划分、值如何交接、判断依据保存在哪里。函数把这些关系集中到一个可检查的范围,前提是人没有把打印、返回、修改和完成混成一件事。
回到开篇,第一种困难要靠返回真实标题并由后续接住来解决;第二种困难要根据目标选择修改共享清单,还是创建新的结果。主例最终得到三条标题,是因为原文已经补齐,调用链实际清理、判断、排版并汇总。理解这些关系以后,下一章再把有关函数组织成模块,复用就有了清楚的基础。
练习与参考分析
练习一:正确文字出现在哪里
小叶写了一个函数,把输入标题去掉首尾空白后打印出来,没有写return。外面用result接收调用结果,再准备把result放入展示表。他认为既然屏幕出现了正确标题,展示表就能取得相同内容。这个判断哪里不充分?如果该函数的用途本来只是显示预览,是否一定要改成返回标题?
**参考分析:**函数中的打印只说明文本送到了输出位置;它正常走到末尾,没有显式返回,调用结果是None。外面的result不会自动收集屏幕文字,因此把它写入展示表可能得到None。需要向后续交出标题时,应让函数返回整理文本,并让调用者使用这个返回值。
若用途确实只是显示预览,不需要向其他步骤提供标题,那么只打印可以符合职责。不能把“没有return”一概判错,也不能把有打印当作已经完成任意结果交付。判断必须与调用者需要什么相连;同样的函数体,在不同使用位置可能有不同评价。
练习二:两次调用之间是谁保留了内容
独立函数把参数collected默认设为空列表,每次向它追加一个标题并返回。小叶连续两次省略collected调用,第二次结果包含第一次标题。他认为局部变量没有在函数结束时清空,所以要在每次return之前执行clear。这个修法能否实现“每次得到一份新的、含当前标题的列表”?
**参考分析:**原因是默认列表在定义时建立,两次省略参数使用了同一个对象,并非局部名字应当靠clear清除。若在return之前清空这个列表,正准备返回的标题也被清掉,调用者得到的就不是目标清单。如果此前还保存着同一列表的引用,那些入口也会看到清空的结果。
更合适的是用None作“未提供清单”的标记,在函数体内发现None时新建列表,再追加并返回。若调用者明确提供共同列表,则是否继续向它追加要按约定说明。修法改变的是对象何时建立、谁共享它,而不是只让最终画面看起来不再累计。
练习三:返回了列表,是否已经得到独立副本
小叶把一份标题列表传给函数,函数直接返回这个参数,没有复制或新建列表。调用者把返回值绑定到candidate,再向candidate追加一条标题。原列表会不会受到影响?若目标是保留原列表,函数怎样调整才足以完成本题要求?本题列表元素都是文本,只需要追加新文本。
**参考分析:**直接返回参数交出的是同一个列表对象。candidate成为另一个访问入口,经由它追加,会修改原入口也指向的列表。return负责交回一个值或对象引用,不负责自动制作副本;换一个接收变量名,也不能改变对象是否共享。
按本题只含文本、只追加外层元素的条件,先复制外层列表,再在新列表追加并返回即可;也可以由调用者明确复制后再交入处理。若后来要求修改内部可变对象,应重新检查复制范围。本题不需要把深复制当作所有情况的默认答案。
练习四:有return为什么还丢了第二条
一个函数先建立result列表,在for循环里逐条整理并追加标题,紧接着就在同一层循环缩进内return result。两条正常输入只得到第一条,空输入则得到None。请分别说明两种结果的实际路径。若目标是无论输入多少条,都返回整理后列表,return应放在哪里?
**参考分析:**两条输入时,第一次循环完成追加就遇到return,整次函数调用结束,第二条没有机会进入。空输入时循环体不运行,里面的return也未执行,函数自然到达末尾而返回None。这是两个不同路径,不能只用“循环漏了一条”概括。
应在循环外、全部遍历完成之后返回已经建立的result。这样非空输入先收齐本批结果,空输入也返回一个有明确含义的空列表。这里移动return的目的,是让交付时点与“完成整批”一致,不是单纯把代码缩进排得好看。
练习五:同样是没有标题,后续动作是否一样
原函数对None和纯空白文本都返回None,调用者只据此把它们列入“暂不能排版”。后来小叶需要分别安排“取得原文”和“检查已经取得却为空的输入”。他能否只看旧返回值确定每条属于哪一种?函数的输出应如何调整?
**参考分析:**旧返回值把两种情况合并了,单看None不能还原此前是哪种输入。若原始资料还保留,可以重新据原字段判断;若只剩旧返回值,就没有足够信息作这个区分,不能替各条猜一个原因。
新的输出可以同时保存状态和文字,例如缺原文、空白、可排版三种status,并在有可用标题时提供text。这样调用者既能汇总可排版结果,又能按不同原因安排后续。旧函数在只问“是否可排版”时可以够用,新目标需要更细的信息;并不是字段越多越好,而是输出要支持下一步实际判断。
练习六:参数出现在调用处,就一定已经生效吗
小叶给批量函数增加prefix参数,希望本轮全部展示文字使用“注意”。调用时确实传了这个参数,但在批量函数内部,把每条标题交给另一个排版函数时仍省略了prefix,后者默认使用“提示”。最终仍显示提示:预约须知。应沿哪项关系查找原因?只把批量函数的参数改名是否能解决?
**参考分析:**外层函数接到了注意,却没有把这个值继续交给内层排版调用。内层没有收到明确前缀,就按自己的默认值产生提示。因此要沿调用链检查参数实际在哪一层被使用,而不是只看最外面的括号里是否出现过它。
应让批量函数把本轮prefix传给排版函数,或者按明确设计在外层完成前缀组合。仅改外层参数名字不会补上缺少的传递动作。若设计本来就要求内层始终使用固定提示,那是另一项职责安排;本题目标已经明确要求注意,应以这个真实要求检验修改是否有效。