ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Python 习题笔记 02】文件读写与列表处理:气温统计、成绩分档、函数封装与排序

【Python 习题笔记 02】文件读写与列表处理:气温统计、成绩分档、函数封装与排序

本系列完整合集:

  • Python 习题笔记合集:程序结构、文件处理与函数封装完整合集

原分篇继续公开保留,可按需查阅。


这一篇对应《Python 程序设计》习题集里连续五页的动手题,从第 6 页一直排到第 10 页。把它们放在一起看,会发现题目表面上各不相同——有气温、有成绩、有排序——但骨架其实只有一条线:先把文件读成字符串,再把字符串切成列表,把列表里的字符串转成整数,然后用循环把整数加工成结果,最后写回文件或打印出来。

所以我不按题号顺序抄一遍,而是按知识点重新归类。一共八节:前三节讲「文件 → 列表 → 数字」这条数据流水线,第四节讲同一道题的第二种写法,第五节讲字符串方法,第六节讲怎么把三个需求拆成三个函数,第七、八节讲两道算法题的套路。


本系列导航

  • 01 程序结构与算法:条件判断、循环与入门经典题
  • 02 文件读写与列表处理:气温统计、成绩分档、函数封装与排序(本篇)
  • 03 函数封装与文件处理:字符统计、素数判断、成绩汇总与有序表插入

一、把文件读成列表:open、readline、strip、split 四件套

题目:temp.txt的第一行是这一周每天的最高气温,第二行是每天的最低气温,数字之间用英文逗号分隔。请把这两行分别读进两个列表,为后面的统计做准备。

思路:文件里的所有内容本质上都是字符串,所以要分四步走。第一步open打开文件拿到句柄;第二步用readline()读一整行;第三步用strip()去掉行末的换行符;第四步用split(',')按逗号切成列表。这四步的顺序不能颠倒——先strip再split,否则最后一个元素会带着换行符,转整数时会报错。读完之后立刻close(),这是习题集里反复强调的一步。

参考代码:

flname="temp.txt"f=open(flname)ht=(f.readline()).strip()L1=list(ht.split(","))lt=(f.readline()).strip()L2=list(lt.split(","))f.close()foriinrange(len(L1)):L1[i]=int(L1[i])L2[i]=int(L2[i])print("最高气温列表:",L1)print("最低气温列表:",L2)

上面这段for循环是把字符串列表原地转成整数列表,等价于L1 = [int(x) for x in L1]。两种写法都要会,考试里看到哪种都不该慌。

易错点:split()括号里什么都不写是按空白字符分割,能顺便吞掉连续空格,而split(',')只按逗号分,两者不能混用。另外L1 = list(ht.split(','))里的list()其实可以省略,split返回的已经是列表。

一句话总结:读文件的固定套路是open → readline → strip → split → close,顺序错了就会把换行符一起吃进数据里。


二、打擂台法求极值,顺便算日均气温

题目:在上一题的基础上,求出这一周的最高气温和最低气温分别出现在第几天;再求出全周的平均气温(各天平均温度的平均值,取整数)。假设入春标准是连续 5 天日均气温超过 10℃,根据这一周的数据判断是否已经入春。

思路:求极值最经典的写法是打擂台——先把第一个元素当作擂主,然后让后面每个元素上来挑战,谁更极端谁就当新擂主,同时记下它的下标。这里要同时记两件事:极值本身和它所在的位置。日均气温则是最高温和最低温的算术平均,所以要把两个列表对应位置相加再除以 2。

参考代码:

maxVal=L1[0]maxDay=0minVal=L2[0]minDay=0foriinrange(len(L1)):ifL1[i]>maxVal:maxVal=L1[i]maxDay=iifL2[i]<minVal:minVal=L2[i]minDay=iprint("这周第"+str(maxDay+1)+"天最热, 最高"+str(maxVal)+"摄氏度")print("这周第"+str(minDay+1)+"天最冷, 最低"+str(minVal)+"摄氏度")L3=[]foriinrange(len(L1)):L3.append(int((L1[i]+L2[i])/2))total=0k=0foriinrange(len(L3)):total=total+L3[i]ifL3[i]>=10:k+=1else:k=0avg=int(total/len(L3))print("周平均气温为:",avg)ifk>=5:print("上海这周已入春")else:print("上海这周末入春")

