学习总结
环境搭建
我们目前使用python最多的方式是基于anaconda来进行包管理。anaconda相对于python的.venv(python自带的)的管理好处在于,conda管理的虚拟关键在anaconda的安装目录之下(也可自定义指定),.venv管理的虚拟环境在代码的同级工作目录下。这二者的差异在进行代码全量拷贝的时候会发现后者占用空间特别大,是因为拷贝时把虚拟环境里的依赖项全都拷贝出去了。所以,我们都更加建议anaconda来进行包管理。
基本数据类型
这里不讨论基本数据类型有哪些,主要是要分清楚哪些是可变数据类型,哪些是不可变数据类型,
Python3 的六个标准数据类型中:
不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。
之所以想讨论这个,是因为变量作为入参进行传入到函数体内部的时候。如果对变量进行修改,可变数据会直接修改数据本身,不可变数据会创建这个数据的副本,然后在副本上进行修改。这二者的理解可以参照引用传递和值传递。
另外还想讨论一下bytes 数据类型,bytes 类型通常用于处理二进制数据,比如图像文件、音频文件、视频文件等等。在网络编程中,也经常使用 bytes 类型来传输二进制数据。在我们之前的开发工作中,zmq的消息传递都需要把字符串的信息处理成bytes来进行传递。
解释器
python官方指定的解释器是CPython,很好理解就是用C写的Python解释器,他的特定就是versatile,但是兼容性高了,性能注定就不会太高。但是目前pytorch等重要的库,CPython的支持是最好的。还有其他的解释器包括:IPython(CPython 的基础之上在交互式方面得到增强的解释器https://ipython.org/)、Jython(专为 Java 平台设计的 Python 解释器(http://www.jython.org/),它把 Python 代码编译成 Java 字节码执行)、PyPy (一种快速、兼容的替代实现(http://pypy.org/),以速度快著称。)
当时看到pypy和CPython,我就想探究这两种解释器的性能差异,首先看一份测试的demo代码:
pypy和cpython在多轮循环上对于性能是有明显区别的。以下分别是CPython和pypy的性能表现(在mac m4芯片上运行)
这里的性能差异主要是:(from deepseek)
CPython:它是一个解释器。执行代码时,它会先将源代码编译成字节码,然后在 Python 虚拟机上一行一行地解释执行这些字节码。这个“翻译”过程在每次运行时都会发生,并且产生了额外的开销。(这个部分up补充一下,目前其实每次运行都会在工作目录下生成_pycache_的路径,这个目录就是为了存储cpython产生的中间产物,编译的字节码)
PyPy:它包含一个即时编译器。它也会先将代码编译成字节码,但不同的是,PyPy 的 JIT 编译器会在程序运行时,动态地将频繁执行的热点代码(比如循环)直接编译成宿主计算机的本地机器码。一旦编译成本地机器码,CPU 就可以直接、高速地执行它,完全跳过了解释字节码的开销。
这里的性能差异可以总结为解释器 vs JIT。
python数据结构
写这部分,主要是我们在做leetcode的时候,经常要写队列和栈,这部分其实用列表可以拟合这两种数据结构。尤其是列表天然就是一个栈,符合"FILO"的原则。但是如果使用队列的话,建议还是使用queue.Queue模块,这个类支持get(),可以符合"FIFO"的原则。
迭代器与生成器
迭代器:这部分开始涉及深度理解了。iter()函数可以把一个可迭代对象包装成迭代器对象,next()每次可以从迭代器抽出一个元素。next()内部会记录着循环的索引值,即记录着输出到了哪个元素。所以我们可以把一个自定义的类创建为迭代器,只需要在这个类内实现_iter_方法和_next_方法。_iter_需要返回这个类的实例,即self。_next_方法最好是超过了迭代对象长度之后raise出一个StopIteration,用来表示超过了长度。
生成器:在 Python 中,使用了 yield 的函数被称为生成器(generator)。跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,我的理解是,定义了生成器之后需要有一个变量来接生成器的初始化。然后生成器每次取值调用next()方法或者从for循环中取值。生成器需要注意的一点在于:生成器函数的优势是它们可以按需生成值,避免一次性生成大量数据并占用大量内存。这个在大模型推理框架中对于token的管理是可以使用生成器来做输出的。因为token的管理在每一个step都是大量的循环,为避免内存占用,选择每次都输出一个token;但是开销也是显而易见的,这也是一种时间换空间的操作,每次调度都需要继续计算,而不是一次性算好。
with函数
从with函数可以引申出python的资源管理,从而引申出python的垃圾回收机制(GC)。
with 是 Python 中的一个关键字,用于上下文管理协议(Context Management Protocol)。它简化了资源管理代码,特别是那些需要明确释放或清理的资源(如文件、网络连接、数据库连接等)。没有with之前,每次都需要显式的调用close()方法或者del来完成垃圾回收,资源清理的操作。增加with之后,python会自动帮我们做资源管理,在代码退出后立即释放with包裹内的变量。with之后的expression必须要能返回一个context_management的对象。这张图就很清晰的描述上下文管理器的执行逻辑。
__exit__() 方法接收三个参数:
exc_type:异常类型
exc_val:异常值
exc_tb:异常追踪信息
如果 __exit__() 返回 True,则表示异常已被处理,不会继续传播;返回 False 或 None,异常会继续向外传播。
这里要注意的是如果返回True,这里面的所有异常都不会被外层捕获,意味着外层是完全不知道里面发生了什么事。我理解的话对于代码调试来说是很遭罪的。所以有机会还是可以打开python源代码关于contextmanagement的代码看一下。
其次的话,我们如果想自定义上下文管理器,也只需要自定义_enter_和_exit_的方法即可。或者使用contextlib的模块来装饰某个函数,自动把函数封装成一个上下文管理器。这里的逻辑是print(f"<{name}>")相当于在_enter_内部,在yeild之后让出控制权给print("这是一个标题"),最后就是print(f"</{name}>")相当于在 _exit_内部。
python函数
函数这个部分,我主要想讨论两个部分就是不定长参数和lambda匿名函数
我们经常能在官方定义的函数体看到*args和**kwargs。用一个*修饰的入参是以元组的形式传递,用**修饰的入参是以字典的形式传递。前者表示各种位置参数,后者表示各种关键字参数。如果函数调用的时候,传入的未知参数是位置传参,则走的是*args,关键字传参则走的是**kwargs。lambda匿名函数经常和map一起搭配使用,把迭代对象内的元素进行统一转换。其次lambda函数可以作为返回值,这样做的好处在于通过一个函数定义可以生成不同的函数体。当然还有和filter()和reduce()函数的搭配使用,这个可以看runnoob。
装饰器
装饰器这部分我觉得是比较绕的,但是只要记住一点,装饰器是传入函数或者类,然后进行修饰,返回函数或者类的一个函数。如果原函数需要传参,那么这个参数可以通过wrapper来传给原函数;此外,如果要用类形式装饰器进行进行类装饰的话,那么需要重新封装一个wrapper的类,把原始类在初始化时实例化,然后定义一个_call_的方法。
Python 提供了一些内置的装饰器,例如:
@staticmethod: 将方法定义为静态方法,不需要实例化类即可调用。
@classmethod: 将方法定义为类方法,第一个参数是类本身(通常命名为 cls)。
@property: 将方法转换为属性,使其可以像属性一样访问。
python模块模块
模块的搜索路径当导入一个模块时,Python 会按照以下顺序查找模块:
当前目录。
环境变量 PYTHONPATH 指定的目录。
Python 标准库目录。
.pth 文件中指定的目录。
这个内容很重要,我们都知道python的模块是通过import进来的,但是如果不搞清楚顺序的话就很难debug。我们在开发过程中,可能环境变量里面指定了一个vllm的模块,但是我们在同级目录下可能也有一个vllm,那么优先从同级目录下读取。这就会导致有时候我们以为是从环境变量或者其他地方读取。所以我们开发过程中尽量避免这些模块重名,如果一定要重名,请理清楚目前的模块导入位置。是在搞不清的时候,下面这段代码可以打印出模块的位置。
其次,python解释器也会带来影响,这些信息开发者必须明确。
另外可以使用dir()来返回模块内定义的所有名称,这个在开发过程中可以查看某个算子包内都有啥算子。
最后聊一下包和模块,模块可以是一个python脚本,包其实就是多个python脚本的集合,然后定义一个_init_的方法。这个初始化方法在包被import的时候会自动执行。这个方法在开发过程中会经常被使用。例如我们在vllm开发过程中,很多的包内模块的初始化和读取环境变量都是初始化过程中完成的。
作用域
作用域分4块,LEGB,local-enclosing-global-built_in;闭包函数内想修改global变量,先把变量用global声明才可以使用。闭包函数内想修改闭包函数外变量,先把变量用nonlocal声明才可以使用。
threading和asyncio
这部分之前是因为在vllm的开发过程中有使用过线程池去做kv cache异步传输。但是后来发现其实传输线程和主线程依旧是串行执行的,原因就在于传输线程的io操作是调用的mooncake的模块去完成的, 在mooncake中并没有告诉python解释器,这是一个io密集的操作,所以并不会主动释放GIL锁,如果GIL锁使用的是ROUND ROBIN策略来分发时间片的话,那么有很多cpu的执行时间是在等待kv cache传输的。所以我们后续使用asyncio来做传输的事情,调用传输接口的时候使用await来让出控制权,目前来说是能显著看到收益的。
写在最后
runnoob上的教程只看了基础版本的,后面的高级教程还没来得及看。主要也是因为目前的开发任务并不涉及到,如果后续有需要可以再来补。然后基础教程靠后的部门有点心不在焉了,学的囫囵吞枣,所以自己的思考也不像前面的那么深入,后续还是需要更进一步。再者如果教程内容是列表一样,罗列接口,那我就觉得没必要仔细去记住,要用的时候就当是工具手册查一下就好了。但是涉及到各种机制和原理的,我还是力求了解的更加清晰准确。希望这些信息可以帮助到有需要的人,欢迎各种技术交流。谨以此片总结我的python基础学习。背景
up不是计算机科班的,所以工作以来虽然一直在用python,但是对python底层运作机制还不是很清楚。尤其是一些高阶用法,上下文管理器,异步编程等等。所以从菜鸟编程中的python教程为基础(https://www.runoob.com/python3/python3-tutorial.html),查漏补缺。这篇笔记不是完整的python学习笔记,只是针对博主之前不理解的部分做详细的解读,也算是自己梳理的总结。但我相信,这部分也是大多数python使用者模糊的部分,消除模糊是更进一步的必备条件。适合人群:这篇帖子适合有python使用经验的开发者,不适合0基础初学者。
学习总结
老牌股票配资平台提示:文章来自网络,不代表本站观点。