【Python学习手册(第四版)】学习笔记22-模块代码编写基础

个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。

本文主要介绍模块编码工具的基础知识:import和from语句,以及reload调用,介绍了模块命名空间(这个概念可能因为翻译问题稍难理解)。全文较简单,主要介绍概念较多。


目录

模块代码编写基础

模块的创建

模块的使用

import语句

from语句

from*语句

导入只发生一次

import和from是赋值语句

文件间变量名的改变

import和from的对等性

from语句潜在的陷阱

何时使用import

模块命名空间

文件生成命名空间

属性名的点号运算

导入和作用域

命名空间的嵌套

重载模块

reload基础

reload实例

为什么要在意:模块重载


模块代码编写基础

Python模块的创建很简单,只不过是用文本编辑器创建的Python程序代码文件而已。不需要编写特殊语法去告诉Python现在正在编写模块,几乎任何文本文件都可以。因为Python会处理寻找并加载模块的所有细节,所以模块很容易使用。客户端只需导入模块,就能使用模块定义的变量名以及变量名所引用的对象。

模块的创建

定义模块,只要使用文本编辑器,把一些Python代码输入至文本文件中,然后以".py"为后缀名进行保存,任何此类文件都会被自动认为是Python模块。在模块顶层指定的所有变量名都会变成其属性(与模块对象结合的变量名),并且可以导出供客户端来使用。

例如,如果在名为module1.py的文件中输入下面的def语句,并将这个文件导入,就会创建一个拥有一个属性的模块对象:变量名printer,而这个变量名恰巧引用了一个函数对象。

这里对模块文件名再多介绍一下。模块怎么命名都可以,但是如果打算将其导入,模块文件名就应该以.py结尾。对于会执行但不会被导入的顶层文件而言,.py从技术上来讲是可有可无的,但是每次都加上去,可以确保文件类型更醒目,并允许以后可以导入任何文件。

因为模块名在Python程序中会变成变量名(没有.py)。

因此,应该遵循第11章所提到的普通变量名的命名规则。例如,你可以建立名为if.py的模块文件,但是无法将其导入,因为if是保留字。当尝试执行importif时,会得到语法错误。

事实上,包导入中所用的模块的文件名和目录名(下一章讨论),都必须遵循第11章所介绍的变量名规则。例如,只能包含字母、数字以及下划线。包的目录也不能包含平台特定的语法,例如,名称中有空格。

当一个模块被导入时,Python会把内部模块名映射到外部文件名,也就是通过把模块搜索路径中的目录路径加在前边,而.py或其他后缀名添加在后边。例如,名为M的模块最后会映射到某个包含模块程序代码的外部文件:<directory>\M.<extension>。

正像上一章所提到的那样,也有可能使用C或C++(或Java,Python这门语言的Jython实现)这类外部语言编写代码来创建Python模块。这类模块称为扩展模块,一般都是在Python脚本中作为包含外部扩展库来使用的。当被Python代码导入时,扩展模块的外观和用法与Python源代码文件所编写的模块一样:也是通过import语句进行读取,并提供函数和对象作为模块属性。

模块的使用

客户端可以执行import或from语句,以使用刚才编写的简单模块文件。如果模块还没有加载,这两个语句就会去搜索、编译以及执行模块文件程序。主要的差别在于,import会读取整个模块,所以必须进行定义后才能读取它的变量名;from将获取(或者说是复制)模块特定的变量名。

下面所有的范例最后都是调用外部模块文件module1.py所定义的printer函数,只不过使用了不同的方法。

import语句

在第一个例子中,变量名module1有两个不同目的:识别要被载入的外部文件,同时会生成脚本中的变量,在文件加载后,用来引用模块对象。

因为import使一个变量名引用整个模块对象,必须通过模块名称来得到该模块的属性(例如,module1.printer)。

from语句

因为from会把变量名复制到另一个作用域,所以它就可以直接在脚本中使用复制后的变量名,而不需要通过模块(例如,printer)。