这里我做了两处修正。第一,扫描件里把累加变量写成了sum,但sum是 Python 的内置函数名,覆盖它会带来隐患,所以改成total。第二,扫描件里记擂主下标的初值写得含混,按语义应当是下标0,因为擂主就是列表第一个元素。

易错点:int((L1[i] + L2[i]) / 2)是向零截断,气温都是正数时没问题,但如果出现负数,int(-2.5)得到-2而-2.5 // 1得到-3,两者不同。还有更隐蔽的一点:这个k在遇到低于 10℃ 的那天会被清零,所以它统计的只是最后一段连续高温,而不是全周最长的一段。要判断「是否出现过连续 5 天」,正确的做法是额外维护一个最大值best = max(best, k),再拿best去比较。

统计目标循环里要维护的量关键动作
极值及其位置擂主值 + 擂主下标先设初值,再逐个挑战
末尾连续段长度一个计数器k命中加一,不命中清零
全周最长连续段计数器k+ 历史最大值清零前先更新历史最大值

一句话总结:打擂台法的核心是「先立擂主再逐个挑战」,而连续段问题里计数器清零前要不要留一份历史最大值,是区分对错的关键。


三、从 score1.txt 到 score2.txt:加权计算与分档统计

题目:当前目录下score1.txt存放着某班学生的计算机课成绩,共学号、平时成绩、期末成绩三列(第一行是表头)。请按平时成绩占 40%、期末成绩占 60% 的比例计算总评成绩并取整,把学号和总评成绩两列写入score2.txt;同时在屏幕上输出学生总人数、各分数档人数(90 以上、8089、7079、60~69、60 分以下)和班级总平均分(取整)。

思路:这是一道「一进一出加统计」的综合题。输入侧用readline逐行推进,输出侧一边写文件一边做统计。五个档位用一个长度为 5 的计数列表c来装,判断时必须从高分档往低分档写elif,这样每个分数只会落进第一个满足条件的分支。注意先跳过表头,再做循环。

参考代码:

f=open("score1.txt")a=f.readline()line=(f.readline()).strip()f2=open("score2.txt","w")f2.write("学号 总评成绩\n")c=[0,0,0,0,0]count=0total=0whilelen(line)!=0:L1=line.split()f2.write(L1[0]+" ")f_score=int(int(L1[1])*0.4+int(L1[2])*0.6)iff_score>=90:c[0]+=1eliff_score>=80:c[1]+=1eliff_score>=70:c[2]+=1eliff_score>=60:c[3]+=1else:c[4]+=1count+=1total+=f_score f2.write(str(f_score)+"\n")line=(f.readline()).strip()f.close()f2.close()avg_score=int(total/count)print("学生总人数为 %d, 90 以上 %d 人, 80-89 间 %d 人, 70-79 间 %d 人, ""60-69 间 %d 人, 60 分以下 %d 人, 班级总平均分为 %d 分"%(count,c[0],c[1],c[2],c[3],c[4],avg_score))

易错点:扫描件里最高档写的是if 90 < f_score <= 100,这把恰好 90 分的人挤进了 80~89 档。题目说的是「90 以上」,按中文习惯应当包含 90,所以我改成了f_score >= 90——分档题的边界一定要看清是「以上」还是「超过」。另外while len(line) != 0的终止条件依赖readline读到文件尾时返回空字符串,所以循环体内必须先处理当前行、再读下一行,顺序反了会漏掉最后一行。第三,取整要放在算完平均分之后再取,如果先把每个人的总评取整再平均,结果会不一样。

一句话总结:分档统计的模板是「一个计数列表 + 从高到低排的 elif」,而while len(line) != 0这种循环要特别小心「先处理再读下一行」的顺序。


四、同一道题的第二种写法:readlines 加列表套列表

题目:还是上一题,换一种文件推进方式重做一遍。

思路:readlines()一次性把整个文件读成一个字符串列表,配合del a[0]删掉表头,之后就能用for循环遍历。这种写法把「读入」和「加工」拆成了两步:先扫一遍算出每个学生的总评,把[学号, 总评]这样的小列表塞进大列表;再扫第二遍做分档和写文件。代码结构更清楚,代价是多占一份内存。

