首页 > 编程语言 >python 导入时与运行时

python 导入时与运行时

时间:2024-07-17 11:18:32浏览次数:15  
标签:__ python 代码 导入 print 执行 运行

转载自我自己的 github 博客 ——> 半天钟的博客

元编程相关博文的目录及链接

这篇博文是元编程系列博文中的其中一篇、这个系列中其他博文的目录和连接见下:

  1. 使用 python 特性管理实例属性
  2. 浅析 python 属性描述符(上)
  3. 浅析 python 属性描述符(下)
  4. python 导入时与运行时
  5. python 元编程之动态属性
  6. python 元编程之类元编程

Preview

Python 导入时和运行时的概念不像 Java 语言一样易于区分。

Java 的 import 只是告诉 Java 编译器需要特定的包,而 Python 则不同,Python 的首次 import 会 “执行所有的顶层代码” 甚至可以做所有运行时能够做到的事。

这篇文章是为了说明白 Python 导入时与运行时的行为差异,并总结出一些经验准则。

将这篇文章纳入元编程系列文章的原因是:了解了这一部分概念以后能够帮助我们理解元编程的相关概念。

导入时与运行时的概念

Python 有两种执行代码的方式:

  1. 通过命令行方式执行 Python 脚本。(运行时)
  2. 将代码从一个文件首次导入到另一个文件/解释器中。(导入时/import)

这两种方式分别对应着导入时与运行时。 也就是说,我们接下来要弄明白的是:

假设我们有一个编写好的 python 脚本(test.py),那么我们在使用命令行使用命令 python test.pyimport test.py 分别会发生什么,并尝试总结出有用的经验准则。

测试代码

为了能够看清首次导入时究竟会发生什么,我们需要提供一个有着类、函数、普通顶层代码的相对复杂的 evaltime.py 文件,还需要在这个文件中再导入另一个 evalsupport.py 文件,用以观察在各种不同条件下的行为evaltime.py代码如下:

以下代码都是《流畅的 python》第二十一章第三节的源码,作者 Luciano Ramalho 也是通过这两个类来总结导入时与运行时的行为差异,我觉得设计的很好。

from evalsupport import deco_alpha

print('<[1]> evaltime module start')

class ClassOne():
    print('<[2]> ClassOne body')

    def __init__(self):
        print('<[3]> ClassOne.__init__')

    def __del__(self):
        print('<[4]> ClassOne.__del__')

    def method_x(self):
        print('<[5]> ClassOne.method_x')

    class ClassTwo(object):
        print('<[6]> ClassTwo body')

@deco_alpha
class ClassThree():
    print('<[7]> ClassThree body')

    def method_y(self):
        print('<[8]> ClassThree.method_y')

class ClassFour(ClassThree):
    print('<[9]> ClassFour body')

    def method_y(self):
        print('<[10]> ClassFour.method_y')

if __name__ == '__main__':
    print('<[11]> ClassOne tests', 30 * '.')
    one = ClassOne()
    one.method_x()
    print('<[12]> ClassThree tests', 30 * '.')
    three = ClassThree()
    three.method_y()
    print('<[13]> ClassFour tests', 30 * '.')
    four = ClassFour()
    four.method_y()

print('<[14]> evaltime module end')

evalsupport.py代码如下:

print('<[100]> evalsupport module start')

def deco_alpha(cls):
    print('<[200]> deco_alpha')

    def inner_1(self):
        print('<[300]> deco_alpha:inner_1')

    cls.method_y = inner_1
    return cls


class MetaAleph(type):
    print('<[400]> MetaAleph body')

    def __init__(cls, name, bases, dic):
        print('<[500]> MetaAleph.__init__')

        def inner_2(self):
            print('<[600]> MetaAleph.__init__:inner_2')

        cls.method_z = inner_2


print('<[700]> evalsupport module end')
'
运行

evaltime.py 中有着:

  1. print('<[1]> evaltime module start') 一样的顶层代码
  2. 像 ClassOne、ClassTwo、ClassThree、ClassFour 的类定义体ClassOne、ClassTwo 还是嵌套关系
  3. 在类中有诸多的类方法,甚至 ClassOne 类还定义了 __init____del__ magic 方法。
  4. ClassFour 继承了 ClassThree
  5. ClassThree 有一个类装饰器 deco_alpha,来自于另一个模块 — evalsupport.py