这和上一个例子有着相同的效果,但是from语句出现时,导入的变量名会复制到作用域内,在脚本中使用该变量名就可少输入一些:可直接使用变量名,而无须在嵌套模块名称之后。

from语句其实只是稍稍扩展了import语句而已。它照常导入了模块文件,但是多了一个步骤,将文件中的一个或多个变量名从文件中复制了出来。

from*语句

最后,下一个例子使用特殊的from形式:当使用*时,会取得模块顶层所有赋了值的变量名的拷贝。在这里,还是在脚本中使用复制后得到的变量名printer,而不需要通过模块名。

从技术角度来说,import和from语句都会使用相同的导入操作。from*形式只是多加个步骤,把模块中所有变量名复制到了进行导入的作用域之内。从根本上来说,这就是把一个模块的命名空间融入另一个模块之中;同样地,实际效果就是可以让我们少输入一些。

导入只发生一次

使用模块时,初学者最常问的问题之一似乎就是:“为什么我的导入不是一直有效?”时常说,第一次导入运作良好,但是在交互式会话模式(或程序运行)期间,之后的导入似乎就没有效果。事实上,本来就应该如此,原因如下。

模块会在第一次import或from时载入并执行,并且只在第一次如此。这是有意而为之的,因为该操作开销较大。在默认的情况下,Python只对每个文件的每个进程做一次操作。之后的导入操作都只会取出已加载的模块对象。

结果,因为模块文件中的顶层程序代码通常只执行一次,你可以凭借这种特性对变量进行初始化。例如,考虑下面文件simple.py。

此例中,print和=语句在模块第一次导入时执行,而变量spam也在导入时初始化:

第二次和其后的导入并不会重新执行此模块的代码,只是从Python内部模块表中取出已创建的模块对象。因此,变量spam不会再进行初始化:

有时需要一个模块的代码通过某种导入后再一次运行,后面会介绍reload语句。

import和from是赋值语句

就像def一样,import和from是可执行的语句,而不是编译期间的声明,而且它们可以嵌套在if测试中,出现在函数def之中等,直到执行程序时,Python执行到这些语句,才会进行解析。换句话来说,被导入的模块和变量名,直到它们所对应的import或from语句执行后,才可以使用。

此外,就像def一样,import和from都是隐性的赋值语句。

  • ·import将整个模块对象赋值给一个变量名
  • ·from将一个或多个变量名赋值给另一个模块中同名的对象

之前谈过的关于赋值语句方面的内容,也适用于模块的读取。例如,以from复制的变量名会变成对共享对象的引用。

就像函数的参数,对已取出的变量名重新赋值,对于其复制之处的模块并没有影响,但是修改一个已取出的可变对象,则会影响导入的模块内的对象。为了解释清楚,思考一下下面的文件small.py。

此处,x并不是一个共享的可变对象,但y是。导入者中的变量名y和被导入者都引用相同的列表对象,所以在其中一个地方的修改,也会影响另一个地方的这个对象。

对于赋值语句和引用之间的图形关系,可以查看图18-1(函数参数传递),只要在心中把“调用者”和“函数”换成“被导入模块”和“导入者”即可。实际效果是相同的,只不过现在面对的是模块内的变量名,而不是函数。在Python中,赋值语句工作起来都是一样的。

图18-1

文件间变量名的改变

前边的例子中,在交互会话模式下对x的赋值运算,只会修改该作用域内的变量x,而不是这个文件内的x。以from复制而来的变量名和其来源的文件之间并没有联系。为了实际修改另一个文件中的全局变量名,必须使用import。

因为像这样修改其他模块内的变量是常常困惑开发人员的原因之一(通常也是不良设计的选择)。

注意:前一个会话中对y[0]的修改是不同的。这是修改了一个对象,而不是一个变量名。

import和from的对等性

在上一个例子中,我们需要在from后执行import语句,来获取small模块的变量名。from只是把变量名从一个模块复制到另一个模块,并不会对模块名本身进行赋值。至少从概念上来说,一个像这样的from语句:

与下面这些语句是等效的:

