又来了,又来了,眼睁睁看着那个进度条在一格一格地挪,像个老头子在公园里散步,不,比那还慢。我电脑的CPU风扇都快起飞了,结果你这Python脚本就给我看这个?那一刻,任凭谁心里都得冒出那句灵魂拷问:Python怎么这么卡?!

这问题,简直是每个从入门到放弃,再到“好吧我认了”的Python开发者心头的一根刺。特别是当你从隔壁C++或者Java阵营过来,那种感觉,就像开惯了赛车突然让你去蹬一辆链条生锈的二八大杠。

别急着砸键盘,也别急着去知乎提问“是不是该转Go了”。这事儿吧,得从根上聊。

首先,得把最大的一个锅甩出来,这锅Python背了,而且背得不冤。它叫——GIL,全局解释器锁(Global Interpreter Lock)

这是个什么鬼东西?

说白了,Python解释器(我们常用的CPython)为了保证线程安全,自己给自己上了一道锁。这道锁霸道得很,它规定:在任何一个时间点,一个Python进程里,只能有一个线程在真正地执行Python字节码。

什么意思?

你电脑就算是八核十六线程的超级怪兽,CPU核心们在那儿一排排地待命,随时准备大干一场。结果呢?你的Python多线程程序,因为这个GIL的存在,就像一个只有一个窗口的银行。尽管你开了十几个线程(叫了十几个号),但柜员(CPU核心)只有一个在干活。其他的线程,就在那排队,等着那个唯一的“执行许可”。场面一度十分尴尬,堪称“一核有难,多-围观”。

所以,当你搞多线程想去做并行计算,期待性能飙升的时候,现实往往是残酷的。对于计算密集型的任务,Python的多线程基本就是个摆设,甚至因为线程切换的开销,比单线程还慢。这就是为啥很多人说Python的多线程是“假”的。

当然,GIL主要影响的是计算密集型场景。如果是IO密集型,比如网络请求、文件读写,那多线程还是有用的。因为一个线程在等网络响应的时候,GIL会释放,让其他线程去干活。这就好比银行柜员在等客户填单子的时候,可以先叫下一个号。

第二个大锅,是Python的解释型语言身份。

这跟编译型语言(比如C++)是两种路子。C++是先把你写的代码完完整整地翻译成机器能直接看懂的机器码(一个.exe文件),执行的时候,CPU直接开干,速度飞快。

Python呢?它是“边执行边翻译”。你写一句,它翻译一句,然后执行一句。中间多了个“解释器”这个翻译官。你想想,一个同声传译,再牛逼,能有提前把稿子全背下来直接说来得快吗?这个“翻译”的过程,本身就是一种开销。每一行代码,都要经过这个流程,你说能不慢吗?

第三点,动态类型

这也是Python被无数人吹捧的优点:写代码真TM爽!一个变量a,上一行可以是整数a = 10,下一行就能是字符串a = 'hello'。不用提前声明类型,方便得一塌糊涂。

但!方便的代价,就是性能的牺牲。

因为类型是动态的,Python解释器在运行的时候,得一直猜,一直检查:“哎,这个a现在是啥类型来着?哦,是个整数,那可以做加法。”“哦豁,现在它变成字符串了,那不能直接加了,得按字符串拼接来。”这个反复确认类型的过程,又是一大笔开销。

而在静态类型语言里,比如C++,你一开始就得告诉编译器int a;a这辈子就是个整数了,敢给它赋值字符串,编译器直接一个大耳刮子扇过来(报错)。这样一来,运行时CPU就知道它是个整数,直接按整数的操作来,没有任何犹豫。

你看,GIL解释型动态类型,这三座大山压下来,Python想跑得快,就像是戴着镣铐跳舞。

那是不是Python就一无是处了?我们就该唾弃它?

当然不是!不然它凭什么能火成现在这样?

问题问的是“Python怎么这么卡”,但更深层的问题其实是:“在知道它卡的情况下,我们为什么还在用它,以及怎么绕过它的‘卡’?”

答案很简单:生态,以及“胶水语言”的定位

你觉得Python慢,是你用“纯Python”在慢。但你有没有想过,你用的那些神级库,比如NumPyPandasSciPyTensorFlow,它们底层是用什么写的?

是C、C++、Fortran!

这些库,把那些最耗费计算资源、最需要性能的部分,用C/C++这些性能怪兽给实现了,然后只暴露一个简单易用的Python接口给你。

你写一行import numpy as np,然后一个简单的数组操作,比如arr * 2。表面上你写的是Python,风平浪静。实际上,底下是NumPy用C语言实现的、优化到极致的循环在疯狂地为你计算,速度快到飞起。这根本就不是Python在跑,是C在跑!

这才是Python的精髓所在。它不追求自己去做所有的脏活累活。它的定位是“胶水”,用它简洁优美的语法,把那些用C/C++写好的高性能模块给粘合起来,构建出一个强大的应用。

所以,一个优秀的Python程序员,他的日常绝对不是写一堆嵌套的for循环去处理百万级的数据。那是菜鸟的写法,不卡才怪。

高手会怎么做?

向量化操作!能用NumPy、Pandas一个函数解决的,绝对不自己写循环。你自己写的那个for循环,是在Python的慢车道上爬行;而NumPy的向量化操作,是直接上了C语言的高速公路。一个天上,一个地下。

用对的数据结构!Python原生的listdict虽然好用,但在某些场景下,性能并不好。学会使用collections模块里的deque,或者在需要极致性能时,考虑用C写一个扩展。

多进程!既然GIL限制了多线程,那对于计算密集型任务,就用multiprocessing库。多进程是操作系统层面的并行,每个进程有自己独立的GIL,互不干扰。这才是真正利用多核的办法。虽然进程间通信麻烦点,但为了性能,值了。

PyPy!如果你有一大堆纯Python代码,实在没法用NumPy之类的库去改写,可以试试PyPy这个JIT(即时编译)解释器。它会在运行时分析你的代码,把那些热点代码编译成机器码,性能提升非常可观,有时候甚至能有好几倍。

所以,回到最初的问题,“Python怎么这么卡?”

因为它本来就不是为你去一个像素一个像素地操作图像、一个字节一个字节地处理音频而设计的。它设计出来,是为了让你用最少的代码、最快的时间,去完成一个想法的原型,去调用那些已经写好的高性能库,去完成复杂的逻辑控制。

它慢,是因为它的哲学是“优化开发人员的时间,而不是优化机器的时间”

当你觉得它卡的时候,多半不是Python的问题,而是你用法不对。你可能正在用Python的短处,去硬碰别人(比如C++)的长处。

所以,下次再有人在你面前抱怨“Python怎么这么卡”,你大概可以给他一个更复杂的眼神,然后悠悠地说:“那是因为,你还没有真正理解Python的强大之处。它慢得有多明显,它在其他地方的优势,就有多香。”

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。