evalsupport.py 中有着:

  1. print('<[100]> evalsupport module start') 一样的顶层代码
  2. 一个函数定义 deco_alpha,其嵌套这一个函数 inner_1。(此函数是类装饰器函数)
  3. 有一个类定义 MetaAleph,其中 __init__ 有一个嵌套函数 inner_2

这两个函数基本已经囊括了 python 代码的所有应用场景、包括顶层代码、类、函数、magic 方法、嵌套类、嵌套函数、类继承、装饰器、导入模块。

接下来我们将观察在首次导入时运行时 python 分别会做出什么动作。

Luciano Ramalho 建议你在看下去前先自己拿纸和笔将自己认为的程序输出写在纸上,我也是这么做的,第一次这么做让我明白了自己在认知上的差错。

导入时解析

python 命令行中运行 import evaltime.py 会得到以下输出:

<[100]> evalsupport module start
<[400]> MetaAleph body 
<[700]> evalsupport module end 
<[1]> evaltime module start 
<[2]> ClassOne body 
<[6]> ClassTwo body 
<[7]> ClassThree body 
<[200]> deco_alpha 
<[9]> ClassFour body 
<[14]> evaltime module end

下面对上面的 10 行代码做出解释:

  1. 导入时,python 会执行 evaltime.py 所导入的 evalsupport.py 模块中的所有顶层代码。
  2. evalsupport.py 中的 deco_alpha 方法体没有执行,实际上 python 编译了 deco_alpha 函数,但是不会执行定义体MetaAleph 类的定义体运行了,但是其中的 __init__ magic 方法没有执行。
  3. 原因见 1
  4. python 会执行 evaltime.py 的所有顶层代码。
  5. ClassOne 的类定义体也执行了、但是类方法没有执行。
  6. 嵌套于 ClassOne 的 ClassTwo 类的定义体也执行了。
  7. ClassThree 的类定义体先于它的类装饰器方法体执行了。
  8. 类装饰器 deco_alpha 在被其装饰的 ClassThree 后执行了方法体,但是嵌套其中的函数 inner_1 没有执行。
  9. ClassFour 类执行了类方法体,但是貌似与其父类 ClassThree 没有关联。
  10. 原因见 4

值得注意的是:这些输出只会在首次 import 时看见,之后再次 import 就不会有任何输出了。

**原因是:**在首次导入时,解释器会从上到下一次性解析完 .py 模块的源码,然后生成用于执行的字节码,并将其存入 __pycache__ 的 .pyc 文件中。如果有句法错误,则会在此时报告。

当本地的 __pycache__ 文件中有最新的 .pyc 文件,那么解释器则会跳过上述步骤,因为已经有运行所需的字节码了。

导入时总结:

在 python 命令行首次输入 import A 时会按照代码顺序执行以下几条:

  1. 会执行模块 A 中的所有顶层代码。
  2. 会执行模块 A 导入的所有模块的顶层代码(上例中的 evalsupport.py)。
  3. 执行的顶层代码中
    1. 若是类的定义体,那么会执行类的定义体、但是只会编译类方法,不会执行类方法定义体。
    2. 若是函数,则解释器只会编译此函数,不会执行函数定义体。
  4. 在执行的类的定义体中
    1. 若是嵌套类,那么会执行嵌套类的定义体,规则与第三条规则中的类定义体情况一致。
    2. 若该类被装饰、在执行完类定义体后执行装饰器函数定义体。
    3. 与该类是否是某个类的子类无关(正常执行)。
  5. 执行装饰器函数定义体中
    1. 若是嵌套函数,那么只会编译此函数,不会执行该函数定义体。

总结来说、首次导入时,所有的顶层代码都会执行,包括在此模块中导入的其他模块的顶层代码;此外,类的定义体也会被执行;除了装饰器函数的定义体会在被装饰的类/函数执行过后执行,其他的所有函数只会被解释器编译,不会执行函数定义体。

if __name__ == ‘__main__’ 属于顶层代码,只是此时这个语句的结果为 False,原因后面会提到。

被编译器编译的意思是:将其解析成可执行的字节码,换言之就是做了全局名称绑定,以便以后要使用时找到它。

对于类而言,被编译器编译后,定义了类的属性和方法,并构建了类对象。

运行时解析

我们直接来看在系统命令行运行 python evaltim.py 会发生什么:

<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body
<[11]> ClassOne tests ..............................
<[3]> ClassOne.__init__
<[5]> ClassOne.method_x
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y
<[14]> evaltime module end
<[4]> ClassOne.__del__

