这一篇是 Python 习题笔记的第三篇,对应习题集里的「函数部分」和与之配套的文件处理题。前两篇讲的是程序结构与数据结构,这一篇把两者合起来用:用函数把一段逻辑装起来,让它既处理键盘输入,也处理文件里的数据。
我把这一页的题按知识点重新归了类,一共五组:字符串逐字符分类统计 → 素数判断 → 文件读取与成绩汇总 → 阶乘的迭代与递归 → 有序表的插入维护。顺序大致是从「单层循环」走到「函数返回值与递归」,最后落到「列表的原地修改」。每一组都给出题目、思路、参考代码和易错点,代码是从扫描件里抄下来并修掉了明显笔误的版本,可以直接运行。
本系列导航
- 01 程序结构与算法:条件判断、循环与入门经典题
- 02 文件读写与列表处理:气温统计、成绩分档、函数封装与排序
- 03 函数封装与文件处理:字符统计、素数判断、成绩汇总与有序表插入(本篇)
一、一行字符的分类统计:三套等价写法
题目:从键盘输入一行字符,分别统计出其中英文字母、空格、数字和其他字符的个数。
思路:这是一道典型的「遍历 + 多分支计数」题,骨架只有四步:初始化四个计数器、拿到一个可迭代对象、逐个字符判断它属于哪一类、最后统一输出。判断字符类别有两条路:
- 手写区间比较:靠
'A' <= ch <= 'Z'之类的比较判断,好处是不依赖任何方法,坏处是容易漏掉大写或小写其中一支; - 用字符串自带方法:
isalpha()判字母、isspace()判空白、isdigit()判数字,可读性最好,也是实际写代码时该用的写法。
遍历方式也有两种:for i in range(len(s))拿到下标再取s[i],或者直接for ch in s拿到字符本身。后者更 Pythonic,因为没有必要为了取字符而先去造一个下标。
| 写法 | 判类方式 | 遍历方式 | 评价 |
|---|---|---|---|
| 扫描件写法 A | 手写区间比较 | 下标遍历 | 贴近 C 语言思维,能跑但啰嗦 |
| 扫描件写法 B | 手写区间比较 | 直接迭代 | 简洁一档,仍有大小写漏写风险 |
| 方法版 | isalpha/isspace/isdigit | 直接迭代 | 推荐写法,语义最清楚 |
参考代码(方法版,首选):
s=input('input a string:')letters=space=digit=other=0forchins:ifch.isalpha():letters+=1elifch.isspace():space+=1elifch.isdigit():digit+=1else:other+=1print('字母数: %d, 空格数: %d, 数字数: %d, 其他字符数: %d'%(letters,space,digit,other))参考代码(扫描件里的手写比较版,保留原风格):
s=input('input a string:')a=b=c=d=0forchins:if(ch<='Z'andch>='A')or(ch<='z'andch>='a'):a+=1elifch==' ':b+=1elifch>='0'andch<='9':c+=1else:d+=1print('英文字母的个数为: '+str(a))print('空格的个数为: '+str(b))print('数字的个数为: '+str(c))print('其他字符的个数为: '+str(d))两段代码的输出格式不同:前者用%d占位符一次性格式化,后者用str()加号拼接。两种都能用,但拼接的写法要求两边都是字符串,遇到整数必须先str()转一次,这是初学者最常见的报错来源。
易错点:
isalpha()会把汉字也算成字母。输入「你好」时letters会加 2,因为中文字符在 Unicode 里属于字母类。如果题目要求只统计英文字母,就必须改回手写区间比较,或者用ch.isascii() and ch.isalpha()。elif的顺序有讲究。判断链是从上往下短路执行的,一旦上面的条件命中,下面的分支不再检查。这里几类互斥,顺序不影响结果;但若把「数字」写在「字母」前面而条件写错,就会出现某一类永远统计不到的死分支。input()的返回值一定是字符串,不要写成int(input(...)),否则输入空格直接崩。- 加号拼接和
%格式化不要混用:'个数为: ' + a会抛TypeError,整数必须显式转字符串。
一句话总结:字符串统计题的模板就是「一个 for 循环 + 一条 if/elif/else 链 + 四个计数器」,选
isalpha这一套方法判类最省心,但要记得它能识别汉字。
二、素数判断:用函数把判断逻辑独立出来
题目:写一个判断素数的函数,在主程序中输入一个整数,调用该函数判断并输出结果。
思路:判断素数的暴力做法是从 2 试到n-1,但只需要试到sqrt(n)就够了——因为如果n = a * b且a <= b,那么必然有a <= sqrt(n)。所以循环上界取int(math.sqrt(n)),复杂度从 O(n) 降到 O(根号 n)。
扫描件用的是标志位法:用变量w记录状态,w == 0表示暂时没发现因子(即仍可能是素数),w == 1表示已确认不是素数。循环条件里带上w == 0,一旦确认合数就立刻停止继续试除。
参考代码(扫描件写法,修正了短路顺序与格式):
importmathdefshushu(n):i,w=2,0ifn<=1:w=1whilew==0andi<=int(math.sqrt(n)):ifn%i==0:w=1breakelse:i+=1returnw n=int(input('n='))ifshushu(n)==0:print(n,'是素数')else:print(n,'不是素数')参考代码(等价的简化写法,更符合 Python 习惯):
importmathdefis_prime(n):ifn<=1:returnFalseforiinrange(2,int(math.sqrt(n))+1):ifn%i==0:returnFalsereturnTrue简化版把「标志位 + break」换成了「直接return False」,效果完全一样。函数里一旦return,后面的语句就不再执行,所以没必要再维护一个标志位。
易错点:
- 短路顺序会决定程序会不会崩。扫描件原样写的是
while i <= int(math.sqrt(n)) and w == 0。当输入n = -5时,n <= 1只把w置 1,循环条件仍然先算math.sqrt(-5),会抛ValueError: math domain error。把w == 0放到and左边就能被短路掉——这个细节在扫描件里是错的,必须改。 n <= 1不能漏。1 和负数都不是素数,0 和 1 也不会进循环,如果不显式判断就会误判为素数。- 边界 2 和 3 要试一下。对
n = 2,int(math.sqrt(2)) == 1,循环一次都不进,直接判定为素数,正确。 - 返回值到底代表什么,必须和调用处对齐。这份参考答案里 0 表示是素数、1 表示不是素数,是反直觉的;如果换成
is_prime返回True/False,调用处的判断也要跟着改成if is_prime(n):。这种「语义反转」是抄答案时最容易出事的地方。
一句话总结:素数判断的循环上界是
sqrt(n)不是n-1,函数内部早返回比标志位清爽,而循环条件的书写顺序决定了负数输入会不会直接把程序炸掉。
三、文件读取与成绩汇总:readlines 加 split 是标准组合
题目:当前目录下有一个文本文件score3.txt,存放着某班学生的学号和两门专业课成绩。要求:① 找出最高分与最低分对应的学号;② 定义函数function1,计算每个学生的平均分(取整数)并输出;③ 定义函数calAvg,计算某门课的平均分,参数是成绩列表名,返回该门课的平均分。
思路:这三小问共享同一套「读文件」骨架,一定要先把它固定下来:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 打开 | f = open(filename) | 拿到文件对象,默认只读 |
| 整体读入 | a = f.readlines() | 每行一个字符串,末尾带\n |
| 关闭 | f.close() | 释放句柄 |
| 去表头 | del a[0] | 丢掉第一行的标题行 |
| 逐行拆列 | line.strip().split() | 去掉换行与空格,再按空白切分成列表 |
| 取值 | int(L1[1]) | 字符串转整数,才能参与算术 |
strip()和split()的分工要分清:strip()去掉字符串首尾的空白和换行符,split()把一行切成若干字段。顺序不能反,先切分再 strip 只会去掉每一列自己的空白,行尾换行如果粘在最后一列上,int()转换就会失败。
第一问「找最高分与最低分对应的学号」,核心是同时记录值和位置:光记分数没用,题目要的是学号,所以每更新一次最值,必须把当前下标一起存下来,最后用L2[maxIndex]反查学号。
参考代码(第一问,已修正为数值比较):
f=open('score3.txt')a=f.readlines()f.close()dela[0]L2=[]L3=[]forlineina:line=line.strip()L1=line.split()L2.append(L1[0])L3.append(int(L1[1]))maxScore=L3[0]maxIndex=0minScore=L3[0]minIndex=0foriinrange(1,len(L3)):ifL3[i]>maxScore:maxScore=L3[i]maxIndex=iifL3[i]<minScore:minScore=L3[i]minIndex=iprint('最高分为: '+str(maxScore)+' 分, 该学生学号为: '+str(L2[maxIndex]))print('最低分为: '+str(minScore)+' 分, 该学生学号为: '+str(L2[minIndex]))参考代码(第二问function1与第三问calAvg,接在第一问后面可直接运行):
deffunction1(filename):f=open(filename)a=f.readlines()f.close()dela[0]L3=[]forlineina:line=line.strip()L1=line.split()avg_score=int((int(L1[1])+int(L1[2]))/2)L3.append([L1[0],avg_score])print('学号 平均分')forL2inL3:print(L2[0]+' '+str(L2[1]))defcalAvg(L):total=0count=0forscoreinL:total+=score count+=1ifcount==0:return0avg_score=int(total/count)returnavg_score f=open('score3.txt')a=f.readlines()f.close()dela[0]L2=[]L3=[]forlineina:line=line.strip()L1=line.split()L2.append(int(L1[1]))L3.append(int(L1[2]))print('专业课 1 的总平均分为',calAvg(L2))print('专业课 2 的总平均分为',calAvg(L3))function1里L3.append([L1[0], avg_score])把「学号」和「平均分」打包成一个小列表塞进L3,之后遍历L3时每个元素就是一个二元组,L2[0]是学号、L2[1]是平均分。这种「嵌套列表」是把多列数据绑在一起的标准做法,比开两个平行列表更不容易错位。
calAvg则体现了函数设计的两个要点:参数是列表名(所以函数与数据来源解耦,键盘造的列表、文件读的列表都能传进去),返回值是平均分(所以调用处可以继续拿它参与计算,而不是只能打印)。
易错点:
- 扫描件里
calAvg写的是sum_count = 0.0,函数体里却用了sum和count两个从未初始化的变量,一运行就NameError。这是明显的笔误,正确写法是初始化两个变量,如上面的total = 0与count = 0。 - 不要把变量命名成
sum。sum是 Python 内置函数,一旦被赋值覆盖,同一个作用域里就再也用不了它了。同理还有list、str、max、min。 - 平均分取整有两种口径。
int(total / count)是直接截断小数,round(total / count)是四舍五入。题目说「取整数」,两种都能解释,但同一份代码里必须统一,否则对不上答案。 - 扫描件第一问没有把分数转成
int,L3里存的是字符串,L3[i] > maxScore就变成了字符串比较。成绩等宽时(都是三位数)碰巧结果对,但一旦出现两位数或带小数就会选出错误的「最高分」。这是扫描件里最隐蔽的一处问题,务必补上int()。 - 文件路径与转义。扫描件里写的是
open('c:\\test\\score3.txt'),双反斜杠是转义写法。更省事的做法是原始字符串r'c:\test\score3.txt',或者直接把文件放在当前目录、只写文件名——这样代码换台机器也能跑。 - 文件用完要
close()。更稳妥的写法是with open(filename) as f:,缩进块结束自动关闭,连异常路径都能覆盖。
一句话总结:文件题的标准骨架是「open → readlines → close → del 表头 → 逐行 strip 加 split → 转数值」,函数要接列表名、要 return 结果,而扫描件里没做数值转换和变量未初始化这两处必须自己补上。
四、阶乘:迭代与递归,同一条数学定义的两条路径
题目:用函数或函数的递归实现求 n! 的算法(主程序已经给出)。
思路:阶乘有两种天然的写法,正好对应两种思维:
- 迭代法:从 1 乘到 n,用一个累乘变量扛住中间结果。这是一条「自底向上」的路,看的是过程。
- 递归法:把
n!定义成n * (n-1)!,问题规模每次缩小 1,直到触底1! = 1。这是一条「自顶向下」的路,看的是定义。
两者复杂度都是 O(n)。递归的代码更短、更贴近数学定义,代价是每一层都要占用调用栈。
参考代码(迭代版):
deffact(n):value=1forcountinrange(1,n+1):value*=countreturnvalue n=int(input('Calculate n! Enter n='))print(n,'!=',fact(n))参考代码(递归版):
deffact(n):ifn==1orn==0:return1returnn*fact(n-1)参考代码(主程序,扫描件已给出,照抄即可):
n=int(input('Calculate n! Enter n='))print(n,'!=',fact(n))易错点:
- 递归必须有出口,而且出口条件要写全。扫描件只写了
if n == 1,此时fact(0)会一路递归到fact(-1)、fact(-2)……永远碰不到 1,最终抛RecursionError。补上n == 0就够了。0 的阶乘定义为 1,这一点别和「0 不是素数」记混。 - 累乘变量的初值是 1,不是 0。写成
value = 0的话,无论乘多少次结果都是 0。 range(1, n + 1)的上界要加一。range是左闭右开的,写成range(1, n)会少乘一项n。*=与普通赋值不要写反。value *= count等价于value = value * count,写成value = count就丢掉了之前所有累乘结果。- 递归深度有上限。默认约 1000 层,输入 2000 就会报递归超限;迭代版没有这个问题。这不是算法错,是 Python 的实现约束。
一句话总结:阶乘的迭代版盯住「初值 1、上界 n 加 1、累乘不覆盖」,递归版盯住「出口写全」——递归只是把循环交给了调用栈。
五、有序表插入:append、insert 与 break 的配合
题目:主程序中已有一个排好序的列表,编写函数insertList,把从键盘接收的整数按原来从小到大的排序规律插入到该列表中。
思路:列表已经有序,所以不需要排序,只需要找到第一个比待插元素大的位置,把新元素插到它前面即可。要分两种情况:
- 比所有元素都大:扫完整个列表也找不到「更大的元素」,这时候要插到末尾,用
append; - 在中途找到位置:用
insert(i, x)插到下标i,然后立刻break跳出循环——因为位置已经找到,再插一次就重复了。
先判第一种情况的写法最稳,因为append在末尾的情形下如果忘了跳出,后面的循环还会继续,逻辑容易乱。
| 方法 | 行为 | 适用场景 |
|---|---|---|
L.append(x) | 追加到末尾 | 新元素比所有元素都大 |
L.insert(i, x) | 插到下标 i,后面的元素整体后移 | 在中间或开头找到位置 |
L.sort() | 整体重排 | 数据本来无序,用来兜底 |
参考代码:
definsertList(L1,x):ifx>L1[len(L1)-1]:L1.append(x)returnforiinrange(0,len(L1)):ifx<L1[i]:L1.insert(i,x)breakreturnL1=[1,4,6,9,13,16,28,40,100]x=int(input('请输入一个要插入的整数:'))insertList(L1,x)print(L1)关于「原地修改」的一点观察:insertList没有return任何数据,却能让主程序里的L1发生变化。原因是列表是可变对象,传进函数的是引用,函数内部对列表做的append、insert会直接反映到调用方。这和整数、字符串作为参数时的行为完全不同——后者在函数里被改动,出了函数就还原了。这道题顺带把「可变对象传引用、不可变对象传值」这个知识点摆在了台面上。
易错点:
L1[len(L1) - 1]在空列表上会抛IndexError。更简洁且安全的边界判断是if not L1 or x > L1[-1]:——L1[-1]本身就是最后一个元素,not L1先挡住空列表。insert和append混用容易插重复。如果循环里没写break,代码会在每一个「比 x 大」的位置都插一次。x < L1[i]用严格小于,表示相等时插到已有相同元素的后面。想插到前面就改成x <= L1[i],这属于「相等元素放前还是放后」的语义选择,题目没规定时必须自己说明。- 不要用
x > L1[-1]之外的排序假设。这个函数只在列表已经有序时成立,对一个乱序列表调用它,结果依然是错的——题目已经给了「已排好序」这个前提,别丢。
一句话总结:有序表插入就是「先判尾部、再扫第一个更大的位置、
insert之后立刻break」,同时记住列表是按引用传递的,函数不用返回值也能改动实参。
全系列总结
三篇笔记连起来,是一条从「会写」到「会用」的路径:
| 篇目 | 主题 | 解决什么问题 | 代表题型 |
|---|---|---|---|
| 01 | 程序结构 | 顺序、分支、循环三块积木怎么搭 | 判断闰年、分段函数、累加累乘 |
| 02 | 数据结构与常用方法 | 字符串、列表、字典各自擅长什么 | 切片、统计频次、列表增删改查 |
| 03 | 综合应用 | 用函数封装逻辑,再接上文件数据 | 字符统计、素数、成绩汇总、递归、有序插入 |
这条路径的逻辑是:01 学会控制流程,02 学会选容器,03 学会把流程和容器打包成可以被反复调用的函数,并让数据从「键盘」扩展到「文件」。到了 03,前两篇的知识基本都会在场:遍历是 01 的循环,split与insert是 02 的方法,而把它们组织起来的是函数这个外壳。
下面这张速查表把三篇里出现频率最高的写法压在一处,考前扫一遍基本够用:
| 题型 | 考察点 | 常用写法 |
|---|---|---|
| 数值判断(素数、闰年) | 循环边界、短路求值 | for i in range(2, int(n ** 0.5) + 1) |
| 分段函数 | 多分支互斥 | if / elif / else链,条件从窄到宽 |
| 累加累乘 | 初值选择 | total = 0累加、value = 1累乘 |
| 字符串分类统计 | 判类方法 | isalpha/isdigit/isspace/ 手写区间 |
| 字符串切分 | 去空白与拆分 | line.strip().split(),先 strip 后 split |
| 列表求最值 | 值加下标同时记 | maxScore配maxIndex,更新时一起改 |
| 列表插入 | 原地修改与引用 | append在尾、insert(i, x)在中间,插完break |
| 文件读取 | 标准骨架 | with open(f) as fp:加readlines()加del a[0] |
| 类型转换 | 字符串转数值 | int(...)、float(...),比较前先转 |
| 递归 | 出口条件 | if n == 0 or n == 1: return 1,否则n * f(n - 1) |
| 函数设计 | 参数与返回值 | 参数传列表名,结果用return而不是 print |
本篇最需要记住的几句话
- 字符串统计题的模板是「一个循环 + 一条 if/elif/else 链 + 若干计数器」,用
isalpha这一套方法判类最省心,但它会把汉字也算作字母。 - 素数判断只需试除到
sqrt(n);n <= 1要单独处理,循环里把状态判断放在and左边可以避免负数输入触发math domain error。 - 文件题的标准骨架:
open → readlines → close → del 表头 → strip 加 split → 转数值,顺序不能乱。 - 函数要「参数接列表名、结果用 return」,这样数据和逻辑解耦,同一个函数既能处理键盘输入也能处理文件数据。
- 求和累乘的初值分别是 0 和 1,变量名不要覆盖
sum、max、list这些内置函数。 - 递归的两个硬要求是出口写全和规模递减,
fact(0)的出口不补上就会一路递归到栈溢出。 - 列表是可变对象,按引用传递——函数里
append、insert会改动调用方的列表,所以insertList不需要返回值。
本篇易错点与记忆提示
isalpha()认汉字:想只统计英文字母,要么手写'A' <= ch <= 'Z'区间,要么加一道ch.isascii()过滤。这是「方法版写得漂亮却答错」的典型。- 字符串比较冒充数值比较:从文件读出来的分数默认是字符串,不转
int就直接比大小,等宽三位数时碰巧对,出现两位数就翻车。凡是参与算术或大小比较的数据,先转类型。 - 变量未初始化:扫描件里
calAvg用了从未定义的sum和count,sum_count = 0.0却没人用它。写循环累加前,先把计数器与累加器都赋上初值。 sum是内置函数不是变量名:把它当累加器用,同一个作用域里内置的sum()就废了。改用total、acc这类名字。- 循环条件的书写顺序有语义:
while w == 0 and i <= int(math.sqrt(n))与反过来写,在负数输入下一个是正常返回、一个是抛异常,靠的就是and的短路。 range右开:range(1, n + 1)才是 1 到 n,累乘和累加都容易在这里少算最后一项。- 递归出口别只写
n == 1:阶乘在 0 上有定义,出口要写成n == 0 or n == 1。 append与insert的角色不要混:尾部追加用append,指定位置用insert(i, x),插完必须break,否则会重复插入。- 记忆口诀:判类看方法、试除到开方、读文件先 strip 后 split、函数参数传列表结果靠 return、递归先想出口。
【此处有图:五组题的知识点关系图——字符串统计与素数判断是最内层的单循环,向外包一层函数变成 function1 与 calAvg,再向外接上文件读取与列表原地修改,构成一条从「单段代码」到「可复用函数处理外部数据」的阶梯】