ARTICLE DETAIL

资讯详情

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

Python实战:写一个幸运大转盘,彻底搞懂for与while循环

Python实战:写一个幸运大转盘,彻底搞懂for与while循环 最初那段经历我得提一下我学循环的时候教材里的例题不是打印九九乘法表就是求1加到100。练习题做完之后代码是写出来了但脑子里始终绷着一根弦——现实中到底什么时候该用for什么时候该用while直到我做了一个控制台版的333幸运大转盘才真正把循环用明白。这系列第2篇我就专门讲讲如何在转盘项目里把循环的每个应用场景都吃透适合已经把print、变量、if基本语法过了一遍想通过一个小项目把循环彻底弄懂的朋友。这篇文章会用到一个完整的抽奖程序从奖项表展示、指针转动、随机停点、多轮抽奖到中奖次数统计全程都绕不开循环。你会发现循环并不难难的是在合适的位置用正确的循环把重复的事情做掉。1. 转盘虽小循环无处不在先拆需求再动手很多人一上来就写代码写到一半发现抽一次还行想再抽一次写不出来原因就是没有提前把需求里的重复动作拎出来。幸运大转盘看起来是一个简单游戏但把转盘这个动作拆细里面至少有四种循环展示奖项要循环遍历、指针扫过格子要循环移动、用户是否续玩是条件循环、统计中奖次数是累加循环。只要能把这四个场景对上号这项目就已经会了一大半。1.1 固定次数的遍历for循环管已知的重复假设奖品池有10个格子你要做的第一件事就是把10个奖展示给玩家看。这属于次数明确、按顺序走一遍的重复for循环最合适。prize_pool [谢谢参与, 5元优惠券, 再来一次, 10元红包, 定制笔记本, 谢谢参与, 50元红包, VIP会员月卡, 谢谢参与, 蓝牙耳机] for i, prize in enumerate(prize_pool, 1): print(f第{i}格{prize})enumerate是Python里非常常用的同时拿序号和值的工具参数里的1表示从第1开始编号而不是从0。如果你不想要序号直接写for prize in prize_pool:也行。这里的关键是因为有多少个奖项是确定的所以用for而for循环的次数由奖品列表的长度决定天然就能保证你每个奖都展示一遍不会多也不会漏。1.2 不确定次数的重复while循环管未知的重复玩家抽完一次之后你会问一句再来一局。这个再来一局的次数完全取决于玩家心情可能玩3次就停也可能玩50次。这种事前不知道到底要循环多少次、只根据某个条件决定是否继续的场景写for循环会很尴尬因为for要求你先知道一个精确次数。is_play True while is_play: play_once() answer input(再来一次(y/n): ).strip().lower() if answer n: is_play False也可以写得更简洁while True: play_once() answer input(再来一次(y/n): ).strip().lower() if answer n: breakwhile True加break是Python里处理菜单循环主循环的常见姿势比用标志位的写法更直接。有些朋友问python中while如何退出循环答案就是三条路把条件变成False、在循环体里用break、用return直接退出整个函数。后面我会专门讲这三者的边界。1.3 数据积累同样是循环统计中奖次数每一轮抽完后要把哪个奖中了多少次记下来。最笨的办法是定义10个变量分别统计但那不可维护。正确做法是用一个字典每中一次就给对应的key加1stats {} prize 10元红包 stats[prize] stats.get(prize, 0) 1这段代码里的stats.get(prize, 0)表示字典里如果还没有这个奖就当作0来处理避免第一次累加时报KeyError。整个抽奖过程反复执行这段逻辑所以它也在一个循环里。循环不只是在界面上转圈圈让重复的数据处理做同一件事本身就是循环最本质的价值。下面是for、while、break这三种结构在转盘项目里的分工我整理成一张表写代码前可以先对着选场景次数是否已知推荐结构说明展示10个奖项已知for遍历列表次数由列表长度决定指针扫过一圈12格已知一圈for嵌套外层控制圈数内层控制格子是否继续抽奖未知while条件判断玩家输入抽奖主循环未知while True break直到玩家明确退出统计每奖次数已知总轮数后循环内累加字典赋值实现2. 让指针在控制台里转起来循环动画怎么写做这个项目时我一度觉得转盘动画是玄学。毕竟控制台没有图形界面怎么让一个指针看起来在动其实原理很简单把指针挨个指向每个格子每个状态停留极短时间快速交替产生转动的错觉。这个交替过程就是循环。2.1 最朴素的动画循环扫过每个扇区先写一个最基础版本让指针从第1格扫到第10格重复扫4圈。import time prize_pool [谢谢参与, 5元优惠券, 再来一次, 10元红包, 定制笔记本] size len(prize_pool) for circle in range(4): # 外层循环4圈 for i in range(size): # 内层循环一圈扫过所有格子 print(f\r指针指向{prize_pool[i]}, end) time.sleep(0.05)这里出现了一个很重要的概念循环嵌套。外层循环控制圈数内层循环控制一圈内指针扫过的格子。很多初学者学循环时不知道嵌套怎么用看这个例子就清楚了——外层是重复几次大动作内层是每次大动作里的小步骤。2.2 随机停点先定结果再定动画我最初写动画时犯过一个典型错误先让指针转几圈停下来之后再随机一个结果印上去。观感非常假因为你眼睛看到指针停在5元优惠券上程序却告诉你中的是蓝牙耳机完全没有关系。正确的抽奖逻辑应该是先把最终停在哪一格用随机数确定下来然后动画在结束前恰好走到那一格。也就是说动画是这个结果的视觉表演不是结果本身。import random final_index random.randint(0, size - 1) # 最终停在下标 final_index pointer 0 while pointer ! final_index: pointer (pointer 1) % size # 加1超出范围就回到0 print(f\r指针指向{prize_pool[pointer]}, end) time.sleep(0.1)其中(pointer 1) % size是循环遍历容器的一个经典写法%取余让指针从最后一格自动绕回第一格这也是环形结构的通用做法。热搜词里有人问循环出来的div一行两个如何算最后两个元素那是把循环用在HTML渲染上的场景原理也类似——你用一个循环生成多个元素再用%判断什么时候该换行属于同一思路。2.3 控制台原地刷新\r回车不换行上面的动画里有一个小细节值得单独说\r。平时print默认是换行输出上一行内容会被顶上去动画会一行一行往下滚很丑。\r可以让光标回到当前行的行首配合end不换行下一次print会覆盖刚才的内容看起来就是原地刷新。如果你在交互式环境里运行建议用sys.stdout.write()加sys.stdout.flush()因为某些环境对print的缓冲不及时动画会卡成一坨。后面完整源码里我会用这种方式。3. 完整可运行的333幸运大转盘V2源码到这里所有关键零件都齐了把它们组装成一个完整程序。这份代码我用的是Python标准库不需要安装任何第三方包复制到任意Python 3环境里就能跑。import random import sys import time PRIZE_POOL [ 谢谢参与, 5元优惠券, 再来一次, 10元红包, 定制笔记本, 谢谢参与, 50元红包, VIP会员月卡, 谢谢参与, 蓝牙耳机, ] # 权重值越大越容易抽到总权重为100方便计算概率 WEIGHTS [32, 18, 12, 10, 8, 7, 5, 4, 3, 1] def show_table(): print( 333幸运大转盘 ) for i, prize in enumerate(PRIZE_POOL, 1): print(f [{i:2}] {prize}) print() def render(pointer): sys.stdout.write(f\r指针正在扫过{PRIZE_POOL[pointer]:6s}) sys.stdout.flush() def spin_to(final_index, rounds4, fast0.06, slow0.15): size len(PRIZE_POOL) pointer 0 # 先快速空转几圈 for _ in range(rounds): for i in range(size): pointer i render(pointer) time.sleep(fast) # 最后慢速走到最终位置 while pointer ! final_index: pointer (pointer 1) % size render(pointer) time.sleep(slow) print() return pointer def play_once(stats, history): show_table() input(按回车开始抽奖...) # 按权重抽取“最终停在哪一格” final_index random.choices(list(range(len(PRIZE_POOL))), weightsWEIGHTS, k1)[0] prize PRIZE_POOL[final_index] spin_to(final_index) print(f中奖结果{prize}) stats[prize] stats.get(prize, 0) 1 history.append(prize) return prize def main(): stats {} history [] total_rounds 0 print(欢迎来到 333 幸运大转盘) while True: prize play_once(stats, history) total_rounds 1 recent - .join(history[-5:]) print(f已抽{total_rounds}次最近5次{recent}) choice input(继续抽奖(y/n)).strip().lower() if choice in (n, no, quit, q): break print(\n抽奖结束统计结果) for p, count in sorted(stats.items(), keylambda x: x[1], reverseTrue): print(f {p}: {count}次) if __name__ __main__: main()几点设计细节值得说明为什么用random.choices而不是random.randint因为真实转盘各奖项概率不同random.choices支持weights权重参数。比如谢谢参与权重32蓝牙耳机权重1长期运行下来后者中奖频率明显更低。如果你用random.randint每个奖是等概率的体验就不够真实。为什么谢谢参与重复出现还按名字统计奖品池里放了3个谢谢参与格子是为了让转盘画面更丰富格子铺得更满。但统计时它们会被合并成一条谢谢参与。这是字典天然的去重效果。如果您想区分格子编号可以在奖品名后面加序号例如谢谢参与(1)。再来一次怎么处理这份代码把再来一次当普通奖品类比了。真实的转盘里如果抽到再来一次一般会再自动抽一次这可以用一个内层while循环实现while prize 再来一次:重新抽。不过要小心极端情况下可能连续抽很多次建议限制次数或者随机权重不要设太高避免死循环。4. 用转盘项目练熟循环控制五个高频必会练习转盘项目你已经跑通了但循环这个知识点建议趁热打铁再练几个经典题。这些题我自己当初都写过也经常在社区热搜里出现它们各自的侧重点不同正好把循环的不同用法补齐。4.1 100以内素数求和内层循环与for-else这道题是循环里的经典送分题用循环找出1到100以内所有素数并计算所有素数之和。核心是判断一个数n是不是素数从2检查到int(n**0.5)1只要能被整除就不是素数。这里有个很特殊的写法是for...else。total 0 for n in range(2, 101): for d in range(2, int(n ** 0.5) 1): if n % d 0: break else: # 循环完整走完、没有被break打断说明没有因子 total n print(total) # 1060for...else的语义是如果循环不是因为break而结束而是自然走完就执行else。这一招在找第一个满足条件/确认没有异常的场景里非常好用很多人不知道Python有这语法其实它比标志位变量写起来干净得多。4.2 斐波那契数列循环里的多元赋值斐波那契每次迭代都需要记住前两项用多元赋值一行搞定a, b 0, 1 for _ in range(20): a, b b, a b print(a, end )a, b b, a b会先计算右边再统一赋值所以不会出现a已经变成b了b的公式算错的问题。如果拆成两行写反而容易踩坑a b b a b # 这时的a已经是旧b了结果不对这就是为什么我在转盘代码里也用pointer (pointer 1) % size这种利用旧值更新新值的写法时注意先取旧值再计算。4.3 李白打酒逆序递推有一道古诗题李白街上走提壶去买酒。遇店加一倍见花喝一斗。三遇店和花喝光壶中酒。问原来壶里有多少酒。题目描述的是一个正向过程但未知量在开始适合逆推最后喝完是0每次倒推一次遇花喝1斗和遇店减半循环3次。wine 0.0 for _ in range(3): wine 1 # 从后往前逆推见花前多1斗 wine / 2 # 遇店前减半 print(wine) # 0.875验证0.875斗进店翻倍变1.75见花喝1斗剩0.75再进店翻倍变1.5喝1斗剩0.5再进店翻倍变1喝1斗刚好0。这个题的特性和转盘动画里的倒推结果很像——先确定最后一步的状态然后一步步往回反推。4.4 求自然常数e的近似值while加浮点数判断热搜里有一道题计算e 1 1/1! 1/2! 1/3! ...直到最后一项的值小于10的-4次方为止。这题用while特别自然因为最后一项是否足够小就是你继续循环的条件。e 1.0 term 1.0 fact 1 n 0 while term 1e-4: n 1 fact * n # n! term 1.0 / fact e term print(e, n) # 大约 2.71828...n为7这里的fact * n是阶乘的递推每次循环只需要乘一个新数不用重新算整个阶乘。注意term是浮点数判断用 1e-4这个1e-4就是10的负4次方。浮点数比较在实际工作中经常遇到建议判断时用term 1e-4这样直接对比不要追求精确等于某个值。4.5 break、continue、else、标志位怎么选学循环最绕的就是控制流程。我按自己的经验给一个选择顺序需求用什么例子提前结束整个循环break用户输入n退出跳过当前这一次进下一次continue抽到谢谢参与时另做处理循环自然结束后再做一件事else素数判断一整轮没break记录是否发生过某件事标志位找到第一个中奖用户后置True直接退出整个函数return在函数里抽奖结束不再续玩continue在转盘项目里也可以有实际用途假设你想统计不包含感谢参与的有效中奖次数在循环开头判断一下抽到谢谢参与就continue不进入统计逻辑。5. 踩过的坑比跑通的代码更值钱循环实战排错写循环写多了谁都会遇到几个让你卡半天的bug。这里把我踩过、也经常在新手代码里见到的坑都列一下你看完能省不少调试点。5.1 while True却忘了break死循环是怎么来的最常见的死循环就是条件永远为真。比如我见过有人把while True:写在主循环里但退出逻辑写成了if answer n: pass整个程序就永远停不下来。解决办法很简单退出时要么break要么返回函数要么把循环条件变量改成False。调试死循环时最简单的方法是先加个print看循环到底走到哪一步或者直接中断运行不要死等。5.2 在循环里修改迭代变量大概率没用很多初学者以为写for i in range(10):之后在循环里加一句i 3就能跳过几次实际上for循环每次是从range里取下一个值你在循环体内改i只是改了当前这个局部变量下一次循环又会重新赋值你的修改基本无效。想跳步用continue或者改用while自己维护计数器。5.3 缩进决定了代码属于谁这是Python特有的坑。同一段代码缩进不同执行次数完全不同for i in range(5): print(内层) # 打印5次 print(外层) # 只打印1次判断缩进的一个技巧是问自己这一句我希望它跟着循环走还是只在循环结束后走一次比如转盘代码里print中奖结果和stats累加应该在循环内部而最后的统计输出应该在主循环外面写岔一个缩进逻辑就乱了。5.4 range的边界左闭右开range(10)产生0到9没有10。range(1, 11)产生1到10。新手最容易把边界搞错尤其是在需要到100的循环里写range(1, 100)结果少算了100。我的习惯是遇到这类题目第一时间把range的起点和终点用print验证一遍特别是要涉及数组最后一个元素时。5.5 浮点精度与重复计算的损耗像求e近似值这类题term是浮点数累加过程中会引入极小的误差但这种误差在工程里通常可接受。如果你要严格判断可以用math.isclose()或者设置一个更大的阈值。另外如果循环体里做了大量重复运算比如每次循环都重新计算len(prize_pool)虽然不会出错但性能很差。更好的做法是在循环之前把size len(prize_pool)算好循环里直接用。转盘动画里的size就是这么处理的。5.6 动画里sleep太多程序卡住的感觉如果你的转盘动画每一帧sleep都设成0.2秒4圈下来需要8秒体验会拖沓。我一般把空转阶段设置成0.04到0.06秒快扫到了接近最终位置时再换成0.15秒慢停。这样动画有节奏感也更像真实转盘减速。也就是完整代码里fast和slow两个参数的区别。6. 这个项目还能往哪儿扩展如果你的转盘程序已经跑通别急着丢开还有几个不错的升级方向顺便把后续要学的知识点提前用到。第一个方向图形界面版。控制台动画虽然有意思但毕竟简陋。可以用tkinter画一个圆形转盘用扇形色块表示每个奖项动画部分用after定时器反复更新角度停止逻辑和我们现在写的完全一样。这里你会接触事件循环的概念其实也是某种意义上的主循环。第二个方向抽奖历史落盘。现在统计结果存在内存里程序一关就没了。把中奖记录按行写入csv文件或者保存成JSON下次启动时加载回来继续累计。这需要用到文件读写也会让你更理解循环处理每一行数据的价值。第三个方向概率模拟验证。手动抽一百次太累你可以写一个模拟函数把play_once里的动画和输入去掉只保留抽奖逻辑循环跑10万次看看各奖项实际概率是否接近权重比例。我试过当总权重为100、样本量10万时各奖项实际频率和理论权重基本一致差别通常在0.1%左右。这个模拟过程本身也是一个while/for循环的实战。我个人做这个项目最大的收获是循环不是一个语法知识点而是一种把重复交给程序的思维习惯。转盘里那些看似必须手动操作的步骤一旦你意识到都是循环代码量立刻少了五分之四。后面你在学列表推导式、生成器、map/filter的时候会看到很多去掉循环的写法但那些写法背后依然是循环的思维——你只有在亲手用循环写过一遍才能真正理解它们省的是什么。这个转盘项目就是很好的起点。
返回列表