就像所有赋值语句一样,from语句会在导入者中创建新变量,而那些变量初始化时引用了导入文件中的同名对象。不过,只有变量名被复制出来,而非模块本身。当我们使用语句from*这种形式时(from module import*),等效的写法是一样的,只不过是模块中所有的顶层变量名都会以这种方式复制到进行导入的作用域中。

注意:from的第一步骤也是普通的导入操作。因此,from总是会把整个模块导入到内存中(如果还没被导入的话),无论是从这个文件中复制出多少变量名。只加载模块文件的一部分(例如,一个函数)是不可能的,但是因为模块在Python之中是字节码而不是机器码,通常可以忽略效率的问题。

from语句潜在的陷阱

from语句会让变量位置更隐秘和模糊(与module.name相比,name对读者而言意义不大),有些Python用户多数时候推荐使用import而不是from。

不过不确定这种建议是否有根据。from得到了广泛的应用,也没太多可怕的结果。实际上,在现实的程序中,每次想使用模块的工具时,省略输入模块的变量名,通常是很方便的。对于提供很多属性的大型模块而言更是如此。例如,标准库中的Tkinter GUI模块。

from语句有破坏命名空间的潜质,至少从理论上讲是这样的。如果使用from导入变量,而那些变量碰巧和作用域中现有变量同名,变量就会被悄悄地覆盖掉。使用简单import语句时就不存在这种问题,因为你一定得通过模块名才能获取其内容(module.attr不会和你的作用域内的名为attr的变量相冲突)。不过,使用from时,只要你了解并预料到可能发生这种事,在实际情况下这就不是一个大问题了,尤其当你明确列出导入的变量名时(例如,from module import x,y,z)

另一方面,和reload调用同时使用时,from语句有比较严重的问题,因为导入的变量名可能引用之前版本的对象。再者,from module import*形式的确可能破坏命名空间,让变量名难以理解,尤其是在导入一个以上的文件时。在这种情况下,没有办法看出一个变量名来自哪个模块,只能搜索外部的源代码文件。事实上,from*形式会把一个命名空间融入到另一个,所以会使得模块的命名空间的分割特性失效。后面其他章节探讨。

此处,也许真正务实的建议就是:简单模块一般倾向于使用import,而不是from。多数的from语句是用于明确列举出想要的变量,而且限制在每个文件中只用一次from*形式。这样一来,任何无定义的变量名都可认为是存在于from*所引用的模块内。使用from语句时,的确要小心一点,但是只要有些常识,多数程序员都会发现这是一种方便的存取模块的方式。

何时使用import

当必须使用两个不同模块内定义的相同变量名的变量时,才真的必须使用import,这种情况下不能用from。

例如,如果两个文件以不同方式定义相同变量名。

而你必须在程序中使用这两个版本的变量名时,from语句就不能用了。作用域内一个变量名只能有一个赋值语句。

不过,只用一个import就可以,因为把所在模块变量名加进来,会让两个变量名都是唯一的。

这种情况很少见,在实际情况中,不太可能遇见。如果你这么做,import允许你避免名字冲突。

模块命名空间

模块最好理解为变量名的封装,也就是定义想让系统其余部分看见变量名的场所。从技术上来讲,模块通常相应于文件,而Python会建立模块对象,以包含模块文件内所赋值的所有变量名。但是,简而言之,模块就是命名空间(变量名建立所在的场所),而存在于模块之内的变量名就是模块对象的属性。

文件生成命名空间

文件如何变成命名空间?简而言之,在模块文件顶层(也就是不在函数或类的主体内)每一个赋值了的变量名都会变成该模块的属性。

例如,假设模块文件M.py的顶层有一个像X=1这样的赋值语句,而变量名X会变成M的属性,可在模块外以M.X的方式对它进行引用。变量名X对M.py内其他程序而言也会变成全局变量。

