《最值得收藏的python3语法汇总》之模块与包机制

目录

关于这个系列

1、组织你的代码

2、Import导入模块

三种导入方式

__name__的作用

搜索路径

循环导入问题

3、构造包

4、Import导入包

5、预编译的模块


 

关于这个系列

《最值得收藏的python3语法汇总》,是我为了准备公众号“跟哥一起学python”上面视频教程而写的课件。整个课件将近200页,10w字,几乎囊括了python3所有的语法知识点。

你可以关注这个公众号“跟哥一起学python”,获取对应的视频和实例源码。

这是我和几位老程序员一起维护的个人公众号,全是原创性的干货编程类技术文章,欢迎关注。


 

1、组织你的代码

通常,我们的程序不会只有一个函数,如果功能需求稍微复杂的程序,也不会都写在一个.py文件里面。这就涉及到一个问题,我们该如何组织这些.py文件呢?

正如我们日常工作中将我们的文档通过文件夹分类一样,我们也通过.py文件以及文件夹的方式进行分类组织。一个大的项目可能有数百个.py文件,这种分类手段是有必要的,而且是必需的。

 

我们将.py文件称作python的模块(module),而将这种文件夹(带__init__.py),称之为python的包(package)。

我们利用模块和包,可以更好地组织我们项目的代码。同时,它们也提供了类似函数那样的代码复用能力。

我们来看python官方手册提供的一个典型案例,下面是一个关于“声音处理”的包目录层次结构:

sound/                          Top-level package

      __init__.py               Initialize the sound package

      formats/                  Subpackage for file format conversions

              __init__.py

              wavread.py

              wavwrite.py

              aiffread.py

              aiffwrite.py

              auread.py

              auwrite.py

              ...

      effects/                  Subpackage for sound effects

              __init__.py

              echo.py

              surround.py

              reverse.py

              ...

      filters/                  Subpackage for filters

              __init__.py

              equalizer.py

              vocoder.py

              karaoke.py

              ...

 

“声音处理”包含了很多功能,比如对各种声音格式的支持、音效增强、过滤器等等。我们将这些功能进行分层拆分,分别放在不同的.py和package中实现。

这里的sound是一个顶层的package,而formats、filters、effects是它的子package,package支持这样的层层嵌套,和文件夹目录一样。需要注意的是,只有包含了__init__.py的目录才会被认为是一个package,否则它就是一个普通的文件夹。__init__.py可以为空,后面我们会详细介绍它的写法。

这里面的所有.py文件都是模块。模块里面的内容没有要求,可以包含变量、函数、类,也可以为空。

 

Python解释器本身提供了庞大的标准库,其本质就是模块或者包。我们使用比较多的re、datetime、copy、array、enum、os、sys、io等等都是标准库。

你可以通过查询官方的标准库参考手册:

https://docs.python.org/zh-cn/3/library/index.html

后面我们也会重点介绍一些常用的标准库。

 

2、Import导入模块

  • 三种导入方式

模块和包,是可以被复用的。比如上面的sound包,我们可以在其它的python程序中导入它,并调用它里面的函数、变量或者类。

这一过程,通过Import机制来实现,如下是最简单的方式:

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./10/10_1.py
# 模块
import keyword
print(keyword.kwlist)

我们通过import导入了一个模块keyword,并接下来获取了它的一个变量kwlist打印出来。

Import语句需要写在使用它的代码之前,而通常我们是建议所有的import语句都写在文件的开头。

 

我们看看keyword.py是什么样的?它非常简单:

# LIB/keyword.py



__all__ = ["iskeyword", "kwlist"]



kwlist = [

    'False',

    'None',

    'True',

    'and',

    'as',

    'assert',

    'async',

    'await',

    'break',

    'class',

    'continue',

    'def',

    'del',

    'elif',

    'else',

    'except',

    'finally',

    'for',

    'from',

    'global',

    'if',

    'import',

    'in',

    'is',

    'lambda',

    'nonlocal',

    'not',

    'or',

    'pass',

    'raise',

    'return',

    'try',

    'while',

    'with',

    'yield'

]