参考代码:

f=open("score1.txt")a=f.readlines()dela[0]L3=[]forlineina:line=line.strip()L1=line.split()f_score=int(int(L1[1])*0.4+int(L1[2])*0.6)L3.append([L1[0],f_score])f.close()c=[0,0,0,0,0]count=0total=0f2=open("score2.txt","w")f2.write("学号 总评成绩\n")forL2inL3:ifL2[1]>=90:c[0]+=1elifL2[1]>=80:c[1]+=1elifL2[1]>=70:c[2]+=1elifL2[1]>=60:c[3]+=1else:c[4]+=1count+=1total+=L2[1]f2.write(L2[0]+" "+str(L2[1])+"\n")f2.close()avg_score=int(total/count)print("学生总人数为 %d, 90 以上 %d 人, 80-89 间 %d 人, 70-79 间 %d 人, ""60-69 间 %d 人, 60 分以下 %d 人, 班级总平均分为 %d 分"%(count,c[0],c[1],c[2],c[3],c[4],avg_score))

易错点:del a[0]是在原列表上删除第一个元素,不是返回一个去掉表头的新列表,所以不能写成a = del a[0]。for L2 in L3里的L2是一个两元素列表,读成绩要用L2[1],写成L2[1]和L2[0]千万别混。还有一个细节:f2.write的参数只能是字符串,拼接L2[1]之前必须str()转换,这是初学者最高频的TypeError来源。

对比项readline 加 whilereadlines 加 for
内存占用只留一行,占用小整文件进内存
代码结构读与算揉在一个循环里可先加工再统计,分层清楚
循环终止靠空字符串判断靠列表遍历自动结束
跳表头先额外读一行丢掉del a[0]
适用场景文件很大时需要反复扫描同一批数据时

一句话总结:readline是「边走边算」,readlines是「先全部拿进来再算」,两种写法没有对错,区别只在内存和结构的取舍。


五、字符串方法:整文件复制并把小写字母变大写

题目:当前目录下有sample12.txt,内容含小写字母和大写字母。请把该文件复制成sample12_copy.txt,并把原文件中的小写字母全部转换为大写字母,其余格式均不变。

思路:readlines()读到的每一行自带换行符,这正是「格式不变」的关键。只需要对每一行调用upper(),再把结果原样写出去,换行符就会自动保留,行数和空行都不会变。

参考代码:

f=open("sample12.txt")L1=f.readlines()f2=open("sample12_copy.txt","w")forlineinL1:f2.write(line.upper())f.close()f2.close()

易错点:upper()返回一个新字符串,原字符串不会改变,所以必须写成f2.write(line.upper()),单写一行line.upper()是没有效果的。第二,如果出于习惯写了line.strip().upper(),换行符就被吃掉了,复制出来的文件会变成一大坨——题目特意强调「其余格式均不变」,就是在提醒不要strip。第三,写模式用'w',如果用'w+'或忘了打开第二个文件句柄,会直接覆盖原文件。

一句话总结:字符串方法都返回新对象、不修改原对象;而readlines保留换行符这件事,正好是「格式不变」需求的实现手段。


六、用函数把一份数据拆成三个问题

题目:当前目录下有class_score.txt,存放某班学生的学号、数学课成绩(第 2 列)和语文课成绩(第 3 列)。要求:(1) 分别求数学和语文的平均分,保留 1 位小数;(2) 找出两门课都不及格(都低于 60)的学生,输出学号和各科成绩;(3) 找出两门课平均分在 90 分以上的学生,输出学号和各科成绩。要求用三个函数分别实现。

思路:三个需求互不依赖,唯一共享的东西是那份数据,所以最自然的切法是——在外面把文件读成一个列表,把列表当参数传给三个函数。这就是函数封装最朴素的价值:同一份数据被反复使用,而每次使用的逻辑互不干扰。三个函数各司其职,主程序只剩下读文件和三次调用。

参考代码:

defoutput_avg(L):sum1=0sum2=0forlineinL:L1=line.strip().split()sum1+=int(L1[1])sum2+=int(L1[2])count=len(L)avg1=round(sum1/count,1)avg2=round(sum2/count,1)print("这个班的数学平均分为: %4.1f, 语文平均分为: %4.1f"%(avg1,avg2))defoutput_notpass(L):print("两门课均不及格的学生学号及数学 语文成绩为:")forlineinL:L1=line.strip().split()ifint(L1[1])<60andint(L1[2])<60:print(line)defoutput_good(L):print("两门课平均分在 90 分以上的学生学号及数学 语文成绩为:")forlineinL:L1=line.strip().split()f_score=round((int(L1[1])+int(L1[2]))/2)iff_score>=90:print(line)f=open("class_score.txt")L=f.readlines()delL[0]output_avg(L)output_notpass(L)output_good(L)f.close()

易错点:第一,列号和下标差 1。学号是第 1 列对应L1[0],数学是第 2 列对应L1[1],语文是第 3 列对应L1[2],写代码时按题目里的「第几列」直接当索引用就会全体错位。第二,del L[0]删表头必须放在readlines之后、调用函数之前,否则平均分的分母会把表头也算进去。第三,「都不及格」要用and连接而不是or,用or会把只挂一门的学生也捞出来。第四,round是银行家舍入,round(2.675, 2)未必得到2.68,涉及精确小数时更稳妥的做法是格式化输出而不是round。

需求遍历时判断输出内容
求两科平均分不需要判断,全部累加两个百分数,保留 1 位小数
两门都不及格int(数学) < 60 and int(语文) < 60原始整行
两门平均分 90 分以上round((数学 + 语文) / 2) >= 90原始整行

一句话总结:函数封装的切分依据是「共享数据 + 独立逻辑」——数据在外面读好当参数传进去,函数内部只关心自己那一个问题。


七、排序算法:选择排序与冒泡排序

题目:编程实现从键盘接收若干个整数,直接输入回车表示结束,用冒泡法或选择法从小到大排序,并把排序结果输出到屏幕。

思路:两道题其实共用同一个主程序,差别只在排序函数内部。选择排序是定位置找元素:外层循环确定第 i 个位置,内层循环在 i 之后找最小值,找到了就交换。冒泡排序是相邻比较、逐轮上浮:内层从后往前两两比较,把小的往前换。两者都是双重循环、都是原地交换。

参考代码:

defselection_sort(List):num=len(List)foriinrange(0,num-1):forjinrange(i+1,num):ifList[j]<List[i]:List[i],List[j]=List[j],List[i]returnListdefbubble_sort(List):foriinrange(0,len(List)-1):forjinrange(len(List)-1,i,-1):ifList[j-1]>List[j]:List[j-1],List[j]=List[j],List[j-1]returnList L1=[]num_str=input("请输入一个需排序的整数: ")whilelen(num_str)!=0:L1.append(int(num_str))num_str=input("请输入一个需排序的整数: ")print("排序后结果:",bubble_sort(L1))

这里必须做两处修正,都是扫描件里的明显笔误。第一,扫描件把选择排序的函数名也写成了bubble,两个函数同名会互相覆盖,所以改名为selection_sort。第二,扫描件里选择排序的内层判断写的是if List[j] > List[i],这个方向得到的是从大到小,与题目要求的从小到大相反,所以把大于号改成了小于号。

易错点:input()返回的永远是字符串,直接回车得到的是空字符串,所以判空要写len(num_str) != 0或num_str != '',而不能在判空之前就int(num_str),否则用户一按回车程序立刻抛ValueError。第二,交换两个元素用List[i], List[j] = List[j], List[i]就够了,不需要中间变量,但一定要写在同一行里。第三,外层循环只到num - 1,因为最后一个元素不需要再排;内层range(len(List) - 1, i, -1)的第三个参数-1表示倒着走,漏了它就是死循环或者白跑。第四,函数里返回的是List,调用时要print它的返回值,直接print(bubble_sort(L1))比先调用再打印更省事。

对比项选择排序冒泡排序
内层比较对象List[i]与List[j]List[j - 1]与List[j]
内层扫描方向从 i+1 往后从末尾倒着往前
每轮结束的效果第 i 个位置确定为最小值第 i 个位置确定为最小值
交换次数较少较多
时间复杂度O(n 平方)O(n 平方)

一句话总结:选择排序是「定位置找元素」,冒泡排序是「相邻比较、逐轮上浮」,两者的循环边界和比较符号是考试最容易设坑的地方。