这里需要更正式地说明模块加载和作用域的概念以了解其原因。

  • ·模块语句会在首次导入时执行。系统中,模块在第一次导入时无论在什么地方,Python都会建立空的模块对象,并逐一执行该模块文件内的语句,依照文件从头到尾的顺序。
  • ·顶层的赋值语句会创建模块属性。在导入时,文件顶层(不在def或class之内)赋值变量的语句(例如,=和def),会建立模块对象的属性,赋值的变量名会存储在模块的命名空间内。
  • ·模块的命名空间能通过属性__dict__或dir(M)获取。由导入而建立的模块的命名空间是字典;可通过模块对象相关联的内置的__dict__属性来读取,而且能通过dir函数查看。dir函数大至与对象的__dict__属性的键排序后的列表相等,但是它还包含了类继承的变量名。也许不完整,而且会随版本而异。
  • ·模块是一个独立的作用域(本地变量就是全局变量)。正如前面章节所显示的,模块顶层变量名遵循和函数内变量名相同的引用/赋值规则,但是,本地作用域和全局作用域相同(更正式的说法是,遵循我们于第17章提及的LEGB范围规则,但是没有L和E搜索层次)。但是,在模块中,模块范围会在模块加载后变成模块对象的属性辞典。和函数不同的是(本地变量名只在函数执行时才存在),导入后,模块文件的作用域就变成了模块对象的属性的命名空间。

以下是这些概念的示范说明。假设在文本编辑器中建立如下的模块文件,并将其命名为module2.py。

这个模块首次导入时(或者作为程序执行时),Python会从头到尾执行其中的语句。有些语句会在模块命名空间内创建变量名,也就是副作用,而其他的语句在导入进行时则会做些实际工作。例如,此文件中的两个print语句会在导入时执行。

一旦模块加载后,它的作用域就变成模块对象(由import取得)的属性的命名空间。然后,可以结合其模块名,通过它来获取命名空间内的属性。

此处,sys、name、func以及klass都是在模块语句执行时赋值的,所以在导入后都变成了属性。

将会在第六部分(第25笔记开始)讨论类,但是请注意sys属性:import语句其实是把模块对象赋值给变量名,而文件顶层对任意类型赋值了的变量名,都会产生模块属性。

在内部模块命名空间是作为辞典对象进行储存的。它们只是普通字典对象,有通用的字典方法可以使用。可以通过模块的__dict__属性获取模块命名空间字典。

在模块文件中赋值的变量名,在内部成为字典的键。因此这里多数的变量名都反映了文件中的顶层的赋值语句。然而,Python也会在模块命名空间内加一些变量名。例如,__file__指明模块从哪个文件加载,而__name__则指明导入者的名称(没有.py扩展名和目录路径)。

属性名的点号运算

这里深入探讨变量名点号运算(notion of name qualification)的概念。在Python之中,可以使用点号运算语法object.attribute获取任意的object的attribute属性

点号运算其实就是表达式,传回和对象相配的属性名的值。例如,上一个例子中,表达式module2.sys会取出module2中赋值给sys的值。

同样地,如果有内置的列表对象L,而L.append会返回和该列表相关联的append方法对象。

属性的点号运算和前面的作用域法则有什么关系呢?其实,二者无关——这是不相关的概念。当使用点号运算来读取变量名时,就把明确的对象提供给Python,来从其中取出赋值的变量名。LEGB规则只适用于无点号运算的纯变量名。

以下是其规则。

  • 简单变量

X是指在当前作用域内搜索变量名X(遵循LEGB规则)。

  • 点号运算

X.Y是指在当前范围内搜索X,然后搜索对象X之中的属性Y(而非在作用域内)。

  • 多层点号运算

X.Y.Z指的是寻找对象X之中的变量名Y,然后再找对象X.Y之中的Z。

  • 通用性

点号运算可用于任何具有属性的对象:模块、类、C扩展类型等。

在后面会看到点号运算对类(这也是继承发生的地方)的意义还要多一点,但是一般而言,此处所列举的规则适用于Python中所有的变量名。

导入和作用域

无论程序中的导入结构或函数调用的结构是什么情况。变量的含义一定是由源代码中的赋值语句的位置决定的,而属性总是伴随着对对象的请求。