下面对上面的代码作出解释:

  1. 第 9 行及之前的输出与首次导入时的输出一致。
  2. 第 10 行开始运行 if __name__ == '__main__': 之后的代码
  3. 11 行是类初始化的标准行为 —— 调用 __init__
  4. 第 14 行值得注意,其 method_y 没有输出类定义体中的 method_y ,而是执行了被类装饰器使用猴子补丁替换掉的方法 -> inner_1 的。
  5. 第 16 行也值得注意,装饰器对 ClassThree 子类 ClassFour 好像没有影响。
  6. 第 18 行,只有在程序结束以后,ClassOne 类的实例才会被垃圾回收程序垃圾回收。

值得注意的是,不管调用多少次 python evaltim.py 输出的结果都一致。

运行时总结

运行时的执行规则与导入时基本一致:

  1. 在系统命令行调用 python evaltim.py 与在 python 命令行调用 import evaltime执行顺序和执行规则一致
  2. 不同的是,运行时会执行 if __name__ == '__main__': 代码块的代码。

值得一提的是,如果你将 if __name__ == '__main__': 代码块的代码全部都变成顶层代码,也就是去掉 if __name__ == '__main__': 这一行,那么运行时的输出与首次导入时的输出几乎一致,只是最后一行销毁 ClassOne 实例的代码不会输出。

这是因为导入时发生在 python 命令行中,在这里 python 程序在 import 完 evaltime 后还没有结束,不会触发垃圾回收机制。

那么实际上,python 导入时与运行时的重大差别在于 if __name__ == '__main__': 代码块的代码是否执行。

接下来我们来探究导入时与运行时特殊变量 __name__ 的情况。

运行时与导入时的差别

现在,我们将 evaltime.py 中的所有代码替换成一句:

print("此时 __name__ 变量的值是:", repr(__name__))
'
运行

repr() 只是将 __name__ 变量的值以字符串的形式返回,不必在意。

在 python 命令行中执行 import evaltime,得到如下结果:

此时 __name__ 变量的值是: 'evaltime'

在系统命令行中执行 python evaltime.py 得到如下结果:

此时 __name__ 变量的值是: '__main__'

那么由此可见、运行时与导入时最大的不同在于 __name__ 变量的值,在导入时 python 解释器会为 __name__ 赋值为模块名(evaltime)。在运行时 python 解释器会为 __name__ 赋值为 __main__

其次、在系统命令行运行 python evaltime.py 不管多少次输出结果都是一致、而在 python 命令行中运行 import evaltime 只会在第一次输出结果、后面的重复调用只会检查 __pycache__ 是否有最新的 .pyc 文件,如果有,则什么也不做。

也就是说、python 导入时与运行时的动作几乎一致,都会按照**相同的规则执行模块文件中的所有顶层代码。**但是由于在导入时与运行时 python 解释器为 __name__ 变量赋值不同,所以我们能够为运行时添加一些导入时所不能做到的操作,比如定义 Python 的 “Main 函数”。

定义 Python 的 “Main 函数”

如果你学过 python ,你一定见过 if __name__ == '__main__': 这句代码,看完上面的文字,你一定已经明白了其原理。

C 语言有一个特殊函数的 main 函数、当操作系统运行 C 程序时会自动执行该函数。

像 C 语言一样有 main 函数的语言不在少数、但是 python 不在其列,python 解释只会想上面的小节描述的一样从头到尾开始执行顶层代码,没有自动执行的特殊函数。

**所以、根据执行方式的不同,是否有着不同行为就相当重要了。**好在,python 解释器在导入时与运行时为特殊变量 __name__ 赋值不同,这提供了 python 拥有 Main 函数的方法。

首先,我们必须意识到,if __name__ == '__main__': 下的代码块只会在运行时执行
其次,我们需要像其他语言使用 Main 函数一样使用它,即:

  1. 将大部分的代码放入模块的函数或者类中。
  2. 创建名为 main() 的函数来包含运行时想要执行的代码,包括调用函数和生成类对象。
  3. if __name__ == '__main__': 代码块下调用 main()。

使用上面的准则来定义 Python 的 Main 函数能够极大程度的解决 python 导入时和运行时动作基本一致的弊端。(例如:导入时可能执行耗时操作、扰乱终端信息等)

End

本文中,我总结了 python 导入时与运行时的区别、指出了导入时会按照一定规则执行所有顶层代码,紧接着我们实验出了 python 运行时的行为与导入时相当一致

接下来、我分析了导入时与运行时的重点差别 —— 解释器会为 __name__ 属性赋不同的值

