1-Python - 模块那些事儿-上回

before

如果要做一份关于"你为什么选择Python"的问卷调查,那么"Python有丰富库(模块)资源"无疑会榜上有名。

这一章,我们就来一场关于库(模块)的探秘之旅吧。

about

在Python中,模块分为三种:

  • 内置模块:打开Python解释器目录,内置模块就在Lib文件夹下

  • 第三方(扩展)模块:第三方模块被统一地存放在本地Python解释器的Lib/site-packages文件夹内

  • 自定义模块:就是我们自己写的模块了。

标准库

Python将常用的实现某类功能的代码组织在一起并起名为模块,随着Python解释器安装到本地,称为内置模块。为了有别于其他的模块,内置模块又称为Python标准库模块。但Python能干的事情实在是太多了,不可能把所有的模块都预先安装在本地的解释器内。

标准库模块被统一放在一个文件夹内,这个文件夹又称为Python标准库。

第三方模块

Python的开发者们根据特定的应用场景开发出了特定用途的模块,这些模块经过Python官方审核通过,就可以被广大Python开发者使用了,这种现成的并未随着解释器内置的模块被统称为第三方模块。

所有发布的模块(包括第三方模块)均维护在PyPI(the Python Package Index)网站上。注意第三方模块在首次使用前必须下载。下载方式有pip或其他的方式。下图所示为PyPI主页:

https://pypi.org/

自定义模块

我们自己在项目写的模块,其实一个Python文件就是一个模块,所以模块并没那么神秘!

曲径通幽处,让我们一探究竟吧!

自定义模块

我们自己在项目写的模块,其实一个Python文件就是一个模块,所以模块并没那么神秘!

曲径通幽处,让我们一探究竟吧!

必要的准备

在探索模块背后的故事之前,要做一些准备。

首先创建module文件夹,其内有a.py和b.py两个同级目录文件。我们接下来的示例代码都会围绕module文件内的两个文件展开:

M:\module\  
   ├─ a.py 
   ├─ b.py
   └─ c.py  

自定义模块的创建

在Python中,一个py文件就是一个模块。但模块名的命名也是要遵循变量的命名规范的,避开关键字和与其他的模块名一致的名字,比如自己定义的模块名不要写成def.py或者 time.py这种方式。

# b.py  
x = 1  
y = [2, 3]    
def foo(x):  
    print('b.foo prints ', x)  
    
# c.py  
print("this is module c")  
x = 2  
y = [3, 4]    
def bar(x):  
    print('c.bar prints ', x)  

上例b.py为一个模块。当这个模块被别的模块调用时,变量xyfoo都会成为模块b的属性,也就是说位于b模块的全局作用域内的变量、函数名、类名都将成为模块b的属性,在别的模块通过module.attribute方式被调用。

模块的导入

模块的导入使用import语句,其语法如下:

import module_name      								# 推荐  
import module_name1, module_name2...module_name n   	# 不推荐  

import语句将模块整体导入到当前模块中,如果一次导入多个模块可以用逗号隔开,但并不推荐这种方式。

# a.py  
import b  
print(b.x, b.y, b.foo)  # 1 [2, 3] <function foo at 0x011E2270>  

上例在模块a中,第2行导入了模块b,第3行通过module.attribute方法打印出了b模块内的变量对应的值和函数地址。

通过上例的import的导入,我们可以在a.py中使用所有模块b的所有属性。但我们想象一个情景,如果模块b中有成千上万个属性被导入到模块a的作用域中(模块中都维护一个作用域来管理这些变量),但a.py中只使用了其中一个或几个属性,而真正用到的属性只有其中一个或几个。如果当程序中这种情景很多的话,无疑会让整个作用域变得臃肿。针对import的这种情况,Python采用from语句来解决这个问题。

from语句,其语法如下:

from module_name import attribute     				# 推荐  
from module_name import attribute1, attribute2     # 不推荐  

from语句获取被调用模块内的指定的属性名:

# a.py  
from b import foo  
print(foo)  			# <function foo at 0x00A12270>  
# print(x)    			# NameError: name 'x' is not defined  
from b import x   
print(x)    			# 1  

上例中,在模块a中专门导入b模块的foo属性,而没有导入的属性则无法调用,如没有导入"x"属性导致的打印报错,要想调用模块bx属性,就要先导入才能使用。

相对于import语句,from语句则是用什么导入什么,更显高效。

如果变量多了怎么办?岂不是要写很多的from语句吗?是的,但Python为解决这个问题,提供了另一种写法:

from module_name import *           					# 不推荐