例如,考虑以下两个简单模块。第一个模块test3.py只在其文件中定义一个全局变量X,以及一个可修改全局变量X的函数。

x = 88
def f():
    global x
    x = 99

第二个模块modb.py定义自己的全局变量X,导入并调用了第一个模块的函数。

x  = 11

import test3
test3.f()
print(x,test3.x)

执行时,test3.f修改test3中的X,而不是modb中的X。test3.f的全局作用域一定是其所在的文件,无论这个函数是由哪个文件调用的,看下结果

11 99

换句话说,导入操作不会赋予被导入文件中的代码对上层代码的可见度:被导入文件无法看见进行导入的文件内的变量名。更确切的说法是:

  • ·函数绝对无法看见其他函数内的变量名,除非它们从物理上处于这个函数内。
  • ·模块程序代码绝对无法看见其他模块内的变量名,除非明确地进行了导入。

这类行为是语法作用域范畴的一部分:在Python中,一段程序的作用域完全由程序所处的文件中实际位置决定。作用域绝不会被函数调用或模块导入影响。

命名空间的嵌套

某种意义而言,虽然导入不会使命名空间发生向上的嵌套,但确实会发生向下的嵌套。利用属性的点号运算路径,有可能深入到任意嵌套的模块中并读取其属性。

例如,考虑下列三个文件。mod3.py以赋值语句定义了一个全局变量名和属性:

接着,mod2.py定义本文件内的X,然后导入mod3,使用点号运算来取所导入的模块的属性。

mod1.py也定义本文件内的X,然后导入mod2,并取出第一和第二个文件内的属性。

实际上,当这里的mod1导入mod2时,会创建一个两层的命名空间的嵌套。利用mod2.mod3.X变量名路径,就可深入到所导入的mod2内嵌套了的mod3。

结果就是mod1可以看见三个文件内的X,因此,可以读取这三个全局范围。

反过来讲,就没这回事了:mod3无法看见mod2内的变量名,而mod2无法看见mod1内的变量名。如果你不以命名空间和作用域的观点思考,而是把焦点集中在牵涉到的对象,这个例子就会比较容易掌握。

在mod1中,mod2只是变量名,引用带有属性的对象,而该对象的某些属性可能又引用其他带有属性的对象(import是赋值语句)。对于mod2.mod3.X这类路径而言,Python只会由左至右进行计算,沿着这样的路径取出对象的属性。

注意到:mod1可以说import mod2,然后mod2.mod3.X,但是,无法说import mod2.mod3。

这个语法牵涉所谓的包(目录)导入,下一个笔记介绍。包导入也会形成模块命名空间嵌套,但是,其导入语句会反映目录树结构,而非简单的导入链。

重载模块

模块程序代码默认只对每个过程执行一次。要强制使模块代码重新载入并重新运行,需要刻意要求Python这么做,也就是调用reload内置函数。

有时候使用reload让系统变得更加动态。简而言之:

  • ·导入(无论是通过import或from语句)只会模块在流程中第一次导入时,加载和执行该模块的代码。
  • ·之后的导入只会使用已加载的模块对象,而不会重载或重新执行文件的代码。
  • ·reload函数会强制已加载的模块的代码重新载入并重新执行。此文件中新的代码的赋值语句会在适当的地方修改现有的模块对象。

为什么要这么麻烦去重载模块?reload函数可以修改程序的一些部分,而无须停止整个程序。因此,利用reload,可以立即看到对组件的修改的效果。重载无法用于每种情况,但是能用时,可缩短开发的流程。

例如,想象一下,数据库程序必在启动时连接服务器,因为程序修改或调整可在重载后立即测试,在调试时,只需连接一次就可以了。长时间运行的服务器可以以这种方式更新自己。

因为Python是解释性的(或多或少),其实已经避免了类似C语言程序执行时所需的编译/连接步骤:在执行程序导入时,模块会动态加载。重载进一步地提供了性能优势,让你可以修改执行中的程序的一部分,而不需要中止。注意:reload当前只能用在Python编写的模块;用C这类语言编写的编译后的扩展模块也可在执行中动态加载,但无法重载。

