"Life is short, I use python"这句脍炙人口的宣传语,在某种程度上表达了使用python开发软件的高效。以笔者目前的能力来说,python开发软件的高效主要体现在两个方面:1. 简单且贴合自然语言的语法设计,这使得python程序的可读性极佳,能够减少将逻辑转换为程序,检查逻辑错误的成本。2. (几乎)一切都是对象的抽象方式,凭此,软件开发者首先可以将物理世界的客观事物都抽象为程序中的对象,然后自由组织及操作。本文将从python程序的执行和python程序中一切皆为对象两个话题,详细阐述笔者对python高效编程的浅见。
因本人才疏学浅,故行文之间恐有纰漏,望诸君海涵,不吝赐教,若能予以斧正,则感激不尽。
python程序的执行
文章图片
图1 first.py
有一个first.py文件, 内容如图1所示。python解释器执行后,依次输出the first file和the main function。由此可知,python解释器是“逐行解释”程序的。
文章图片
图2 second.py
在first.py的同级目录下添加一个second.py文件, 内容如图2所示,first.py的内容更新为图3所示。python解释器执行first.py后,依次输出the second file,the first file和the main function。这说明,import语句被最先执行,而且second.py不是主module。此外,同级目录下多了一个__pycache__目录,其内有一个second.cpython-38.pyc文件(笔者使用的是CPython3.8解释器),它是一个binary文件,执行python second.cpython-38.pyc后,打印the second file。由此可知,python解释器是先编译second.py,然后将pyc文件给first.py使用。因此python解释器运行python程序的过程,包含了编译与解释两个阶段。
文章图片
图3 更新后的first.py
从上述内容可知,import的语义是导入pyc文件。那么,python解释器抛出ModuleNotFoundError的实质是,__pycache__的父目录没有被加入到python解释器的搜索路径中。一般地,python解释器的搜索路径包括python解释器执行的当前目录,标准库及pip安装目录,环境变量PYTHONPATH以及sys.path.insert/append动态添加的目录。
进一步地,python解释器是如何执行”import second“这条语句的呢?python官方文档在importlib中给出了答案[1]。首先调用__init__.py文件中的import_module函数,然后调用_bootstrap.py文件中的_gcd_import函数,最后调用同文件下的_find_and_load函数,其源码如下图4所示。通过源码可知,python将程序运行所需的所有模块都保存在了sys.modules中,它是一个字典。
文章图片
图4 _find_and_load函数源码
python程序中一切皆为对象
修改first.py文件的内容如图5所示,python解释器执行first.py后,输出内容如注释所示,这里,我们先断言它们都是类的实例化对象。如何验证呢?
文章图片
图5 first.py
python提供了__class__属性来获取对象的类。修改first.py文件内容如图6所示, 访问它们的__class__后,输出的类如注释所示,由此验证了它们都是对象。而且,输出的这些类与MyClass一样,它们仍是类的实例化对象,可以继续访问__class__属性,输出的类均为
文章图片
图6 first.py的类
结合python解释器逐行解释的工作原理可知,def myFunc经过编译、解释后会被创建为名为myFunc的function对象,class MyClass经过编译、解释后会被创建为名为MyClass的class对象。查阅python官方文档可知,类function实例化的function对象myFunc只允许被调用以及作为参数传递,MyClass则可以实例化object[2]。而且,Class MyClass等同于MyClass = type('MyClass', (), {}),其中type(className, bases, dict, **kwds)为type的一种构造函数[3]。
文章图片
图7 python的对象体系
理解了python在宏观层面的对象体系后,我们将目光聚焦到对象本身。在python中,object是数据和方法的聚合体, 其中数据与方法的可调用对象, 统称为object的属性(attribute),内置函数dir(object)可以查看object的属性,object调用访问符"."可以访问它的属性。
例如, 在first.py的同级目录下添加一个third.py文件, 内容如图8所示。python解释器执行third.py后,third.py中的输出内容如图8中注释所示。由此可知,module对象second,class对象MyClass及function对象myFunc,都是module对象first的属性。在third.py中使用first.XXX即可访问它们。通过属性__module__,对象可以获得它从属的module的名称。
文章图片
图8 third.py
除了符号"."以外,python还提供了内置函数getattr(object, name),能够通过字符串映射到object的属性。与之相对地,内置函数setattr(object, name, value)则用于更新属性的值,这就是python的反射机制。因为python程序中几乎一切皆为对象,所以熟练掌握这些内置函数后,软件开发者能够轻松实现基于字符串的事件驱动。
至此,我们梳理一下上述内容:
1. 解释器逐行解释python程序。
2. python文件被import后,会被python解释器转换成module对象。
3. 所有module对象被归纳在sys.modules字典内,key是moudle对象的名称。
4. python文件内的class对象,function对象,基本数值对象,module对象,都是该python文件被转换后的module对象的属性。
5. __class__属性可以获取对象的class对象,__module__属性可以获取对象从属模块的名称。
6. 内置函数getattr和setattr实现了以字符串映射到object的属性。
理论上,基于以上6点内容,我们可以从任意python对象出发,获取到python程序中的其他已被解释器实例化的任意对象。比如,从任意python对象推得其从属的module对象,然后获取属性module,最后访问属性module内的对象。或者借助sys.modules,绕过import语句,直接访问其他module内的python对象。
python解释器的解释顺序和python程序中一切皆为对象的抽象方式,赋予了软件开发者自由编排、更改python程序行为的能力。在此基础上,软件开发者再结合设计模式,开发经验以及大量练习,就可以实现高效、简洁、表达能力强的python程序。
【Python高效编程】参考文献
[1] https://docs.python.org/3/lib...
[2] https://docs.python.org/3/lib...
[3] https://docs.python.org/3/lib...