采用*号,含义跟import 语句差不多,都是将被调用模块的属性整体复制过来使用。

# a.py 
from b import *  
print(x, y)     # 1 [2, 3]  

采用*这种方式,就可以直接在模块a中使用模块b的所有属性了,非常灵活方便。但我们不推荐这种写法。因为并不知道到底都有哪些变量被导入,很可能会跟本地的变量造成冲突,这是我们不推荐的原因。不过Python为此也做出了一些努力,使用__all__方法来跟*号搭配使用,而且该方法只能跟*搭配才起作用,并且与别的导入方式并不冲突:

# b.py  
__all__ = ['x', 'y']  
x = 1  
y = [2, 3]  
def foo(x):  
    print('b.foo prints ', x)   
# a.py  
from b import *  
print(x, y)     # 1 [2, 3]  
# print(foo)    # NameError: name 'foo' is not defined    
from b import foo  
print(foo)      # <function foo at 0x01232300>  

上例中,模块b中,在用*导入的方式时,希望有哪些变量可以被别的模块调用,那么就把它们放在__all__方法维护的列表内。不在列表内的将无法调用,如第10行在调用foo时抛出了NameError异常,而第9行则被顺利调用。那么为了能调用模块b中的foo,就需要在第11行再次用from方式显式导入。

from语句的弊端

from语句会让变量名变得模糊。如果导入多个模块,那么使用from语句很难分辨某个变量来自哪个模块:

# a.py 
from b import *  
from c import *  
print(x)  
print(n)  

上例中,能一眼看出xn归属于哪个模块吗?相较于import语句的b.x,单独的x对我们来说并不能提供太多的有效信息。而且form语句也在潜在破坏名称空间。

什么意思呢?来看示例:

# a.py 
from b import x  
x = 2  
print(x)  # 2  

上例中,通过from语句导入过来的变量x被本地的作用域中的变量x悄悄地覆盖掉了。而使用import则有效地避免了此类问题,并且Python提供了as语句来解决这个问题:

import module_name as alias      						  
from module_name import attribute as alias 	# 推荐 

通过as语句为模块的某个属性起个别名。

# a.py 
from b import x as d  
x = 2  
print(x)  # 2  
print(d)  # 1  

上例中,as语句有效地避免重了名问题。通过为模块b中的属性x起个别名dd指向真实的x

object.attribute

当导入模块后,通过模块名点属性,调用其内对应的方法。

在Python中,任何对象都可以通过.来获取该对象的attribute属性,如果该attribute存在的话。

点号运算是一个表达式,返回该对象匹配的属性名的值。比如s.replace会返回sreplace方法对象。需要注意的是,s在通过点号运算找replace方法时,和作用域法则没有关系。

单个变量s,从当前作用域内找到变量s,遵从LEGB法则。

s.f,从当前作用域内找到变量s,然后从s中找属性f,跟作用域无关。

s.f.e,从当前作用域内找到变量s,然后从s中找属性f,再从属性f中找寻属性e

让模块如脚本一样运行

前面我们说,每个文件都是一个模块,那么每个模块不能仅仅被调用,也要负责本身的逻辑。如在模块a中定义了一个登录函数,在本模块内实现登录逻辑:

# a.py  
def login(user, pwd):  
    print(user, pwd)  
login('oldboy', '666')      	# oldboy 666  
# b.py  
import a    						# oldboy 666  

上例模块a实现了一个登录功能。那么当这个模块被模块b调用时,也会触发该函数的执行。但这并不是我们想要的结果,我们只是想调用这个login函数,实现自己的功能,而不是触发原函数的执行。这该怎么办呢?Python采用__name__帮助我们解决这个问题:

# a.py  
print(__name__, type(__name__))     	# __main__ <class 'str'>  
# b.py  
print(__name__, type(__name__))     	# __main__ <class 'str'>  
import a    								# a <class 'str'>  

由上例的执行结果可以总结__name__的特性:

  • 当模块自己被当成脚本执行时,__name__返回__main__
  • 当模块被导入而引发内部代码的执行时,__name__返回该模块的名字。

根据__name__的特性,来解决我们的问题:

# a.py  
def login(user, pwd):  
    print(user, pwd)    
if __name__ == '__main__':    
    login('oldboy', '666')      		# oldboy 666  
# c.py  
import a    

上例中,通过在模块a内判断__name__的值的不同,来决定login函数是否执行。也就是说该文件是被当成脚本还是当成模块被导入,而会分别返回不同的值。当__name__等于__main__的时候,表示是模块自己在执行代码,就执行login函数,而当__name__不等于__main__的时候,表示是要被别的模块导入,就通不过if判断,从而解决了我们的需求。