根据这个的性质,我引出了 Python Main 函数的使用准则使用这些准则能够很大程度的避免python 导入时和运行时动作基本一致导致的弊端。

 

标签:__,python,代码,导入,print,执行,运行
From: https://www.cnblogs.com/lidabo/p/18306908

相关文章

  • 0基础学python-15:封装、继承和多态
    目录前言 一、封装(Encapsulation)私有变量: 二、继承(Inherit) 三、多态(Polymorphism)总结前言        封装、继承和多态是面向对象编程的三大基本特性,它们与面向对象编程(OOP)密切相关。  一、封装(Encapsulation)概念:封装指的是将数据(属性)和操作数据的方法......
  • SqlServer SQL语句或存储过程运行慢 使用 WITH RECOMP ILE 或 OPTION (RECOMPILE)(重新
    如果您的存储过程包含参数可以重新申明变量把参数接收下,可能解决你过程执行慢的原因。如果未能解决,请参考以下文章内容:WITHRECOMPILE子句可以在以下地方使用:一种是当你创建一个过程时,例如:CREATEPROCEDUREMySPWITHRECOMPILEAS这指示SQLServer在每次调用时重新编......
  • 【python学习】第三方库之pandas库的定义、特点、功能、使用场景和代码示例
    引言pandas是一个强大的Python库,用于数据分析和数据处理。它基于NumPy,提供了灵活的数据结构(Series和DataFrame)和数据操作功能,是数据科学和机器学习中不可或缺的工具文章目录引言一、安装`pandas`第三方库二、`pandas`的定义三、特点3.1强大的数据结构3.2灵活的数据......
  • 从汇编层看64位程序运行——栈保护
    大纲栈保护延伸阅读参考资料在《从汇编层看64位程序运行——ROP攻击以控制程序执行流程》中,我们看到可以通过“微操”栈空间控制程序执行流程。现实中,黑客一般会利用栈溢出改写NextRIP地址,这就会修改连续的栈空间。而编译器针对这种场景,设计了“栈保护”机制。栈保......
  • 【python】PyQt5的窗口界面的各种交互逻辑实现,轻松掌控图形化界面程序
    ✨✨欢迎大家来到景天科技苑✨✨......
  • 自动化测试-RobotFramework环境准备(python基础)
    学习总结,有错误欢迎指出。总结:推荐为每个python相关项目创建独立的虚拟环境。1.虚拟环境安装虚拟环境:为每个项目提供独立的执行器和包管理。示例:使用python内置库venv模块,在项目根目录下创建虚拟环境。python-mvenv.venv-m:执行“位于搜索路径(sys.path)下......
  • Python办公自动化:效率飞跃,自动化批量汇总Excel到Word
    Python办公自动化:效率飞跃,自动化批量汇总Excel到Word原创 丹心向阳 数海丹心 2024年06月23日07:30 山东摘要:每个月底,是许多数据分析师的梦魇,尤其是当他们需要从成百上千的Excel报表中汇总数据到Word时。本文将讲述小李如何使用Python自动化技术,几秒钟完成原本需要通宵达旦......
  • Python自动化:智能对比Word文档,秒速锁定差异!
    Python自动化:智能对比Word文档,秒速锁定差异!原创 丹心向阳 数海丹心 2024年06月28日07:00 山东摘要:在我们的工作和学习中,经常需要对文档进行多次修改,如何快速准确地识别文档的最终版本,一直是让人头疼的问题。本文将介绍一种Python自动化技术,它可以自动对比两个Word文档之间......
  • Python自动化:10行代码免费解锁抖音、快手、小红书平台资源,无水印视频一键下载
    Python自动化:10行代码免费解锁抖音、快手、小红书平台资源,无水印视频一键下载原创 丹心向阳 数海丹心 2024年06月19日07:30 山东摘要:抖音、快手、小红书作为国内顶尖的短视频和娱乐平台,汇聚了巨大的流量和丰富的创意内容。对于自媒体从业人员而言,这些平台上的灵感和视频资......
  • Kylin系列(十二)监控与运维:保持 Kylin 系统稳定运行
    目录1.监控的基础1.1为什么需要监控1.2监控的核心指标2.使用监控工具2.1Prometheus与Grafana2.1.1安装Prometheus2.1.2安装Grafana2.1.3配置Kylin监控2.2其他监控工具3.运维中的最佳实践3.1定期检查和维护3.2建立完善的备份机制3.3实时预警和......