资讯动态

Python进阶实战:环境搭建、数据结构、并发与可视化

发布时间:2026/10/27 5:57:27 来源:尧图企业网站定制
写这篇的时候我一直在想一个问题很多学Python的朋友前几篇跟着教程把语法、函数、类、文件操作都过了一遍到第十篇反而不知道下一步该干什么了。语法都认识但离能做东西总觉得隔着一层。这一篇我就把进阶路上最容易卡住的三类问题一次讲透环境怎么搭才能长期舒坦、数据结构和并发到底怎么用在真实场景里、画图这种小事怎么不翻车。与其继续按语法手册往下念我更愿意直接带你看几个我实际项目中反复用到的组合拳。它们串起来正好是一套能干活的Python基本功。1. 进阶者真正要过的三道坎环境、思维与性能1.1 为什么大多数人的Python水平卡在了会写这一步我带过不少新人也看过很多自学者的代码发现一个共同规律从会写Python到写好Python中间不是缺语法知识而是缺一套稳定的环境和一种工程化思维。先说环境。热搜词里常年霸榜的python安装python环境变量配置python安装numpy库的方法看着是入门问题其实很多人到了进阶阶段还在被它们反复折磨。我最早学Python的时候图省事在系统里装了一个全局解释器今天pip install一个numpy明天又装一个pandas半年后各种包版本互相打架想卸载又怕把别的东西带崩最后只能重装系统。那段经历让我明白一件事环境问题不解决后面学再多都是建在沙子上。再说思维。新手写代码的目标是跑通进阶的目标是扛得住。比如处理一份一万行的数据新手用for循环遍历没问题但放到生产环境处理百万级数据同样的写法可能跑到第二天早上还出不来结果。所谓进阶其实就是把能跑升级成跑得对、跑得快、跑不挂。最后是性能。Python的性能话题很玄说它慢吧它又是AI和数据科学领域的事实标准。这里面的关键不是Python本身慢而是你知不知道在恰当的场合用合适的武器。线程、进程、协程三件事我都踩过坑后面专门用一章讲清楚。1.2 一套能陪我走到精通的开发环境我是这样搭的说到环境我不推荐新手一上来就折腾Docker或者云开发环境性价比太低。我推荐的方案是本地解释器虚拟环境VSCode三件套完全够用到你真正接触到团队协作项目。第一步装Python本体。Windows用户去官网或Microsoft Store安装安装时务必勾选最下面那个Add Python to PATH选项。80%的cmd里输入python没反应都是因为这个没勾上。如果已经装完才发现没勾也可以用安装包重新运行一次选Modify补上或者手动把Python安装目录加到系统环境变量里。Linux用户相对省心Ubuntu/Debian系执行sudo apt install python3就行但要注意系统自带的Python版本往往偏旧。要是你需要安装较新版本我建议用源码编译别直接覆盖系统的/usr/bin/python3那会影响系统工具的运行。网上搜linux系统安装python一大半都是教这个事情的重点就是装到/usr/local不要动系统自带版本。macOS用户建议用Homebrewbrew install python然后让Homebrew的Python排在系统自带Python前面能少踩很多PATH的坑。第二步给每个项目建一个独立的虚拟环境。这一步是从会写到会干活的分水岭。命令很简单python -m venv venvWindows下激活venv\Scripts\activateLinux/macOS下激活source venv/bin/activate激活后你在命令行里输入python走的就是这个项目自己的解释器了pip安装的所有包都只影响当前项目。我最开始不建虚拟环境后来被依赖冲突教育过一次就再也不敢偷懒了。项目哪怕只写一个20行的脚本我也会先建环境再动手——因为你永远不知道这个脚本三个月后会变成什么样子。第三步VSCode配置Python开发环境。VSCode装个官方Python扩展然后CtrlShiftP执行Python: Select Interpreter也就是热搜里那个add python interpreter选择你刚才建好的虚拟环境。再顺便装上Ruff代码检查和格式化插件保存时自动帮我修掉低级语法问题写起来会清爽很多。依赖管理我习惯用最朴素的requirements.txtpip freeze requirements.txt别人拿到项目后执行pip install -r requirements.txt就能复现环境。后面等你进入正经团队了再升级到poetry或者uv也不迟但核心思路不会变依赖是项目的一部分必须跟着项目走。2. 邻接矩阵、数组切片与队列AI数据处理的三块地基2.1 邻接矩阵用二维数组把关系建模很多做AI或数据分析的朋友第一次看到邻接矩阵这个词是在图神经网络或者路径规划相关的资料里。它本身不复杂假设有N个节点我们就建一个N×N的矩阵matrix[i][j]表示从第i个节点到第j个节点有没有边有向图不一定是1或0也可以填权重。举个最直白的例子。假设你手上有4个城市的数据1号和2号之间有直飞航班2号和3号有3号和4号有1号和4号也有。用邻接矩阵表示就是import numpy as np cities [北京, 上海, 广州, 成都] n len(cities) # 方法一手工初始化全0矩阵再填边 adj_matrix np.zeros((n, n), dtypeint) edges [(0, 1), (1, 2), (2, 3), (0, 3)] for i, j in edges: adj_matrix[i][j] 1 adj_matrix[j][i] 1 # 无向图要对称 # 方法二直接用numpy构建 adj_matrix np.array([ [0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0] ])新手最容易犯的错是以为邻接矩阵只能装0和1。实际上在AI场景里它更多用来存权重或者特征值。比如你在做一个路线推荐系统矩阵里存的可以是距离、时间或者拥堵系数第i行第j列的值越大说明两个节点间的代价越大。后面做最短路径算法的时候直接在这个矩阵上跑就行。还有一个很重要的常识真实世界里的图几乎都是稀疏的。一万个节点的图如果只有五万条边用普通二维数组存会浪费四千多万个格子。这种时候就该换成稀疏矩阵表示用scipy.sparse或者干脆用networkx库来管理图结构。但无论用哪个库手工构建邻接矩阵的过程一定要亲自写几遍因为它是N维数组操作和图论算法的交界点写一遍就会对矩阵的形状、索引、方向有肌肉记忆。2.2 数组切片与矩阵操作Numpy高性能操作的入门套路说实话我见过太多人学过Numpy之后就把它忘在脑后遇到矩阵操作还是用list套list手动写循环。这种习惯放到AI项目里是灾难因为Python原生list做矩阵乘法跟Numpy比慢几十倍不止。Numpy的核心操作里第一个必须玩熟的是切片slicing。热搜词里有python数组切片命令说明这是高频疑问。语法就一句话arr[start:stop:step]三个位置都可以省略start缺省从头开始stop缺省到最后step缺省为1。import numpy as np data np.arange(12).reshape(3, 4) print(data) # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]] # 取第2行索引从0开始 row data[1, :] # [4 5 6 7] # 取第2列到第3列的所有行 cols data[:, 1:3] # [[1 2], [5 6], [9 10]] # 反向切片把第3列倒序拿出来 reversed_col data[::-1, 2]这里有个所有教程都会强调但我偏要再强调一遍的坑切片的默认行为是视图不是副本。也就是说你改切片出来的子数组原来的数组也会跟着变。拿到别人的数据、想先截一段做分析临时改一改的时候很多人就是在这里被坑的。要真正复制一份数据必须显式调用.copy()sub data[1:, :2].copy() sub[0, 0] 999 print(data) # 原数组不受影响矩阵操作里还有几个高频命令reshape改变形状、transpose或.T做转置、dot或做矩阵乘法。比如在AI里做批量数据处理时常需要把一行数据从行向量变成列向量很多人会写data[:, np.newaxis]其实.reshape(-1, 1)更直观。记住一句话凡是涉及二维以上数组形状变换的操作先停下来想一下你想得到的是形状、方向还是值再决定用什么函数能少改很多bug。2.3 Queue队列的不堵塞真相从同步阻塞到超时处理热搜词里有个python队列queue不堵塞这个关键词背后肯定藏着一个懵圈的朋友。我直接说结论queue.Queue本身是线程安全的FIFO队列但它的get()方法默认是阻塞的。所谓不堵塞是你主动用非阻塞方法或者超时机制实现的不是它天生就非阻塞。典型的生产者-消费者模型代码长得是这样import queue import threading import time q queue.Queue(maxsize10) def producer(): for i in range(5): q.put(i) print(f生产: {i}) time.sleep(0.1) def consumer(): while True: try: item q.get(timeout2) print(f消费: {item}) q.task_done() except queue.Empty: # 超时2秒还没拿到东西退出循环 break t1 threading.Thread(targetproducer) t2 threading.Thread(targetconsumer) t1.start() t2.start() t1.join() t2.join()这里get(timeout2)就是不堵塞的标准做法队列为空时线程不会无限卡死等两秒就抛一个queue.Empty异常让你处理退出逻辑。get_nowait()则是立刻返回队列空就直接抛异常适用于你确定当前不该等待的场景比如UI线程里定时去轮询队列。我踩过的坑是task_done()和join()搭配错位。q.join()会一直阻塞直到队列里所有任务都被task_done()标记为完成。如果你只get()却不调task_done()那么join()会永远等下去程序就假死了。当时我以为队列的get已经把任务拿走、队列空了join就该返回结果发现完全不是这样。get和task_done是一对缺一个就出事。3. 协程与嵌套线程把并发写清楚的三种姿势3.1 线程嵌套线程的踩坑记录异常、爆炸与退不出的循环先说一个我在真实项目里遇到的问题批量下载几百个文件我为每个大文件开了一个线程然后每个线程内部又为了并发加速给文件切片再开了几个小线程。结果程序运行到一半突然沉默了几分钟然后炸出一堆报错。排查了半天根因有两个第一内部线程的异常不会自动传给外层线程。外层线程看到内层线程异常退出不会知道原因主线程更不知道整个流程在某个子任务失败后陷入僵局。你很难定位到底哪个切片失败了。第二线程数随任务量指数级膨胀。十个大任务、每个开五个小任务就是五十个线程再叠加每批次波动线程调度开销比干活还大系统负载直线上升。线程嵌套本身不是不能用但必须有一个明确的管理层。现在我的建议是能用单层就只用单层。外层用ThreadPoolExecutor管理任务每个任务内部不要再开线程而是把细粒度任务投递回到同一个线程池或者最多用一层受限的嵌套。这是典型的需要控制扇出的场景。给你看一个正面写法from concurrent.futures import ThreadPoolExecutor, as_completed def download(url): # 单个下载任务不再开线程 return url def batch_download(urls): with ThreadPoolExecutor(max_workers8) as executor: futures [executor.submit(download, url) for url in urls] for future in as_completed(futures): try: print(future.result()) except Exception as e: print(f任务失败: {e})这一版的好处是所有异常都汇流到as_completed循环里处理哪些任务失败一目了然线程池也会在with块退出时自动回收。3.2 async/await协程用单线程搞定高并发IO协程是个好东西但很多文章把它讲玄了。我尽量用最直白的话说协程就是单线程里的分时复用。你的程序碰到await关键字的时候知道自己要等网络请求或者读写文件返回这段时间CPU闲着也是闲着干脆跳去执行另一个任务。等网络响应回来了再回过头继续往下走。写异步代码只需要记住三件事用async def定义协程函数用await调用另一个协程用asyncio.run()或事件循环驱动整个程序。import asyncio import httpx async def fetch_one(url): async with httpx.AsyncClient(timeout10) as client: resp await client.get(url) return url, resp.status_code async def main(): urls [ https://api.example.com/data/1, https://api.example.com/data/2, https://api.example.com/data/3, ] tasks [fetch_one(url) for url in urls] results await asyncio.gather(*tasks) for url, code in results: print(f{url} - {code}) asyncio.run(main())注意requests库是同步的阻塞线程的那套逻辑放进async代码里不会自动变成异步。要在协程里发HTTP请求请使用httpx或aiohttp这两个异步版本客户端。这个坑我见人踩过无数次写了个async函数里面用的还是requests.get()结果并发请求把所有协程卡成了串行性能一点没提升。3.3 选型表什么场景用线程、进程还是协程这个表是我自己实践后整理的不一定适合所有场景但作为初始选型参考90%的情况够用场景类型首选方案备选方案原因网络请求、文件读写等IO密集型协程asyncio httpx/aiohttpThreadPoolExecutor协程单线程开销最小并发量高简单并发脚本、任务数量几十个ThreadPoolExecutor协程线程池API简单好理解好维护CPU密集计算图像处理、数值计算ProcessPoolExecutormultiprocessing直接开进程绕开GIL限制多核并行混合型负载协程并发IO run_in_executor丢计算任务给进程池拆成两个独立服务让合适的东西去合适的执行器有一句忠告想写给刚进阶的朋友别一上来就追求异步。如果你到现在还没吃透同步代码建议先把阻塞任务丢进线程池这条路走通再碰async/await。异步代码调试起来比同步难得多堆栈信息会绕好几个弯报错原因藏在事件循环深处对新手很不友好。4. 李白打酒这类递归题练的是AI算法思维4.1 原题、数学建模与暴力枚举思路李白打酒是经典的算法练习题。原题大意李白提着酒壶出门买酒遇到酒店就把酒量翻一倍遇到花就喝掉一斗。已知他出门时壶中有一定量的酒总共遇到酒店和花若干次经典说法是三遇店和花最后恰好喝光。问壶中原有多少酒这道题在网络上的变体非常多热搜词李白打酒python大多是问怎么用代码枚举所有可能过程的。常见的版本是壶中初始不知道有多少酒一共遇到店5次、花10次最后壶中酒喝光问初始酒量有多少种可能。先做数学建模。定义wine为当前壶中酒量shop为已经遇到的店数flower为已经遇到的花数。初始状态是(wine, 0, 0)目标是(0, 5, 10)且最后一个动作必须是遇花。每走一步有两种选择def solve(wine, shop, flower, path): if shop 5 and flower 10: if wine 0: solutions.append(path.copy()) return # 遇店酒量翻倍 if shop 5: solve(wine * 2, shop 1, flower, path [店]) # 见花喝掉一斗 if flower 10 and wine 0: solve(wine - 1, shop, flower 1, path [花])直接暴力枚举的话把wine从0到某个上限都试一遍每个分支都递归下去也能搜出答案。但效率不高而且初始值范围不好定。更好的思路是逆向倒推从最终状态酒为0、店5花10开始往回推遇花的逆操作是加1斗遇店的逆操作是减半。倒推的好处是状态空间大幅缩小因为路径已经固定为那条唯一的过程序列我们只需要验证每种可能的初始值能否走到终点。实际代码里用DFS枚举过程更通用因为它可以处理店和花顺序有多种可能的版本——这种版本问的是有多少种行走顺序能让最后喝光答案往往是一个几千上万的大数正是搜索算法的用武之地。4.2 递归剪枝把状态空间一步步压小讲到DFS枚举就一定要讲剪枝。不剪枝的机器人会把所有可能路径都走一遍状态空间是指数级的。以店5次、花10次、最后喝光为例总共15步每一步只有两个选项全部枚举是2的15次方也就是32768种可能这数量其实还好。但如果你把规模扩大到店和花各几十次指数增长就变成天文数字了。剪枝的本质是提前判断某条路不可能到达终点直接放弃。以这道题为例我常用的剪枝条件有三个酒量变成负数直接不可能。酒量不是整数直接不可能遇店翻倍后酒量一定是整数减半后出现小数说明这条路径的倒推状态不合法。剩下的店和花数量已经少于还能做的步数或者剩余酒量太大花全喝完也清不掉直接不可能。# 完整可运行版本统计方案数 def count_solutions(wine, shop, flower): if wine 0 or wine % 1 ! 0: return 0 if shop 5 and flower 10: return 1 if wine 0 else 0 if shop 5 and wine ! flower: # 已无店可遇酒量必须恰好等于剩余花数 return 1 if wine flower else 0 if wine 10 - flower: # 酒喝不完了剪枝 return 0 total 0 if shop 5: total count_solutions(wine * 2, shop 1, flower) if flower 10: total count_solutions(wine - 1, shop, flower 1) return total # 尝试初始酒量 1~20 for w in range(1, 21): cnt count_solutions(w, 0, 0) if cnt 0: print(f初始酒量 {w} 斗共有 {cnt} 种过程满足条件)这个版本里最关键的几行是wine 10 - flower这个剪枝剩余的见花次数最多能消耗等于次数的酒量如果当前酒量超过这个数后面无论如何都喝不完。剪枝条件写得好指数级搜索可以被压成多项式级的遍历这在人工智能的很多搜索算法里是同样的思路。4.3 回溯思想在AI路径规划与贝叶斯搜索中的影子学递归题的价值不在于背答案而在于练状态设计、剪枝、回溯这套方法。这套方法在AI领域到处都是。路径规划算法比如机器人室内导航本质上就是在网格或拓扑图上做深度优先或广度优先搜索每走一步就是一种状态转移撞墙就回溯换路。数独求解是典型的回溯填一个空位、检查约束、不合法就回退。在国际象棋和围棋AI里蒙特卡洛树搜索MCTS更是把探索-回溯-再探索这个循环放大到了极致AlphaGo就是靠这个思路赢下棋局的。包括贝叶斯优化的搜索策略也脱离不了采样-评估-根据结果决定下一步采样范围的循环。所以我的建议是刷题的时候不要只刷数量每做一道递归/回溯题问自己三个问题——状态是什么转移动作有哪些约束条件能不能帮我剪枝想清楚这三个问题比默写十道题都有用。5. 让图表不再翻车matplotlib的横坐标与出图细节5.1 横坐标太密集先看数据量再看刻度策略画个图本身不难难的是画一张能看、能交差、能进报告的图。多数人第一次用matplotlib画时间序列时都会碰到一个问题横坐标挤成一团黑压压的竖线打出来根本没法看。python画图横坐标太密集这个热搜词背后至少站着几千个深夜改图的同学。为什么会密集原因无非三类数据点太多刻度自动生成时取了太多个位置。横轴标签是长字符串比如带时分秒的时间戳每根柱子都标上就叠字了。日期型坐标没有格式化matplotlib直接把整数时间戳打印出来。解决方案我都试过最有效的三个手段import matplotlib.pyplot as plt import numpy as np x np.arange(100) y np.sin(x / 5) plt.figure(figsize(10, 4)) # 手段一限制刻度的最大数量 plt.locator_params(axisx, nbins10) # 手段二手动指定刻度间隔 plt.xticks(ticksnp.arange(0, 100, 10)) # 手段三旋转标签并右对齐缓解长标签叠字 plt.xticks(rotation45, haright) plt.plot(x, y) plt.tight_layout() plt.show()实际项目中我习惯先nbins10把刻度数量压下来再根据需求手动补关键节点比如月度数据我就只标每年一月和七月。别指望matplotlib自动选刻度选得好审美这回事它真的学不会。如果横轴是日期时间还需要额外用matplotlib.dates格式化。这里有一个绕不开的坑pandas的DatetimeIndex直接传给matplotlib时默认刻度标签是一长串数字。要显示成2024-01这种格式得先用mdates.DateFormatter设置格式再用mdates.MonthLocator指定间隔import matplotlib.dates as mdates fig, ax plt.subplots() ax.plot(df.index, df[value]) ax.xaxis.set_major_locator(mdates.MonthLocator(interval1)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) plt.xticks(rotation30, haright) plt.show()5.2 保存图片格式、dpi、bbox_inches和中文乱码画图只是第一步把图保存成文件往往是最后一步也是翻车重灾区。plt.savefig()和plt.show()的坑在于很多人点完show()手动关掉窗口再去savefig()存下来的经常是一张空白图。正确做法是先savefig再show或者给savefig传figure引用。保存参数各有各的用处参数常用值应用场景dpi150网页、300论文/打印控制清晰度bbox_inchestight自动裁剪空白避免坐标标签被砍掉transparentTrue在背景色非白色的页面上嵌入图片formatpng/pdf/svg位图和矢量图之间的取舍plt.savefig(output.png, dpi300, bbox_inchestight)格式选择也有讲究。网页和PPT里用PNG最省心论文投稿建议用PDF矢量图这样放大到任意尺寸都不会糊编辑也挑不出毛病。SVG适合Web项目因为它是浏览器直接支持的矢量格式。中文乱码是另一大痛点。matplotlib默认字体不含中文字符任何中文标签都会变成方框。最常见的解决方法是显式指定一个系统自带中文字体plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, WenQuanYi Zen Hei] plt.rcParams[axes.unicode_minus] False # 解决负号显示成方框的问题在Windows上SimHei和Microsoft YaHei基本都有Linux服务器上的matplotlib出中文乱码通常是系统没装中文字体先apt install fonts-wqy-zenhei再指定字体就能解决。macOS可以用PingFang SC或Arial Unicode MS。5.3 一张能直接进报告的图需要做的五件事被导师、甲方、读者反复教育之后我现在出图前固定检查五件事全局设置字体和字号。标题不小于14坐标轴标签不小于12不然图插到PPT里一缩放就什么都看不清。坐标轴要有单位和含义。很多人只写时间和数值不如写时间月和收入万元一目了然。网格线能帮读图但别喧宾夺主。plt.grid(True, alpha0.3)比默认网格线淡很多既能对齐数据点又不抢主线。保存参数统一。我用固定模板dpi300加bbox_inchestight一张图只出了一个尺寸不会因为窗口大小变化导致图幅忽大忽小。核心结论直接在图上标注。在峰值点加一个annotate写一句Max: 12.5比让读者自己去找峰值友好得多。import matplotlib.pyplot as plt import numpy as np def build_chart(x, y, title, xlabel, ylabel, output_path): plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(8, 5)) ax.plot(x, y, markero, linewidth2) ax.set_title(title, fontsize14) ax.set_xlabel(xlabel, fontsize12) ax.set_ylabel(ylabel, fontsize12) ax.grid(True, alpha0.3) ax.locator_params(axisx, nbins8) max_idx int(np.argmax(y)) ax.annotate(fMax: {y[max_idx]:.2f}, xy(x[max_idx], y[max_idx]), xytext(x[max_idx] - 2, y[max_idx] 1), arrowpropsdict(arrowstyle-)) fig.tight_layout() fig.savefig(output_path, dpi300, bbox_inchestight) plt.close(fig)把画图封装成一个纯函数传参数出图是我强烈推荐的习惯。好处很明显不用每次重画都复制粘贴一大段配置改参数只要改入口。6. 综合实战构建距离矩阵热力图一条龙小项目6.1 需求设计与方案取舍怎么把散装知识串成项目最后用一个小项目把这篇文章的知识点串起来。我做的是城市两两距离矩阵可视化手头有若干城市的经纬度计算每两个城市之间的球面距离构建距离矩阵然后画一张热力图存成PNG文件。这个项目虽然小但五脏俱全搭建邻接/距离矩阵用到第2章的矩阵思维。用ThreadPoolExecutor并发计算多对城市距离用到第3章的并发选型虽然计算量小主要是演示结构。用matplotlib画热力图用到第5章的出图细节。数据来源我直接用内置经纬度不涉及网络请求这样人人都能跑通。你想换成真实数据把城市列表换成从公开地图API拉回来的坐标就行。它的价值不在于难而在于你能在20分钟内从零跑通一个输入城市列表输出一张能看的矩阵图的完整流程。6.2 核心代码与关键点注释import numpy as np import matplotlib.pyplot as plt from concurrent.futures import ThreadPoolExecutor, as_completed # 城市名称 - (纬度, 经度) cities { 北京: (39.9042, 116.4074), 上海: (31.2304, 121.4737), 广州: (23.1291, 113.2644), 成都: (30.5728, 104.0668), 武汉: (30.5928, 114.3055), 西安: (34.3416, 108.9398), } # 半正矢公式Haversine计算球面距离单位公里 def haversine(lat1, lon1, lat2, lon2): R 6371.0 p1 np.radians(lat1) p2 np.radians(lat2) dp np.radians(lat2 - lat1) dl np.radians(lon2 - lon1) a np.sin(dp / 2) ** 2 np.cos(p1) * np.cos(p2) * np.sin(dl / 2) ** 2 return 2 * R * np.arcsin(np.sqrt(a)) names list(cities.keys()) n len(names) # 任务列表计算所有城市对的距离 tasks [(i, j) for i in range(n) for j in range(i 1, n)] dist_matrix np.zeros((n, n)) # 用线程池并发计算距离对称矩阵只需算上三角 def calc(task): i, j task lat1, lon1 cities[names[i]] lat2, lon2 cities[names[j]] return i, j, haversine(lat1, lon1, lat2, lon2) with ThreadPoolExecutor(max_workers4) as executor: futures [executor.submit(calc, t) for t in tasks] for future in as_completed(futures): i, j, dist future.result() dist_matrix[i][j] dist dist_matrix[j][i] dist # 画热力图 fig, ax plt.subplots(figsize(8, 6)) im ax.imshow(dist_matrix, cmapYlOrRd, interpolationnearest) ax.set_xticks(range(n)) ax.set_yticks(range(n)) ax.set_xticklabels(names, rotation45, haright) ax.set_yticklabels(names) # 在格子中心标注数值 for i in range(n): for j in range(n): ax.text(j, i, f{dist_matrix[i][j]:.0f}, hacenter, vacenter, colorblack, fontsize9) fig.colorbar(im, axax, shrink0.8) fig.suptitle(城市间球面距离矩阵公里, fontsize14) fig.tight_layout() fig.savefig(distance_matrix.png, dpi200, bbox_inchestight) plt.close(fig) print(dist_matrix)代码里几个细节值得讲一下我用interpolationnearest保留了格子的清晰边界热力图边缘不会糊成渐变更适合看矩阵格子。因为矩阵是对称的tasks只生成了i j的上三角任务计算量几乎省了一半。plt.close(fig)是主动关图释放内存。如果你在循环里连续画几十张图又不关内存会明显上涨。6.3 跑通之后还可以往三个方向进阶这个demo跑通后想继续往精通方向走我建议按以下顺序扩展第一把距离矩阵喂给图算法库networkx用nx.from_numpy_array把矩阵变成图对象然后直接跑最短路径Dijkstra、最小生成树MST这些经典算法。这是从数据结构走向算法应用最自然的一步。第二把并发部分替换成协程。如果你的项目从城市坐标内置换成从公开API批量获取坐标网络IO就变成核心瓶颈了此时用httpx.AsyncClient配合asyncio.gather来并发请求性能会比线程池再上一个台阶。注意把while Truesleep这套同步逻辑换掉异步代码不能按同步思维写。第三把可视化从静态热力图升级成交互式地图。用folium把城市经纬度直接画到真实地图上点与点之间的连线粗细表示距离远近这样输出比一张PNG热力图直观得多做汇报展示的时候效果拔群。我自己写这个例子最大的体会是知识不串起来就是死的。当你把数组切片、矩阵、并发、画图一个个知识点放进一个真实能跑的小项目里它们之间会自然产生化学反应。那些热搜词里的问题比如python构建邻接矩阵python画图横坐标太密集python队列queue不堵塞本质上都是某个项目链条上断掉的一环。把一环一环扣回去进阶的路就走顺了。你手头如果有想练的手上项目建议也按这个思路拆一拆明确输入输出挑一把合适的并发工具把结果可视化出来然后存好图、写好注释让它成为你作品集里的一张牌。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