模块导入规范

在使用这些模块时,也要遵循导入规范:

  • 内置模块在模块最上部,第三方模块在中间,自定义模块放在最下面。
  • import语句、 from语句和def语句一样,是可执行的赋值语句,那么二者可以嵌套在def或者if语句中,只有当程序在执行到该语句时,Python才会解析。我们在使用模块前,就像def一样,要先导入才能使用。
  • import语句是将模块整体赋值给一个变量,模块内的所有全局作用域下的变量名都成为该变量的属性。
  • from语句是将模块内位于全局作用域下的一个或者多个变量名赋值给一个变量。

需要注意的是,form语句做赋值操作时,会造成对共享对象的引用:

# a.py  
from b import x, y  
print(x, y)  # 1 [2, 3]  
x = 11  
y[1] = 22  
import b  
print(b.x, b.y)  # 1 [2, 22]  

上例中的x并不是一个可变类型的对象,而y是,第5行在对y中的元素重新赋值的时候,是对导入进来的y变量对应的列表对象进行的操作。所以在此处修改是会影响到原模块的变量的。

如果想要实现对模块b中x变量的修改,那么就必须使用import。修改方式如下:

# a.py  
import b  
b.x = 11  

模块导入只发生一次

当import和form语句第一次执行时,会逐一执行被导入模块内的语句:

# c.py  
print("this is module c")  
def foo():  
    print('before reload')

# a.py  
import c    								# this is module c  
import c  
import c  

由上例可以看到,只有在第一次导入时触发了模块c内的代码的执行,后面的每次导入只是取出第一次导入时赋值后的变量对象而已,至于这么做的原因——是因为模块导入需要很大的开销的。

模块导入都发生了什么

关于sys:https://www.cnblogs.com/Neeo/articles/14071371.html

本节主要使用了sys.pathsys.modules这两个方法。

我们不止一次说模块只有在第一次被导入时,执行一次模块内部的代码。

通过import语句将整个模块当作对象赋值给一个变量(该模块对象),模块内位于全局作用域内的变量都成为该变量的属性。

或者通过from语句将模块内的一个或多个变量赋值给一个同名变量,我们直接调用此变量而省略模块名这一步骤。

下面例子中,from语句和import是等效的:

import c  
foo = c.foo  
foo()   								# before reload  
from c import foo  
foo()   								# before reload  

那么它们在背后是如何工作的?

  • 找到导入的模块文件。Python解析到import语句时,会自动地在sys.path中搜索模块路径,并找到这个模块,添加到sys.modules字典中。一般地,Python有标准库模块帮我们完成这些事情。

  • 编译成字节码。Python在找到符合import语句的文件后,检查此文件的时间戳,如果发现字节码文件(文件在导入时就被编译完成)比源代码文件时间戳早(比如你修改过原文件),那么就会重新生成字节码,否则就会跳过此步骤。如果,Python在搜索时只找到了字节码而没有找到源代码文件,那么就会直接执行字节码文件。

  • 执行字节码。当Python在执行import语句对应的字节码文件时,文件内的所有代码都会依次执行。在执行时,所有被赋值的变量都会成为该模块的属性。

上面说过脚本执行到模块导入时,就在sys.path中搜索模块路径,那这个路径是啥?sys.modules又是干嘛的?

import b
import sys
print(sys.path)

"""
[
    'D:\\tmp\\module', 'D:\\tmp\\module', 'C:\\Python36\\python36.zip', 
    'C:\\Python36\\DLLs', 'C:\\Python36\\lib', 'C:\\Python36', 
    'C:\\Users\\Anthony\\AppData\\Roaming\\Python\\Python36\\site-packages', 
    'C:\\Python36\\lib\\site-packages', 'C:\\Python36\\lib\\site-packages\\win32', 
    'C:\\Python36\\lib\\site-packages\\win32\\lib', 'C:\\Python36\\lib\\site-packages\\Pythonwin', 
    'C:\\Program Files\\JetBrains\\PyCharm2018.1.3\\helpers\\pycharm_matplotlib_backend'
]
"""
print(sys.modules)
"""
# 结果集太多,仅展示部分
{
    'builtins': <module 'builtins' (built-in)>, 'sys': <module 'sys' (built-in)>, 
    '_frozen_importlib': <module 'importlib._bootstrap' (frozen)>, '_imp': <module '_imp' (built-in)>, 
    '_warnings': <module '_warnings' (built-in)>, '_thread': <module '_thread' (built-in)>, 
    '_weakref': <module '_weakref' (built-in)>, '_frozen_importlib_external': <module 'importlib._bootstrap_external' (frozen)>, 
    '_io': <module 'io' (built-in)>, 'marshal': <module 'marshal' (built-in)>, 'nt': <module 'nt' (built-in)>, 
    'sitecustomize': <module 'sitecustomize' from 'C:\\Program Files\\JetBrains\\PyCharm2018.1.3\\helpers\\pycharm_matplotlib_backend\\sitecustomize.py'>, 
    'b': <module 'b' from 'D:\\tmp\\module\\b.py'>
 }
"""