iskeyword = frozenset(kwlist).__contains__

第一行,给一个叫__all__的变量进行了赋值。这个变量有特殊含义,它是给python解释器看的,它表示在import该模块时需要导入的符号列表。

第二行,定义了一个变量kwlist,是一个列表,它里面存了python的所有保留字。

第三行,定义iskeyword是frozenset的一个内建函数__contains__,它判断一个字符串是不是kwlist里面的保留字。

 

我们还可以通过from xxx import xxx的方式导入:

from keyword import kwlist

print(kwlist)

这种导入比前面的要精确,范围更小。这种导入方式适合你明确知道你需要的功能的时候,比如这个例子中,我明确知道我只需要打印kwlist,所以可以指定只导入kwlist。

这种方式在使用时会显得更加简洁,我们只需要使用kwlist即可,而不用像上面那样写keyword.kwlist。

另一个好处是,第一种方式会把这个模块的所有符号一股脑的全部导入进来,而第二种方式则更加精确。当我们需要使用同一个模块中的多个函数或者变量时,我们可以采用第一种方式,因为采用第二种方式的话我们会写多次from xxx import xxx。

 

无论上面哪种方式,它们都存在命名冲突的风险。比如我们自己的程序中可能已经存在一个变量叫做keyword或者kwlist了。这时候我们需要为我们导入的模块或者函数起一个别名,如下:

import keyword as kwmodule

from keyword import kwlist as kwall

print(kwmodule.kwlist)
print(kwall)

我们通过as xxx来起别名,如上kwmodule就是keyword模块的别名,而kwall就是变量kwlist的别名。在下面的使用中,我们就只能使用这个别名。这就解决了命名重复的问题。

 

第二种导入方式也可以导入模块中所有的符号(变量、函数、类等),需要使用*通配符来表示。它和第一种方式的全部导入,是有区别的。第一种方式导入的符号是keyword,模块中的所有符号都包含在keyword里面,所有调用方式都必须是keyword.xxx的方式。而采用*号通配符,则是将模块中的符号全部导入进来,可以直接调用。

我们可以通过dir(obj)内建函数,打印出某个对象的符号表,如果不带参数,则打印当前模块的符号表。

import keyword

print(dir())
print(dir(keyword))

输出为:

['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'keyword']

['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'iskeyword', 'kwlist']

前面那些__XXX__的大家先不用关注,他们是python对象自带的符号。我们看到,这种import方式,它只导入了keyword。而keyword里面包含了iskeyword和kwlist。

 

from keyword import *

print(dir())

输出为:

['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'iskeyword', 'kwlist']

这种方式是直接把模块中的符号导入进来了,平铺在当前符号表中。

*号通配符的方式,通常不被推荐使用。一个重要的原因是,如果模块中的符号很多的时候,你很难保证不出现重名的情况,重名导致的覆盖会让你的程序变得很难看懂,而且容易出错。另外,我们的程序中通常会导入多个外部模块,要保证这些模块中的符号相互之间不命名冲突,几乎是不可能的。

 

  • __name__的作用

一个模块只会被导入一次,不管你执行了多少次import。这样可以防止导入模块被一遍又一遍地执行。

在第一次导入时,模块中的代码会被依次执行。你可以理解为,Python解释器在import的时候,将对应的模块解析并执行了一遍。

我们定义了一个模块,用于管理“水果清单”,命名为fruits.py。

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./10/
# 这是一个用于演示模块的例子

fruits = {'apple': 10, 'pear': 5}

print(f"use module: fruits. {fruits}")

def get_fruits():
    return fruits

def add_fruits(*args):
    if len(args) == 0:
        return
    for item in args:
        fruits[item[0]] = item[1]

def del_fruit_one(name):
    fruits.pop(name)

我们在另外一个文件中,用不同的方式导入这个模块。

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./10/10_2.py
# 模块
import fruits
import fruits as ff
from fruits import add_fruits
add_fruits(['orange', 8])
print(fruits.fruits)

输出为:

use module: fruits. {'apple': 10, 'pear': 5}

{'apple': 10, 'pear': 5, 'orange': 8}

我们可以看到,我们导入了三次,但是模块中的这行打印语句print(f"use module: fruits. {fruits}")只被执行了一次。

 

这样的设计是很有用的,在一些复杂的项目中,有些模块会被层层导入。比如:A.py 导入了B.py,而C.py同时导入了A.py和B.py,对于C来说,B是被重复导入了的。在C语言中,我们需要给每个头文件定义一个宏,就是为了防止被重复导入。Python已经帮我们解决了这个问题。

 

模块中的可执行代码通常用于模块的一些初始化工作,比如给一些全局变量赋值等。但是对于某些代码,我们可能不太希望在导入时被执行,比如对模块功能的一些测试代码。对于这种代码,我们可以通过判断__name__等于__main__来避免被执行。

 

如下,我们给fruits模块添加了一些不希望被执行的测试代码:

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./10/
# 这是一个用于演示模块的例子

fruits = {'apple': 10, 'pear': 5}

print(f"use module: fruits. {fruits}")

def get_fruits():
    return fruits

def add_fruits(*args):
    if len(args) == 0:
        return
    for item in args:
        fruits[item[0]] = item[1]

def del_fruit_one(name):
    fruits.pop(name)


# for test, 在被其它程序导入时,不会被执行
if __name__ == '__main__':
    add_fruits(['orange', 8])
    del_fruit_one('apple')
    print(get_fruits())

当我们直接运行fruits.py这个模块时,它的输出为:

use module: fruits. {'apple': 10, 'pear': 5}

{'pear': 5, 'orange': 8}

可以看到,最后那段测试代码是被执行了的。而当我们import模块时,这部分代码是不会被执行的。

if __name__ == '__main__'

这个语句被广泛使用,它并不是什么特殊的语法,它就是一个普通的if条件判断。只不过我们利用了__name__这个属性的特点。所有的模块都有__name__这个属性,它表示当前模块的名字。如果当前是主运行模块(就是python解释器直接运行的那个.py文件),那么它的__name__就是“__main__”。而对于import的模块,__name__则是模块名,本实例中为‘fruits’。

所以,当fruits.py被直接运行时,那段测试代码可以被执行。当作为模块被导入时,则不会执行。我们同样可以通过断点来查看:

 

写习惯了C语言的同学一定知道,C语言的程序入口是main()函数。而python是解释性语言,它是从.py文件的第一行依次往下解析并执行的。如果你也想在python中定义一个执行入口,那么可以使用这个__name__的条件判断。

做法是,将除了全局变量定义、函数定义、类定义等定义相关的代码,都放到这个条件判断里面。这样,python解释器在解析前面的定义代码的时候,不会实质性执行任何逻辑。只有到了这里才会执行,相当于是一个程序执行的入口。

 

这种写法是非常普遍的,大家一定要学会。

 

  • 搜索路径

当我们import某个模块的时候,系统是如何知道这个模块放在哪里的呢?这就是import的搜索路径问题。

Python将搜索路径存储在sys模块的path变量中,它是一个列表结构,里面存储了多个路径。Import时,系统会依次在这些路径下面去找对应的模块。我们可以将这个path变量打印出来看看,因为这个比较简单,我们直接在cmd中输出:

>>> import sys

>>> sys.path

['',

'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\python38.zip', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\DLLs', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\lib', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\lib\\site-packages']

Sys.path里面定义了多个路径,其中第一个路径是一个空字符串,这表示在当前目录下搜索(运行python解释器的路径)。在import时,首先会在python内置库中进行搜索,找不到才从path里面逐个搜索。

 

Sys.path是可以修改的,我们可以把我们期望的搜索路径加入到列表里面去。比如,我们新建一个目录叫pyfruits,然后把fruits.py剪切到这个目录下。这样,我们import fruits时,是找不到模块的,会报错。

ModuleNotFoundError: No module named 'fruits'

 

这时,我们可以将pyfruits路径加入到sys.path中去。

import sys

sys.path.append('D:\\跟我一起学python\\练习\\10\\pyfruits')
import fruits

这个代码就不会报错,因为系统会自动到pyfruits目录下面去搜索该模块。

 

 

  • 循环导入问题

Python支持模块之间的循环导入,如下:

# a.py

import b
def func_a():
    pass



# b.py

import a

def func_b():
    pass



#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python
#  file: ./10/10_4.py
#  模块间循环导入
import a
a.func_a()

上面定义了两个模板a.py和b.py,它们之间相互导入对方。这就是循环导入,python支持这种导入方式。

循环导入很容易出错,所以我们需要深入去理解python导入模块的过程,才能避免这些错误,比如我们将上面的导入方式改为 from xxx import xxx,就会报错:

# a.py

from b import func_b
def func_a():
    pass



# b.py

from a import func_a

def func_b():
    pass



#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python
#  file: ./10/10_4.py
#  模块间循环导入
import a
a.func_a()

这种循环导入会导致异常:

ImportError: cannot import name 'func_a' from partially initialized module 'a' (most likely due to a circular import)

它的意思是,由于循环导入的原因,模块a没有完全初始化,所以func_a找不到。

我们通过下图来分析一下整个过程:

 

我们解释一下上图的过程:

  1. 在我们的主文件里面执行导入语句 import a。解释器会到sys模块的modules变量里面去查找,是否已经存在a。sys的modules是一个字典数据类型的变量,它存储了当前运行环境中所以已经加载过的modules。如果a从未被加载过,那么会创建一个module a的对象,并存储在sys.modules中。
  2. 填充module a的__dict__变量,这个变量是用来存储module a的属性的,包括变量、函数、类等等。解释器从a.py的第一条语句开始解析,由于第一条语句是from b import func_b,所以需要先导入模块b。
  3. 同样,解释器会到sys模块的modules变量里面去查找,是否已经存在b。由于是第一次加载,所以需要新创建module b对象,并保存在sys.modules中。
  4. module b的__dict__变量同样为空,所以需要填充。同模块a的过程一样,解释器会去逐条解析b.py,由于第一条语句是 from a import func_a,所以又需要先导入模块a。到这里,其实已经形成了一个循环。
  5. 此时,再导入a时,我们在sys.modules中能查询到module a了。因为我们真正要导入的是模块a中的func_a这个属性,所以还需要去查询module a的__dict__里面是否存在func_a。很显然,__dict__还是空的,没有找到func_a,所以就报出来前面的异常信息。

事实上,解释器还没来得及解析a.py里面的func_a语句,就被打断了而去导入b。这就是循环导入导致这个错误的根本原因。

 

这个例子和我们第一个例子的区别在于,第一个例子我们是import module,而第二个例子我们是from module import attr,可以认为它除了import module之外,还需要导入属性attr,所以它比第一个例子多了一个查找__dict__的动作。这也就是为什么第二个例子报错,而第一个例子没有报错的原因。

 

我们抛开这个具体的例子来看,循环导入错误的根本原因是:模块的属性在还没有被解析到__dict__之前就被引用了。所以,我们要尽量避免这种情况的发生。

 

通常我们有几种方法来解决这个问题:

  1. 从代码组织架构层面,尽量避免出现模块的循环导入。这是解决问题的最高级的方法,也是我们代码架构的一个重要原则。
  2. 在真正需要使用的时候才导入模块,这样可以把导入这个动作尽量滞后,从而让属性得到解析。
  3. 不要用from module import attr,而用import module。它的原理和第2点其实本质上是一样的。

 

对于上面的第2点解决方案,我们可以看下面的例子:

# a.py

from b import y
x = 100
def func_a():
    print(f"this is func_a, y is {y}")
    pass



# b.py

from a import x
y = 200
def func_b():
    print(f"this is func_b, x is {x}")
    pass



# file: ./10/10_4.py
# 模块间循环导入
import a
a.func_a()

根据我们前面的分析,这样写会报错,因为找不到属性x。

X和y两个属性其实都是在函数func_a和func_b里面使用的,所以我们可以把import语句放到函数里面去执行。

# a.py

# from b import y
x = 100
def func_a():

from b import y
    print(f"this is func_a, y is {y}")
    pass



# b.py

# from a import x
y = 200
def func_b():

from a import x
    print(f"this is func_b, x is {x}")
    pass



# file: ./10/10_4.py
# 模块间循环导入
import a
a.func_a()

这样就可以正常运行。因为module b是在我们真正调用func_a时才被加载的,这就达到了所谓的延迟加载效果。我们可以通过下面代码测试一下:

import a

from sys import modules
print(f"module a is loaded? {'a' in modules}")
print(f"module b is loaded? {'b' in modules}")
a.func_a()
print(f"module a is loaded? {'a' in modules}")
print(f"module b is loaded? {'b' in modules}")

输出为:

module a is loaded? True

module b is loaded? False

this is func_a, y is 200

module a is loaded? True

module b is loaded? True

可以看到,在a.func_a()之前,module b是没有被加载的。

 

循环导入容易出错,而且不好理解影响代码可读性。所以大家在模块设计阶段应该尽可能避免出现循环导入的情况。如果实在无法避免,那么要保证属性在解析后才会被引用。

 

 

3、构造包

前面我们讲了,python的包其实对应的就是文件系统里面的文件夹。但是,文件夹不全是python包。要形成python包,还必须满足一个条件,就是这个文件夹里面必须包含一个__init__.py文件,这个文件可以为空,但是不能没有。

前面的例子中,我们的目录结构如下:

pyfruits/                     dir

      fruits.py               module

这里面没有包含__init__.py,所以它没有形成python包。

 

我们在里面新建一个空的__init__.py,让它成为一个包。

pyfruits/                     dir

      __init__.py             Initialize the package

      fruits.py               module

 

当然,__init__.py可以不为空,那么这个文件到底有什么作用呢?

这个文件可以设置一个很重要的变量__all__,它是一个列表结构。大家还记得import模块的时候,我们可以使用通配符*来导入该模块的所有符号吗?这个__all__就是用来指定可以导入的符号列表的。

同样,在导入包的时候,我们也可以通过通配符*来导入该包下面的所有模块。在__init__.py里面设置__all__变量,可以指定可以导入的模块列表。

比如,我们看python的一个标准库email,它的__init__.py文件中设置了__all__,如下:

__all__ = [

    'base64mime',
    'charset',
    'encoders',
    'errors',
    'feedparser',
    'generator',
    'header',
    'iterators',
    'message',
    'message_from_file',
    'message_from_binary_file',
    'message_from_string',
    'message_from_bytes',
    'mime',
    'parser',
    'quoprimime',
    'utils',
    ]

当我们不设置__all__时,系统只会确保__init__.py里面的代码被执行,如果这个文件中定义了变量或者函数之类的符号,也可以被导入。但是这个包里面的模块不会被导入。

__init__.py里面也可以写功能代码,但是通常我们不会这样做,功能代码我们会放在单独的功能模块里面实现。我们要尽量保证这个文件是轻量级的。

除了__all__以为,你可以在这个文件里面定义一些诸如版本号、作者等变量信息,也可以做一些简单的API封装。

 

__all__也可以直接写在.py模块的头部,用于指定该模块中哪些符号可以被导入。但是它的默认导入行为和包是不一致的,模块不设置__all__,默认全部导入。

 

4、Import导入包

所有的编程语言,几乎都使用一个点号(.)来表达层次关系。Package本质上是将模块组织成了一种层次化的结构。所以,我们采用点号来描述package和module之间的这种层次关系。

类似于模块的导入,如果我们要从包中导入fruits模块,也有三种方式。

 

方式一、

import pyfruits.fruits

print(pyfruits.fruits.fruits)

这种直接导入的方式,在调用时需要写完整的从包到模块的路径。显得很冗长,如果有多层包结构,这个调用名字会更长,所以我们也可以给它取一个别名。

import pyfruits.fruits as f

print(f.fruits)

这样的代码就会显得清爽很多了。

 

方式二、

from pyfruits import fruits

print(fruits.get_fruits())

当然你也可以from到模块那一层,然后import模块里面的一个符号。这个和模块的导入是一模一样的。

from pyfruits.fruits import get_fruits

print(get_fruits())

 

方式三、

from pyfruits import *

print(fruits.fruits)

 

注意我们需要将fruits模块添加到__init__.py中:

#  __init__.py

__all__ = ['fruits']

同样,采用通配符*导入所有模块的方式是不推荐的,除非你的程序要使用该包下面的大部分模块。

 

如果我们在__init__.py中定义了符号(变量或者函数等),那么它们可以被视作是包的属性,比如:

#  __init__.py


__version__ = '1.0.0'
__author__ = 'tiger'

def hello():
    return 'hello'



import pyfruits

print(pyfruits.hello())
print(pyfruits.__version__)
print(pyfruits.__author__)

 

python包是可以层层嵌套的,就像文件系统的目录结构一样。不同层级的包定义和导入规则都是一样的,我们就不展开赘述了。

 

5、预编译的模块

我们知道python是一门解释性的语言,它不会像C语言那样,会先编译成一个二进制的可执行文件。但是,这并不意味着python就没有编译过程。

Python代码的运行也会先进行编译,只是它生成的是一个叫字节码的文件.pyc,这个文件里面存的不是机器码,所以计算机是无法直接运行的。这个文件需要通过解释器逐行解释并运行。

当我们import一个模块的时候,会先将这个模块进行编译生成.pyc文件,然后加载。为了提高这个加载过程的效率,这些编译过的.pyc文件会被缓存在__pycache__目录下面。当这个模块再次被加载,就直接加载.pyc文件,这样就提升了模块加载的性能。

 

缓存的.pyc文件以这样的方式命名:模块名.解释器-版本号.pyc。

系统会自动检查是否需要重新编译模块,它会以.py文件的更新时间作为判断依据。

需要注意的是,.pyc的缓存机制,只会提升模块加载阶段的性能,对运行性能没有任何影响。

 

.pyc是一种字节码文件,这些字节码只有python解释器自己能理解。它是操作系统无关的,也就是说我们在windows上面编译的.pyc可以拿到linux上面去运行。这有点类似于JAVA的.class文件。所以,我们把.pyc的运行环境也可以称作python虚拟机PVM,类似于JAVA虚拟机JVM。

 

在实际的项目发布时,如果我们不想让自己的源代码暴露给用户,也可以预编译出这些模块的字节码文件,将它们提供给用户。我们可以手动生成这些.pyc文件,一种方法是在cmd命令行中带-m参数:

python -m fruits.py

 

也可以通过py_compile模块来编译。

#  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python

# file: ./10/10_2.py
# 批量编译模块
import py_compile
py_compile.compile('./pyfruits/fruits.py')

 

关于项目发布的话题,我们会在整个教程的最后给大家讲解。


本文有视频讲解,视频和实例源码下载方式:点击->我的主页,查看个人简介。

我尽量坚持每日更新一节。

更多python教程,请查看我的专栏《0基础学python视频教程》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值