八、打擂台法找最高分和最低分

题目:当前目录下有score2.txt,存放着某班学生的计算机课成绩,共学号、总评成绩两列。请查找最高分和最低分的学生,并在屏幕上显示其学号和成绩。

思路:扫描件这一题只印到了开头几行(读完del a[0]、L2=[]、L3=[]就换页了),后面的内容没有给出。所以下面按打擂台法补一个等价的最小实现:先把文件读成「学号 + 整数成绩」的二元列表,再让第一个学生当擂主,逐个挑战。

参考代码:

f=open("score2.txt")a=f.readlines()dela[0]L2=[]forlineina:line=line.strip()ifline=="":continueL1=line.split()L2.append([L1[0],int(L1[1])])f.close()maxStu=L2[0]minStu=L2[0]foriteminL2:ifitem[1]>maxStu[1]:maxStu=itemifitem[1]<minStu[1]:minStu=itemprint("最高分学生:",maxStu[0],maxStu[1])print("最低分学生:",minStu[0],minStu[1])

易错点:第一,擂主初值不能写成0或者空列表。如果班里所有人都是 0 分,用 0 当最高分初值就找不到人;如果出现负分,用 0 当最低分初值也一样会错。正确的做法永远是拿数据里真实存在的第一个元素当擂主。第二,readlines的结果里可能夹着空行,line.split()遇到空行会返回空列表,紧接着的L1[1]就会抛IndexError,所以要先continue跳过。第三,这里把整个item列表赋给maxStu,之后对maxStu只是读不写,所以不会污染原数据;但如果后续要修改它,就必须用切片item[:]做浅拷贝。

一句话总结:打擂台法求极值,初值一定是数据中的真实元素,而列表套列表的写法能让「学号 + 成绩」这两个信息一起被记住。


本篇最需要记住的几句话

  1. 读文件的固定套路是open → readline → strip → split → close,顺序错了就会把换行符一起吃进数据里。
  2. split出来的是字符串列表,参与比较和求和前必须先int转换,这是所有统计题的第一步。
  3. 打擂台法的初值必须是数据中真实存在的第一个元素,用0当擂主是隐藏最深的错误。
  4. 连续段计数器的清零逻辑决定了答案对不对:只维护一个k只能得到末尾那一段,要得到全周最长的一段必须再维护一个历史最大值。
  5. 分档统计用「一个计数列表 + 从高到低排的elif」,边界词是「以上」还是「超过」要看清。
  6. readline与readlines是两种风格而不是两种对错:前者边读边算省内存,后者先读后算结构清楚。
  7. 字符串方法都返回新对象,line.upper()不写进write就等于没做。
  8. 选择题排序还是冒泡排序,看内层比较的是List[i]还是List[j-1];input()判空必须在int()之前。

本篇易错点与记忆提示

  • sum不要当变量名:它是内置函数,覆盖之后同一作用域里再用sum(L)就会报错。习题集里多处直接拿sum累加,写成total更稳妥。
  • int是向零截断,不是四舍五入:int(89.9)得到89,如果要四舍五入必须先round。总评成绩写的是「取整」,两种理解会差一分。
  • int((最高 + 最低) / 2)和(最高 + 最低) // 2在负数上不等价,气温题出现负值时就会露馅。
  • while len(line) != 0循环里先处理当前行、再读下一行,顺序反了会漏掉最后一行或者直接跳过第一行。
  • del a[0]是原地删除,不是表达式,不能写在赋值号右边。
  • 列号与下标差 1:题目说第 2 列,代码里就是L1[1],这是成绩统计题最常见的错位来源。
  • 'w'模式会清空原文件:往同一个路径写之前想清楚,尤其别把输入文件和输出文件写成同一个名字。
  • 扫描件里的排序函数名有笔误(选择排序也叫bubble),两个同名函数后定义的会覆盖先定义的,改名前一定要核对。
  • round采用银行家舍入:round(2.5)得到2而不是3,对成绩取整有影响时要改用格式化输出。

【此处有图:一条从temp.txt与score1.txt出发的数据流水线示意,依次经过open/readline、strip/split、int 转换、循环统计、write 回盘五个方框,并在split与int之间标注易错点「字符串列表必须先转整数」】

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进