如上例,sys.path是一个全局列表,它维护了一个个的路径信息,索引0位置是当前脚本所在的工作目录,其他的也都是各个模块所在的目录。

当一些极端情况下,sys.path[0]如果是"",表示当前工作目录不可用,如因此产生了一些问题,可以强制将该工作目录sys.path[.insert(0, current_work_path)

当脚本执行到导入模块的语句时,Python解释器会自动的在sys.path中维护的目录中寻找模块是否存在,如果存在即将该目录内的模块返回,这也意味着,如果你想让你的模块能优先被找到,你就把你的模块所在目录插入到列表的前面。

如果所有的目录下都找了一遍,也没找到要导入的模块,就抛出importError信息。

sys.path返回了要导入的模块,就会将该模块缓存到sys.modules这个全局字典中去,该模块的模块名称为key,模块所在的路径为value。

这个全局字典拥有字典的全部特性,那么当重复导入时,Python首先会检查这个缓存字典,如果存在,直接从字典内取出模块对象。如果字典内没有该模块对象,那么就会执行上面的步骤生成这个模块对象,并将模块返回,如果在该步骤中没有找到模块,仍然会抛出importError信息。

因为缓存字典的存在,也能解释通为啥模块导入只发生一次了!

模块重载

上面说过模块只有在第一次导入时执行一次。但在有些时候,必须使模块重新导入并重新运行。Python提供了reload函数强制使模块重新执行一次导入过程:

# c.py  
print("this is module c")  
def foo():  
    print('before reload')  

# 解释器执行  
>>> import c  
this is module c  
>>> c.foo()  
before reload    

# c.py  
print("this is module c")  
def foo():  
    print('after reload')    

# 解释器执行  
>>> c.foo()  
before reload  
>>> from importlib import reload  
>>> reload(c)  
this is module c  
<module 'c' from 'F:\\c.py'>  
>>> c.foo()  
after reload  

由上例的现象可以看到,当模块被导入后,就不受源文件修改的影响了,如果需要,就是用reload重载(相当于重新导入)。

关于reload函数需要补充的是一下内容:

  • Python 2.x版本中,reload函数为内置函数,可以直接调用。

  • Python 3.x版本中,reload函数被移动到importlib模块内了,调用前需要导入。

  • reload函数无法重载from语句的导入,仅限于import语句形式。

还有一点需要注意,目前有两种形式导入reload:

from importlib import reload  		# 推荐使用
reload(module_obj)  
from imp import reload  				# 强烈不推荐
reload(module_obj)  

你可能在别的代码中发现reload函数是在imp模块内,但是在Python 3.4版本以后,imp模块在慢慢弃用,reload函数也从imp模块转移到importlib模块内,虽然目前imp还能用,但我们并不推荐使用了。

模块的闭环导入

现在有b、c两个模块,且各自的代码如下:

# c.py  
import b  
def foo(): ...  
b.foo()  	# AttributeError: module 'b' has no attribute 'foo'
# b.py  
import c  
def foo(): ...  
c.foo()  	  

正如上面例子所示,在模块c中导入模块b,在模块b中导入模块c。此时我们运行模块c,会发生什么样的事情?

  • 程序从上往下执行到第2行,导入模块b,程序跳转到模块b内,执行其内的代码。
  • 程序在模块b内执行到第6行,导入模块c,程序又跳转到模块c内,从第2行开始往下执行,第2行,定义函数foo,这步没问题,接着往下走。
  • 序执行到第4行,调用b中的foo属性,抛出AttributeError错误并中止程序。原因是什么呢?原因是在程序在模块b内只执行了一行代码就跳转了,因为碰到调用模块c(第6行),程序没有往下执行。也就是说,foo变量没有成为模块c的属性,却在第4行就去调用,结果就是报错。

由上例可以看到,在导入模块时,应该避开这种相互导入的情况,也就是闭环式的导入。


that's all
posted @ 2020-12-01 11:24  听雨危楼  阅读(408)  评论(0编辑  收藏  举报