ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Python 习题笔记 03】函数封装与文件处理:字符统计、素数判断、成绩汇总与有序表插入

【Python 习题笔记 03】函数封装与文件处理:字符统计、素数判断、成绩汇总与有序表插入 这一篇是 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直接迭代推荐写法语义最清楚参考代码方法版首选sinput(input a string:)lettersspacedigitother0forchins:ifch.isalpha():letters1elifch.isspace():space1elifch.isdigit():digit1else:other1print(字母数: %d, 空格数: %d, 数字数: %d, 其他字符数: %d%(letters,space,digit,other))参考代码扫描件里的手写比较版保留原风格sinput(input a string:)abcd0forchins:if(chZandchA)or(chzandcha):a1elifch :b1elifch0andch9:c1else:d1print(英文字母的个数为: 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,w2,0ifn1:w1whilew0andiint(math.sqrt(n)):ifn%i0:w1breakelse:i1returnw nint(input(n))ifshushu(n)0:print(n,是素数)else:print(n,不是素数)参考代码等价的简化写法更符合 Python 习惯importmathdefis_prime(n):ifn1:returnFalseforiinrange(2,int(math.sqrt(n))1):ifn%i0: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 2int(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]反查学号。参考代码第一问已修正为数值比较fopen(score3.txt)af.readlines()f.close()dela[0]L2[]L3[]forlineina:lineline.strip()L1line.split()L2.append(L1[0])L3.append(int(L1[1]))maxScoreL3[0]maxIndex0minScoreL3[0]minIndex0foriinrange(1,len(L3)):ifL3[i]maxScore:maxScoreL3[i]maxIndexiifL3[i]minScore:minScoreL3[i]minIndexiprint(最高分为: str(maxScore) 分, 该学生学号为: str(L2[maxIndex]))print(最低分为: str(minScore) 分, 该学生学号为: str(L2[minIndex]))参考代码第二问function1与第三问calAvg接在第一问后面可直接运行deffunction1(filename):fopen(filename)af.readlines()f.close()dela[0]L3[]forlineina:lineline.strip()L1line.split()avg_scoreint((int(L1[1])int(L1[2]))/2)L3.append([L1[0],avg_score])print(学号 平均分)forL2inL3:print(L2[0] str(L2[1]))defcalAvg(L):total0count0forscoreinL:totalscore count1ifcount0:return0avg_scoreint(total/count)returnavg_score fopen(score3.txt)af.readlines()f.close()dela[0]L2[]L3[]forlineina:lineline.strip()L1line.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)是四舍五入。题目说「取整数」两种都能解释但同一份代码里必须统一否则对不上答案。扫描件第一问没有把分数转成intL3里存的是字符串L3[i] maxScore就变成了字符串比较。成绩等宽时都是三位数碰巧结果对但一旦出现两位数或带小数就会选出错误的「最高分」。这是扫描件里最隐蔽的一处问题务必补上int()。文件路径与转义。扫描件里写的是open(c:\\test\\score3.txt)双反斜杠是转义写法。更省事的做法是原始字符串rc:\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):value1forcountinrange(1,n1):value*countreturnvalue nint(input(Calculate n! Enter n))print(n,!,fact(n))参考代码递归版deffact(n):ifn1orn0:return1returnn*fact(n-1)参考代码主程序扫描件已给出照抄即可nint(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):ifxL1[len(L1)-1]:L1.append(x)returnforiinrange(0,len(L1)):ifxL1[i]:L1.insert(i,x)breakreturnL1[1,4,6,9,13,16,28,40,100]xint(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和countsum_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再向外接上文件读取与列表原地修改构成一条从「单段代码」到「可复用函数处理外部数据」的阶梯】
返回列表