reload移入了imp标准库模块中——在Python 3.0中叫做imp.reload。需要一条额外的import语句或from语句来载入该工具。

reload基础

与import和from不同的是:

·reload是Python中的内置函数,而不是语句。

·传给reload的是已经存在的模块对象,而不是变量名。

·reload在Python 3.0中位于模块之中,并且必须导入自己。

因为reload期望得到的是对象,在重载之前,模块一定是已经预先成功导入了(如果因为语法或其他错误使得导入没成功,你得继续试下去,否则将无法重载)。此外,import语句和reload调用的语法并不相同:reload需要小括号,但import不需要。

重载看起来如下所示。

一般的用法是:导入一个模块,在文本编辑器内修改其原代码,然后将其重载。当调用reload时,Python会重读模块文件的源代码,重新执行其顶层语句。也许有关reload所需要知道的最重要的事情就是,reload会在适当的地方修改模块对象,reload并不会删除并重建模块对象。因此,程序中任何引用该模块对象的地方,自动会受到reload的影响。

下面是一些细节。

  • ·reload会在模块当前命名空间内执行模块文件的新代码。重新执行模块文件的代码会覆盖其现有的命名空间,并非进行删除而进行重建。
  • ·文件中顶层赋值语句会使得变量名换成新值。例如,重新执行的def语句会因重新赋值函数变量名而取代模块命名空间内该函数之前的版本。
  • ·重载会影响所有使用import读取了模块的客户端。因为使用import的客户端需要通过点号运算取出属性,在重载后,它们会发现模块对象中变成了新的值。
  • ·重载只会对以后使用from的客户端造成影响。之前使用from来读取属性的客户端并不会受到重载的影响,那些客户端引用的依然是重载前所取出的旧对象。

reload实例

在下面这个例子中,要修改并重载一个模块文件,但是不会中止交互模式的Python会话。重载也在很多场景中使用(参考边栏内容),但是,为了解释清楚,我们举一个简单的例子。首先,在选择的文本编辑器中,编写一个名为changer.py的模块文件,其内容如下。

这个模块会建立并导入两个变量名:一个是字符串,另一个是函数。现在,启动Python解释器,导入该模块,然后调用其导出的函数。此函数会打印出全局变量message的值。

不关掉解释器,现在,在另一个窗口中编辑该模块文件。

改变message变量和printer函数体:

然后,回到Python窗口,重载该模块从而获得新的代码。注意下面的交互模式,再次导入该模块并没有效果。得到原始的message,即使文件已经修改过了。我们得调用reload,才能够获取新的版本。

注意:reload实际是返回了模块对象:其结果通常被忽略,但是因为表达式结果会在交互模式提示符下打印出来,Python会打印默认的<module 'name'>表现形式。

为什么要在意:模块重载

除了可以在交互式提示符号下重载(以及重新执行)模块外,模块重载在较大系统中也有用处,在重新启动整个应用程序的代价太大时尤其如此。例如,必须在启动时通过网络连接服务器的系统,就是动态重载的一个非常重要的应用场景。

重载在GUI工作中也很有用(组件的回调行为可以在GUI保持活动的状态下进行修改)。此外,当Python作为C或C++程序的嵌入式语言时,也有用处(C/C++程序可以请求重载其所执行的Python代码而无须停止)。参考《Programming Python》有关重载GUI回调函数和嵌入式Python程序代码的更多细节。

通常情况下,重载使程序能够提供高度动态的接口。例如,Python通常作为较大系统的定制语言:用户可以在系统运作时通过编写Python程序定制产品,而不用重新编译整个产品(或者甚至获取整个源代码)。这样,Python程序代码本身就增加了一种动态本质了。

不过,为了更具动态性,这样的系统可以在执行期间定期自动重载Python定制的程序代码。这样一来,当系统正在执行时,就可采用用户的修改;每次Python代码修改时,都不需要停止并重启。并非所有系统都需要这种动态的实现,但对那些需要的系统而言,模块重载就提供了一种易于使用的动态定制工具。

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兴焉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值