通过内置对象理解 Python(二)
2021-11-03
所有的内置函数
用 dir() 函数可以查看所有内置函数:
1 | print(dir(__builtins__)) |
内容很多,但别担心,我们会把它们分成不同的组,然后逐一解决。
异常
Python 有66个内置的异常类(到目前为止),每个类都用于程序中作为解释和捕获代码中的错误和异常的方法。
为了解释为什么 Python 中有单独的异常类,我们看一个简单的例子:
1 | def fetch_from_cache(key): |
注意看 get_value() 函数,正常情况下,通过 fetch_from_cache() 函数返回缓存中的值,否则执行 fetch_from_api() 函数。
在这个函数中有三种情况:
如果
key不在缓存中,执行cached_items[key]将引发KeyError异常。此异常会被except分支捕获,之后执行此分支下的语句。如果
key存在于缓存中,将返回相应的值。还有第三种情况:
key是None。在fetch_from_cache()函数中将引发ValueError异常,用以表示此时传给函数的值不合适。因为try ... except ...只捕获KeyError异常,ValueError异常会直接显示给用户。1
2
3
4
5
6x = None
get_value(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in get_value
File "<stdin>", line 3, in fetch_from_cacheValueError: key must not be None>>>
如果 ValueError 和 KeyError 不是已经定义好的,上面就不能直接使用。
在内置作用域中,并非所有以大写字母开始的名称都引用了异常类型对象,还有另一种类型的内置对象的名称首字母是大写的:常量。下面就来研究这些。
常量
总共有5个常量:True 、False 、None 、Ellipsis 和 NotImplemented 。
True、False 和 None 是最明显的常量。
Ellipsis 是一个有趣的词,它实际上有两种形式:单词 Ellipsis 和符号 … (英文的省略号)。 它的存在主要是为了支持打印注释以及一些其他奇特的切片提供支持。推荐阅读本公众号已经发布的文章《Python 中的省略号》 。
NotImplemented 是一个更有趣的关键词(在我的书中,已经提到过,True 和 False 分别是 1 和 0 ——最新即将出版的《Python 完全自学教程》会有更专门的介绍,请参阅:www.itdiffer.com/self-learning.html )。当你想告诉 Python 解释器某个类没有定义某个操作符时,就是在这个类的操作符定义中使用 NotImplemented 。
Python 中的所有对象都可以自定义所有 Python 操作符,比如 + 、- 、+= 等,在我的每本书中都有相应的案例演示定义方法,比如针对 + 就要重写特殊方法 __add__ ,针对 += 重写特殊方法 __iadd__ 等等。
看一个简单的例子:
1 | class MyNumber: |
这样就定义了一个对象,当此对象与任何其他数字相加,就相当于其他数字加上 28 。
1 | num = MyNumber() |
上面的代码示例没有使用 3 + num 的形式,因为它行不通:
1 | 100 + numTraceback (most recent call last): |
但是,通过添加 __radd__ 操作符,就可以很容易支持 3 + num 的形式了,因为该操作符添加了对右加的支持。
1 | class MyNumber: |
另外,有了 __radd__ 方法后,这能实现两个 MyNumber 类的的实例相加:
1 | num = MyNumber() |
但是假设你只想用这个类支持整数加法,而不支持浮点数加法。此时就要使用 NotImplemented :
1 | class MyNumber: |
从操作符方法中返回 NotImplemented ,就是告诉 Python 解释器当前的操作是不受支持的。然后Python 解释器会将其包装到 TypeError 异常中,并带有一个有意义的说明:
1 | n + 0.12Traceback (most recent call last): |
Python 的常数还有一个奇怪的事实,它们是直接在 C 语言的代码中实现的,比如这里。
全局变量
上面看到的内置对象中,还有另一组看起来很奇怪的名称,如:__spec__ 、 __loader__ 、__debug__ 等。
这些实际上不是 builtins 模块所特有的。 这些属性存在于 Python 的每个模块的全局作用域中,它们是模块的属性。包含了导入模块的相关信息。
__name__
__name__包含了模块的名称。 例如, builtins.__name__ 指的是字符串 builtins 。运行一个 Python 文件时,文件也是作为一个模块运行的,并且它的模块名是 __main__。 这应该解释了在Python文件中使用的 if __name__ == '__main__' 是如何起作用的。
__doc__
__doc__包含模块的文档字符串。 当执行 help(module_name) 时,它会显示模块文档内容。
1 | import time |
__package__
__package__表示该模块所属的包。 对于顶级模块,它与 __name__ 相同。 对于子模块,它的 __package__ 值就是包的 __name__ 值。 例如:
1 | import urllib.request |
__spec__
__spec__ 属性含有详细的模块信息,即元数据,如模块名称、模块类型,以及它的存储路径等。
1 | $ tree mytest |
可以看出,mytest 是使用 NamespaceLoader 从目录 /tmp/mytest 中找到的,而 mytest.a.b 是使用 SourceFileLoader 从源文件 b.py 中加载的。
__loader__
在交互模式中看看 __loader__ 是什么:
1 | __loader__ |
__loader__ 的作用在于导入模块的时候,被设置为导入对象。这个特定的模块定义在 _frozen_importlib 模块中,用于导入内置模块。
更仔细地看一下之前的 mytest.__spec__ 输出结果,会发现模块详情的 loader 属性是 Loader 类,而这些类是来自稍有差别的 _frozen_importlib_external 模块。
因此,你可能会问,这些以 _frozen 命名模块是什么? 顾名思义——它们是“冻结模块”。
这两个模块的“真正”源代码实际上在 importlib.machinery 模块中。 这些以 _frozen 命名的对象是这些加载器源代码的冻结版本。 为了创建冻结模块,Python 代码被编译为代码对象,编组到文件中,然后添加到 Python 可执行文件中。
Python 冻结了这两个模块,因为它们实现了导入系统的核心,因此,当解释器启动时,它们不能像其他 Python 文件一样被导入。 本质上,它们的存在是为了引导导入系统。
有趣的是,在 Python 中还有另一个定义明确的冻结模块,它是: __hello__ 。
1 | import __hello__ |
这是所有语言中最短的 hello world 代码吗? 亦或向前辈致敬?
这个 __hello__ 模块最初被添加到 Python 中,是作为对冻结模块的测试,以查看它们是否正常工作。 从那以后,它一直作为“复活节彩蛋”留在Python语言中。
__import__
__import__ 是一个内置函数,它定义了 import 语句在 Python 中的工作方式。
1 | import random |
从本质上讲,每个 import 语句都可以转换为 __import__ 函数调用。 在内部,这差不多就是 Python 对导入语句的处理(但在C语言中更直接)。
__debug__
在 Python 中,这是一个全局常量,几乎总是被设置为 True 。
其含义是 Python 在调试模式下运行。 默认情况下,Python 总是在调试模式下运行。
此外,Python 还可以运行在“优化模式”下。要在“优化模式”下运行,可以在启动时增加 -O 参数。这种模式只是阻止断言语句(至少目前如此)。老实说,没什么实际作用。
1 | $ python |
此外, __debug__ 、True 、False 和 None 是 Python 中唯一的真常量,即这4个常量是Python中唯一不能用赋值语句重写为新的其他值的全局变量。
1 | True = 42 |
__build_class__
这个全局变量是在 Python 3.1 中添加的,有了它以后,就可以在定义类的时候,除了继承,还可以使用用任意位置参数和关键字参数。 这个特性的技术原因比较复杂,而且它涉及到像元类这样的高级主题,此处暂不详解。
但要知道的是,有了它,在创建类时就可以这样做:
1 | class C: |
在 Python 3.1 之前,像上面那样,用于创建类 D 的语法只允许传入元类和可供继承的基类。新的语法则允许有可变数量的位置参数和关键字参数。这种变化似乎带来了一点混乱和复杂。
但是,在调用常规函数的代码中,我们已经用了它。 因此,有人建议 Class X(...) 语法可以用函数调用的方式替代:__build_class__('X', ...) 。
__cached__
当导入一个模块时, __cached__ 属性存储该模块的已编译的 Python 字节码的缓存文件的路径。
你可能对 Python 的“编译”感到奇怪,没错。Python 是编译的。事实上,所有 Python 代码都是编译的,但不是编译为机器代码,而是编译为字节码。 为了解释这一点,先解释 Python 如何运行代码。
以下是 Python 解释器运行代码的步骤:
- 获取源文件,并将其解析成语法树。语法树是代码的一种表示形式,它更容易被程序理解。 它查找并报告代码中的任何语法错误,并确保没有歧义。
- 下一步是将语法树编译为字节码。 字节码是用于Python虚拟机的一组微指令。 这个“虚拟机”是 Python 解释器的逻辑所在。 它本质上是在本地计算机上模拟一个非常简单的基于栈的计算机,以便执行 Python 代码。
- 然后,Python 源代码以字节码形式在 Python 虚拟机上运行。 字节码指令是简单的指令,比如,从当前栈中推送和取出数据。当这些指令一个接一个地运行时,将执行整个程序。
在导入模块时,将源码“编译为字节码”要花费一定时间,而后,Python 将字节码存储到 .pyc 文件中,并将其存储在名为 __pycache__ 的文件夹中。 然后,所导入的模块的 __cached__ 属性值为该 .pyc 文件。
当以后再次导入同一个模块时,Python 会检查该模块的 .pyc 版本是否存在,然后直接导入已经编译过的版本,从而节省大量的时间和计算。
如果还没有理解,可以在 Python 代码中直接运行或导入 .pyc 文件,就像其他的 .pyc 文件一样:
1 | import test |
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
关注微信公众号,读文章、听课程,提升技能