免费python在线观看 源码
一、Python语言的魅力
让我们来谈谈Python语言的魅力所在。作为一门动态类型的语言,Python的设计哲学强调代码的可读性和简洁的语法(尤其是使用空格缩进划分代码块,而不是使用大括号或关键字)。这使得Python成为了编写简单脚本和快速原型开发的理想选择。
二、免费在线看Python源码的途径
接下来,我要为大家介绍几种免费的在线看Python源码的平台。其中最具代表性的就是GitHub了,它是一个面向开源及私有软件项目的托管平台,我们可以在这里找到大量的Python项目的源码,通过阅读他人的源码,可以更好地理解Python语言的特性和使用方法。
还有一些在线教育平台也提供了Python课程的学习,比如Coursera、慕课网等,这些平台上的课程通常会有详细的视频讲解和配套的源代码供我们学习和实践。
三、如何将Python源码生成html格式
我们来谈一谈如何将Python源码生成HTML格式。其实,这个过程并不复杂,我们可以使用一些工具来实现这个目标。
让Python速度提高100倍,只需不到100行Rust代码!
北京不少程序员都抱怨Python代码跑的慢,尤其是当处理的数据集比较大的时候。对此,本文作者指出:只需不到100行Rust代码就能解决这个问题。
原文链接:https://ohadravid.github.io/posts/2023-03-rusty-python/
未经授权,禁止转载!
作者|OhadRavid译者|弯月责编|郑丽媛出品|CSDN(ID:CSDNnews)最近,我们的一个核心Python库遇到了性能问题。这是一个非常庞大且复杂的库,是我们3D处理管道的支柱,使用了NumPy以及其他Python数据科学库来执行各种数学和几何运算。
具体来说,我们的系统必须在CPU资源有限的情况下在本地运行,虽然起初的性能还不错,但随着并发用户数量的增长,我们开始遇到问题,系统也出现了超负载。
我们得出的结论是:系统至少需要再快50倍才能处理这些增加的工作负载——而我们认为,Rust可以帮助我们实现这一目标。
因为我们遇到的性能问题很常见,所以下面,我来简单介绍一下解决过程:
(a)基本的潜在问题;
(b)我们可以通过哪些优化来解决这个问题。
我们的运行示例
首先,我们通过一个小型库来展示最初的性能问题。
假设有一个多边形列表和一个点列表,且都是二维的,出于业务需求,我们需要将每个点“匹配”到一个多边形。
我们的库需要完成下列任务:
?从点和多边形的初始列表(全部为2D)着手。
?对于每个点,根据与中心的距离,找到离点最近的多边形的子集。
?从这些多边形中,选择一个“最佳”多边形。
代码大致如下:
fromtypingimportList,Tupleimportnumpyasnpfromdataclassesimportdataclassfromfunctoolsimportcached_propertyPoint=np.array@dataclassclassPolygon:x:np.arrayy:np.array@cached_propertydefcenter(self)->Point:...defarea(self)->float:...deffind_close_polygons(polygon_subset:List[Polygon],point:Point,max_dist:float)->List[Polygon]:...defselect_best_polygon(polygon_sets:List[Tuple[Point,List[Polygon]]])->List[Tuple[Point,Polygon]]:...defmain(polygons:List[Polygon],points:np.ndarray)->List[Tuple[Point,Polygon]]:...性能方面最主要的难点在于,Python对象和numpy数组的混合。
下面,我们简单地分析一下这个问题。
需要注意的是,对于上面这段代码,我们当然可以把一切都转化成numpy的向量计算,但真正的库不可能这么做,因为这会导致代码的可读性和可修改性大大降低,收益也非常有限。此外,使用任何基于JIT的技巧(PyPy/numba)产生的收益都非常小。
为什么不直接使用Rust重写所有代码?虽然重写所有代码很诱人,但有一些问题:
?该库的大量计算使用了numpy,Rust也不一定能提高性能。
?该库庞大而复杂,关系到核心业务逻辑,而且高度算法化,因此重写所有代码需要付出几个月的努力,而我们可怜的本地服务器眼看就要挂了。
?一群好心的研究人员积极努力改进这个库,实现了更好的算法,并进行了大量实验。他们不太愿意学习一门新的编程语言,而且还要等待编译,还要研究复杂的借用检查器——他们不希望离开舒适区太远。
小心探索下面,我来介绍一下我们的分析器。
Python有一个内置的Profiler(cProfile),但对于我们来说,选择这个工具不太合适:
?它会为所有Python代码引入大量开销,却不会给原生代码带来额外开销,因此测试结果可能有偏差。
?我们将无法查看原生代码的调用帧,这意味着我们也无法查看Rust代码。
所以,我们计划使用py-spy,它是一个采样分析器,可以查看原生帧。他们还将预构建的轮子发布到了pypi,因此我们只需运行pipinstallpy-spy即可。
此外,我们还需要一些测量指标。
#measure.pyimporttimeimportpoly_matchimportos#Reducenoise,actuallyimproveperfinourcase.os.environ["OPENBLAS_NUM_THREADS"]="1"polygons,points=poly_match.generate_example()#Wearegoingtoincreasethisasthecodegetsfasterandfaster.NUM_ITER=10t0=time.perf_counter()for_inrange(NUM_ITER):poly_match.main(polygons,points)t1=time.perf_counter()took=(t1-t0)/NUM_ITERprint(f"Tookandavgof{took*1000:.2f}msperiteration")这些测量指标虽然不是很科学,但可以帮助我们优化性能。
“我们很难找到合适的测量基准。但请不要过分强调拥有完美的基准测试设置,尤其是当你优化某个程序时。”
——NicholasNethercote,《TheRustPerformanceBook》
运行该脚本,我们就可以获得测量基准:
$pythonmeasure.pyTookanavgof293.41msperiteration对于原来的库,我们使用了50个不同的样本来确保涵盖所有情况。
这个测量结果与实际的系统性能相符,这意味着,我们的工作就是突破这个数字。
我们还可以使用PyPy进行测量:
$condacreate-npypyenv-cconda-forgepypynumpycondaactivatepypyenv$pypymeasure_with_warmup.pyTookanavgof1495.81msperiteration先测量
首先,我们来找出什么地方如此之慢。
$py-spyrecord--native-oprofile.svg--pythonmeasure.pySamplingprocess100timesasecond.PressControl-Ctoexit.Tookanavgof365.43msperiterationStoppedsamplingbecauseprocessexitedWroteflamegraphdatato'profile.svg'.Samples:391Errors:0我们可以看到开销非常小。相较而言,使用cProfile得到的数据如下:
$python-mcProfilemeasure.pyTookanavgof546.47msperiteration7551778functioncalls(7409483primitivecalls)in7.806seconds…下面是我们获得的火焰图:
每个方框都是一个函数,我们可以看到每个函数花费的相对时间,包括它正在调用的函数(沿着图形/栈向下)。
要点总结:
?绝大部分时间花在find_close_polygons上。
?大部分时间都花在执行norm,这是一个numpy函数。
下面,我们来仔细看看find_close_polygons:
deffind_close_polygons(polygon_subset:List[Polygon],point:np.array,max_dist:float)->List[Polygon]:close_polygons=[]forpolyinpolygon_subset:ifnp.linalg.norm(poly.center-point)close_polygons.append(poly)returnclose_polygons我们打算用Rust重写这个函数。
在深入细节之前,请务必注意以下几点:
?此函数接受并返回复杂对象(Polygon、np.array)。
?对象的大小非常重要(因此复制需要一定的开销)。
?这个函数被调用了很多次(所以我们引入的开销可能会引发问题)。
我的第一个Rust模块PyO3是一个用于Python和Rust之间交互的crate,拥有非常好的文档。
我们将调用自己的poly_match_rs,并添加一个名为find_close_polygons的函数。
mkdirpoly_match_rscd"$_"pipinstallmaturinmaturininit--bindingspyo3maturindevelop刚开始的时候,我们的crate大致如下:
usepyo3::prelude::*;#[pyfunction]fnfind_close_polygons()->PyResult)>{Ok(())}#[pymodule]fnpoly_match_rs(_py:Python,m:PyModule)->PyResult{m.add_function(wrap_pyfunction!(find_close_polygons,m)?)?;Ok(())}我们还需要记住,每次修改Rust库时都需要执行maturindevelop。
改动就这么多。下面,我们来调用新函数,看看情况会怎样。
>>>poly_match_rs.find_close_polygons(polygons,point,max_dist)ETypeError:poly_match_rs.poly_match_rs.find_close_polygons()takesnoarguments(3given)第一版:Rust转换
首先,我们来定义API。
PyO3可以帮助我们将Python转换成Rust:
#[pyfunction]fnfind_close_polygons(polygons:Vec,point:PyObject,max_dist:f64)->PyResult>>{Ok(vec![])}PyObject(顾名思义)是一个通用、“一切皆有可能”的Python对象。稍后,我们将尝试与它进行交互。
这样程序应该就可以运行了(尽管不正确)。
我直接把原来的Python函数复制粘帖进去,并修复了语法问题。
#[pyfunction]fnfind_close_polygons(polygons:Vec,point:PyObject,max_dist:f64)->PyResult>{letmutclose_polygons=vec![];forpolyinpolygons{ifnorm(poly.center-point)<max_dist{close_polygons.push(poly)}}Ok(close_polygons)}可惜未能通过编译:
%maturindevelop...error[E0609]:nofield`center`ontype`Py`-->src/lib.rs:8:22|8|ifnorm(poly.center-point)<max_dist{|^^^^^^unknownfielderror[E0425]:cannotfindfunction`norm`inthisscope-->src/lib.rs:8:12|8|ifnorm(poly.center-point)<max_dist{|^^^^notfoundinthisscopeerror:abortingdueto2previouserrors]58/59:poly_match_rs我们需要3个crate才能实现函数:
#ForRust-nativearrayoperations.ndarray="0.15"#Fora`norm`functionforarrays.ndarray-linalg="0.16"#Foraccessingnumpy-createdobjects,basedon`ndarray`.numpy="0.18"首先,我们将point:PyObject转换成可以使用的东西。
我们可以利用PyO3来转换numpy数组:
usenumpy::PyReadonlyArray1;#[pyfunction]fnfind_close_polygons(//Anobjectwhichsays"IhavetheGIL",sowecanaccessPython-managedmemory.py:Python,polygons:Vec,//Areferencetoanumpyarraywewillbeabletoaccess.point:PyReadonlyArray1,max_dist:f64,)->PyResult<Vec<PyObject>>{//Convertto`ndarray::ArrayView1`,afullyoperationalnativearray.letpoint=point.as_array();...}现在point变成了ArrayView1,我们可以直接使用了。例如:
//Makethe`norm`functionavailable.usendarray_linalg::Norm;assert_eq!((point.to_owned()-point).norm(),0.);接下来,我们需要获取每个多边形的中心,然后将其转换成ArrayView1。
letcenter=poly.getattr(py,"center")?//Python-stylegetattr,requiresaGILtoken(`py`)..extract::>(py)?//TellPyO3whattoconverttheresultto..as_array()//Like`point`before..to_owned();//Weneedoneofthesidesofthe`-`tobe"owned".虽然信息量有点大,但总的来说,结果就是逐行转换原来的代码:
usepyo3::prelude::*;usendarray_linalg::Norm;usenumpy::PyReadonlyArray1;#[pyfunction]fnfind_close_polygons(py:Python,polygons:Vec,point:PyReadonlyArray1,max_dist:f64,)->PyResult>{letmutclose_polygons=vec![];letpoint=point.as_array();forpolyinpolygons{letcenter=poly.getattr(py,"center")?.extract::>(py)?.as_array().to_owned();if(center-point).norm()<max_dist{close_polygons.push(poly)}}Ok(close_polygons)}对比一下原来的代码:
deffind_close_polygons(polygon_subset:List[Polygon],point:np.array,max_dist:float)->List[Polygon]:close_polygons=[]forpolyinpolygon_subset:ifnp.linalg.norm(poly.center-point)<max_dist:close_polygons.append(poly)returnclose_polygons我们希望这个版本优于原来的函数,但究竟有多少提升呢?
$(cd./poly_match_rs/maturindevelop)$pythonmeasure.pyTookanavgof609.46msperiteration看起来Rust非常慢?实则不然,使用maturindevelop--release运行,就能获得更好的结果:
$(cd./poly_match_rs/maturindevelop--release)$pythonmeasure.pyTookanavgof23.44msperiteration这个速度提升很不错啊。
我们还想查看我们的原生代码,因此发布时需要启用调试符号。即便启用了调试,我们也希望看到最大速度。
#addedtoCargo.toml[profile.release]debug=true#Debugsymbolsforourprofiler.lto=true#Link-timeoptimization.codegen-units=1#Slowercompilationbutfastercode.第二版:用Rust重写更多代码接下来,在py-spy中通过--native标志,查看Python代码与新版的原生代码。
再次运行py-spy:
$py-spyrecord--native-oprofile.svg--pythonmeasure.pySamplingprocess100timesasecond.PressControl-Ctoexit.这次得到的火焰图如下所示(添加红色之外的颜色,以方便参考):
看看分析器的输出,我们发现了一些有趣的事情:
1.find_close_polygons::...::trampoline(Python直接调用的符号)和__pyfunction_find_close_polygons(我们的实现)的相对大小。
?可以看到二者分别占据了样本的95%和88%,因此额外开销非常小。
2.实际逻辑(if(center-point).norm()<max_dist{...})是lib_v1.rs:22(右侧非常小的框),大约占总运行时间的9%。
?所以应该可以实现10倍的提升。
3.大部分时间花在lib_v1.rs:16上,它是poly.getattr(...).extract(...),可以看到实际上只是getattr以及使用as_array获取底层数组。
也就是说,我们需要专心解决第3点,而解决方法是用Rust重写Polygon。
我们来看看目标类:
@dataclassclassPolygon:x:np.arrayy:np.array_area:float=None@cached_propertydefcenter(self)->np.array:centroid=np.array([self.x,self.y]).mean(axis=1)returncentroiddefarea(self)->float:ifself._areaisNone:self._area=0.5*np.abs(np.dot(self.x,np.roll(self.y,1))-np.dot(self.y,np.roll(self.x,1)))returnself._area我们希望尽可能保留现有的API,但我们不需要area的速度大幅提升。
实际的类可能有其他复杂的东西,比如merge方法——使用了scipy.spatial中的ConvexHull。
为了降低成本,我们只将Polygon的“核心”功能移至Rust,然后从Python中继承该类来实现API的其余部分。
我们的struct如下所示:
//`Array1`isa1darray,andthe`numpy`cratewillplaynicelywithit.usendarray::Array1;//`subclass`tellsPyO3toallowsubclassingthisinPython.#[pyclass(subclass)]structPolygon{x:Array1,y:Array1,center:Array1,}下面,我们需要实现这个struct。我们先公开poly.{x,y,center},作为:
?属性
?numpy数组
我们还需要一个constructor,以便Python创建新的Polygon:
usenumpy::{PyArray1,PyReadonlyArray1,ToPyArray};#[pymethods]implPolygon{#[new]fnnew(x:PyReadonlyArray1,y:PyReadonlyArray1)->Polygon{letx=x.as_array();lety=y.as_array();letcenter=Array1::from_vec(vec![x.mean().unwrap(),y.mean().unwrap()]);Polygon{x:x.to_owned(),y:y.to_owned(),center,}}//the`Py`inthereturntypeisawayofsaying"anObjectownedbyPython".#[getter]fnx(self,py:Python<'_>)->PyResult>>{Ok(self.x.to_pyarray(py).to_owned())//CreateaPython-owned,numpyversionof`x`.}//Samefor`y`and`center`.}我们需要将这个新的struct作为类添加到模块中:
#[pymodule]fnpoly_match_rs(_py:Python,m:PyModule)->PyResult{m.add_class::()?;//new.m.add_function(wrap_pyfunction!(find_close_polygons,m)?)?;Ok(())}然后更新Python代码:
classPolygon(poly_match_rs.Polygon):_area:float=Nonedefarea(self)->float:...下面,编译代码——虽然可以运行,但速度非常慢!
为了提高性能,我们需要从Python的Polygon列表中提取基于Rust的Polygon。
PyO3可以非常灵活地处理这类操作,所以我们可以通过几种方法来完成。我们有一个限制是我们还需要返回Python的Polygon,而且我们不想克隆任何实际数据。
我们可以针对每个PyObject调用.extract::(py)?,但也可以要求PyO3直接给我们Py。
这是对Python拥有的对象的引用,我们希望它包含原生pyclass结构的实例(或子类,在我们的例子中)。
#[pyfunction]fnfind_close_polygons(py:Python,polygons:Vec>,//ReferencestoPython-ownedobjects.point:PyReadonlyArray1,max_dist:f64,)->PyResult<Vec<Py<Polygon>>>{//Returnthesame`Py`references,unmodified.letmutclose_polygons=vec![];letpoint=point.as_array();forpolyinpolygons{letcenter=poly.borrow(py).center//NeedtousetheGIL(`py`)toborrowtheunderlying`Polygon`..to_owned();if(center-point).norm()<max_dist{close_polygons.push(poly)}}Ok(close_polygons)}下面,我们来看看使用这些代码的效果如何:
$pythonmeasure.pyTookanavgof6.29msperiteration我们快要成功了,只需再提升一倍的速度即可。
第三版:避免内存分配我们再来看一看分析器的结果。
1.首先,我们看到select_best_polygon,现在它调用的是一些Rust代码(在获取x和y向量时)。
?我们可以解决这个问题,但这是一个非常小的提升(大约为10%)。
2.我们看到extract_argument花费了大约20%的时间(在lib_v2.rs:48下),这个开销相对比较大。
?但大部分时间都花在了PyIterator::next和PyTypeInfo::is_type_of中,这可不容易修复。
3.我们看到大量时间花在了内存分配上。
?lib_v2.rs:58是我们的if语句,我们还看到了drop_in_place和to_owned。
?实际的代码大约占总时间的35%,远超我们的预期。所有数据都已存在,所以这一段本应非常快。
下面,我们来解决最后一点。
有问题的代码如下:
letcenter=poly.borrow(py).center.to_owned();if(center-point).norm()<max_dist{...}我们希望避免to_owned。但是,我们需要一个已拥有的norm对象,所以我们必须手动实现。
具体的写法如下:
usendarray_linalg::Scalar;letcenter=poly.as_ref(py).borrow().center;if((center[0]-point[0]).square()+(center[1]-point[1]).square()).sqrt()<max_dist{close_polygons.push(poly)}然而,借用检查器报错了:
error[E0505]:cannotmoveoutof`poly`becauseitisborrowed-->src/lib.rs:58:33|55|letcenter=poly.as_ref(py).borrow().center;|------------------------|||borrowof`poly`occurshere|atemporarywithaccesstotheborrowiscreatedhere......58|close_polygons.push(poly);|^^^^moveoutof`poly`occurshere59|}60|}|-...andtheborrowmightbeusedhere,whenthattemporaryisdroppedandrunsthe`Drop`codefortype`PyRef`借用检查器是正确的,我们使用内存的方式不正确。
更简单的修复方法是直接克隆,然后close_polygons.push(poly.clone())就可以通过编译了。
这实际上是一个开销很低的克隆,因为我们只增加了Python对象的引用计数。
然而,在这个例子中,我们也可以通过一个Rust的常用技巧:
letnorm={letcenter=poly.as_ref(py).borrow().center;((center[0]-point[0]).square()+(center[1]-point[1]).square()).sqrt()};ifnorm<max_dist{close_polygons.push(poly)}由于poly只在内部范围内被借用,如果我们接近close_polygons.pus,编译器就可以知道我们不再持有引用,因此就可以通过编译。
最后的结果:
$pythonmeasure.pyTookanavgof2.90msperiteration相较于原来的代码,整体性能得到了100倍的提升。
总结我们原来的Python代码如下:
@dataclassclassPolygon:x:np.arrayy:np.array_area:float=None@cached_propertydefcenter(self)->np.array:centroid=np.array([self.x,self.y]).mean(axis=1)returncentroiddefarea(self)->float:...deffind_close_polygons(polygon_subset:List[Polygon],point:np.array,max_dist:float)->List[Polygon]:close_polygons=[]forpolyinpolygon_subset:ifnp.linalg.norm(poly.center-point)<max_dist:close_polygons.append(poly)returnclose_polygons#Restoffile(main,select_best_polygon).我们使用py-spy对其进行了分析,即便用最简单的、逐行转换的find_close_polygons,也可以获得10倍的性能提升。
我们反复进行分析-修改代码-测量结果,并最终获得了100倍的性能提升,同时API仍然保持与原来的库相同。
最终得到的Python代码如下:
importpoly_match_rsfrompoly_match_rsimportfind_close_polygonsclassPolygon(poly_match_rs.Polygon):_area:float=Nonedefarea(self)->float:...#Restoffileunchanged(main,select_best_polygon).调用的Rust代码如下:
usepyo3::prelude::*;usendarray::Array1;usendarray_linalg::Scalar;usenumpy::{PyArray1,PyReadonlyArray1,ToPyArray};#[pyclass(subclass)]structPolygon{x:Array1,y:Array1,center:Array1,}#[pymethods]implPolygon{#[new]fnnew(x:PyReadonlyArray1,y:PyReadonlyArray1)->Polygon{letx=x.as_array();lety=y.as_array();letcenter=Array1::from_vec(vec![x.mean().unwrap(),y.mean().unwrap()]);Polygon{x:x.to_owned(),y:y.to_owned(),center,}}#[getter]fnx(self,py:Python)->PyResult>>{Ok(self.x.to_pyarray(py).to_owned())}//Samefor`y`and`center`.}#[pyfunction]fnfind_close_polygons(py:Python,polygons:Vec>,point:PyReadonlyArray1,max_dist:f64,)->PyResult>>{letmutclose_polygons=vec![];letpoint=point.as_array();forpolyinpolygons{letnorm={letcenter=poly.as_ref(py).borrow().center;((center[0]-point[0]).square()+(center[1]-point[1]).square()).sqrt()};ifnorm<max_dist{close_polygons.push(poly)}}Ok(close_polygons)}#[pymodule]fnpoly_match_rs(_py:Python,m:PyModule)->PyResult{m.add_class::()?;m.add_function(wrap_pyfunction!(find_close_polygons,m)?)?;Ok(())}要点总结?Rust(在PyO3的帮助下)能够以非常小的代价换取Python代码性能的大幅提升。
?对于研究人员来说,PythonAPI非常优秀,同时使用Rust快速构建基本功能是一个非常强大的组合。
?分析非常有趣,可以帮助你了解代码中的一切。