{{ selected_gold.title }}

  • ¥ {{ selected_gold.original_price }}
  • ¥ {{ selected_gold.price }}
  • {{ selected_gold.number_of_order }} 人订阅
  • {{ selected_gold.total_likers_count }}
    由 {{ selected_gold.creator_name }} 编辑

    {{ title }}

    请登录购买({{ selected_gold.price }}元),即可解锁剩余教程
    点击购买

    • Air
    • 2018年9月20日

    Python中的模块(Module)和包(Package)

    当你编写Python程序时,你经常会引入别人编写好的模块。有的模块是Python自带的,你无需安装就能直接引用,而有的模块则是由Python生态系统里的第三方工程师提供的,你需要通过pip安装之后,才能进行使用。由于在编写Python程序的时候会经常引用别人编写好的模块,因此你应该花一些时间来了解与之有关的知识点。为了让你对Python中的模块有一个清晰的认识,这篇文章将从以下几步来展开:

    1. 如何引入模块
    2. 理解模块(Module)
    3. 包(Package)

    如何引入模块

    首先,我们来看一看,如何引入Python内置的模块。新建文件start.py,内容如下所示:

    import os
    

    以上指令引入了Python内置模块os,引入它之后,我们便可以使用这个模块内部的函数,比如你可以调用它提供的函数cpu_count来获取CPU的核数:

    import os
    
    print(os.cpu_count())
    

    在命令行中执行以下命令,得到的结果如下所示:

    % python3 start.py
    4
    

    接下来,我们看看如何引入第三方提供的模块,这里,我们使用requests作为例子,关于它的使用说明,你可以到它的官网查阅。由于你使用的不是Python内置的模块,因此,你首先需要执行以下命令来安装requests

    pip install requests
    

    安装之后,在start.py中添加以下指令,:

    import requests
    

    该指令的作用是导入requests模块,接下来,你便可以使用该模块所提供的功能。比如,我们可以使用其中的get函数来获取digolds.cn的网页内容,具体的指令如下所示:

    r = requests.get('https://digolds.cn')
    print(r.text)
    

    除此之外,你还可以通过from module import submodule来引入模块中的某个函数,比如下面的代码和上面的代码是等价的:

    from requests import get
    r = get('https://digolds.cn')
    print(r.text)
    

    或者,你也可以通过import module.submodule.submodule来引入模块中的某个函数,如下所示:

    import requests.get
    r = requests.get('https://digolds.cn')
    print(r.text)
    

    这里的submodule有可能是另外一个模块,也可能是当前模块中的某一个常量、函数或者类等。

    理解模块(Module)

    我们已经学习了如何在Python中引入模块,接下来,让我们花一点时间来理解一下模块相关的知识点。

    首先,你需要知道一个Python文件就是一个模块(但是,需要注意的是,除了Python文件,目录或C语言编译出来的可执行文件,也可以作为Python的模块,这里,为了方便讨论,我们假设一个模块对应一个Python文件),像之前创建的start.py,它就是一个模块。假设,你创建了另外一个文件abc.py,那么你可以在该文件里通过以下指令来引入start模块:

    import start
    

    你可以执行以下指令来把之前引入的内置模块os所对应的文件打印出来:

    import os
    
    print(os.__file__)
    

    输出结果如下所示:

    % python3 start.py
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/os.py
    

    可见,我们导入os的内容是来自以上文件os.py。如果你的操作系统是遵守Posix标准的,那么,打开以上文件后,你会看到以下指令:

    import posix
    __all__.extend(_get_exports_list(posix))
    

    以上指令会把函数cpu_count添加进__all__中,而变量__all__是一个字符串数组,用于指定要把当前模块中的哪些接口、常量、类等暴露出去。你可以运行以下指令来打印出模块os都提供了哪些功能:

    import os
    
    print(os.__all__)
    

    得到的输出结果如下所示:

    ['altsep', 'curdir', 'pardir', 'sep', 'pathsep', 'linesep', 'defpath', 'name', 'path', 'devnull', 'SEEK_SET', 'SEEK_CUR', 'SEEK_END', 'fsencode', 'fsdecode', 'get_exec_path', 'fdopen', 'popen', 'extsep', '_exit', 'CLD_CONTINUED', 'CLD_DUMPED', 'CLD_EXITED', 'CLD_TRAPPED', 'DirEntry', 'EX_CANTCREAT', 'EX_CONFIG', 'EX_DATAERR', 'EX_IOERR', 'EX_NOHOST', 'EX_NOINPUT', 'EX_NOPERM', 'EX_NOUSER', 'EX_OK', 'EX_OSERR', 'EX_OSFILE', 'EX_PROTOCOL', 'EX_SOFTWARE', 'EX_TEMPFAIL', 'EX_UNAVAILABLE', 'EX_USAGE', 'F_LOCK', 'F_OK', 'F_TEST', 'F_TLOCK', 'F_ULOCK', 'NGROUPS_MAX', 'O_ACCMODE', 'O_APPEND', 'O_ASYNC', 'O_CLOEXEC', 'O_CREAT', 'O_DIRECTORY', 'O_DSYNC', 'O_EXCL', 'O_EXLOCK', 'O_NDELAY', 'O_NOCTTY', 'O_NOFOLLOW', 'O_NONBLOCK', 'O_RDONLY', 'O_RDWR', 'O_SHLOCK', 'O_SYNC', 'O_TRUNC', 'O_WRONLY', 'POSIX_SPAWN_CLOSE', 'POSIX_SPAWN_DUP2', 'POSIX_SPAWN_OPEN', 'PRIO_PGRP', 'PRIO_PROCESS', 'PRIO_USER', 'P_ALL', 'P_PGID', 'P_PID', 'RTLD_GLOBAL', 'RTLD_LAZY', 'RTLD_LOCAL', 'RTLD_NODELETE', 'RTLD_NOLOAD', 'RTLD_NOW', 'R_OK', 'SCHED_FIFO', 'SCHED_OTHER', 'SCHED_RR', 'ST_NOSUID', 'ST_RDONLY', 'TMP_MAX', 'WCONTINUED', 'WCOREDUMP', 'WEXITED', 'WEXITSTATUS', 'WIFCONTINUED', 'WIFEXITED', 'WIFSIGNALED', 'WIFSTOPPED', 'WNOHANG', 'WNOWAIT', 'WSTOPPED', 'WSTOPSIG', 'WTERMSIG', 'WUNTRACED', 'W_OK', 'X_OK', 'abort', 'access', 'chdir', 'chflags', 'chmod', 'chown', 'chroot', 'close', 'closerange', 'confstr', 'confstr_names', 'cpu_count', 'ctermid', 'device_encoding', 'dup', 'dup2', 'environ', 'error', 'execv', 'execve', 'fchdir', 'fchmod', 'fchown', 'fork', 'forkpty', 'fpathconf', 'fspath', 'fstat', 'fstatvfs', 'fsync', 'ftruncate', 'get_blocking', 'get_inheritable', 'get_terminal_size', 'getcwd', 'getcwdb', 'getegid', 'geteuid', 'getgid', 'getgrouplist', 'getgroups', 'getloadavg', 'getlogin', 'getpgid', 'getpgrp', 'getpid', 'getppid', 'getpriority', 'getsid', 'getuid', 'initgroups', 'isatty', 'kill', 'killpg', 'lchflags', 'lchmod', 'lchown', 'link', 'listdir', 'lockf', 'lseek', 'lstat', 'major', 'makedev', 'minor', 'mkdir', 'mkfifo', 'mknod', 'nice', 'open', 'openpty', 'pathconf', 'pathconf_names', 'pipe', 'posix_spawn', 'posix_spawnp', 'pread', 'putenv', 'pwrite', 'read', 'readlink', 'readv', 'register_at_fork', 'remove', 'rename', 'replace', 'rmdir', 'scandir', 'sched_get_priority_max', 'sched_get_priority_min', 'sched_yield', 'sendfile', 'set_blocking', 'set_inheritable', 'setegid', 'seteuid', 'setgid', 'setgroups', 'setpgid', 'setpgrp', 'setpriority', 'setregid', 'setreuid', 'setsid', 'setuid', 'stat', 'stat_result', 'statvfs', 'statvfs_result', 'strerror', 'symlink', 'sync', 'sysconf', 'sysconf_names', 'system', 'tcgetpgrp', 'tcsetpgrp', 'terminal_size', 'times', 'times_result', 'truncate', 'ttyname', 'umask', 'uname', 'uname_result', 'unlink', 'unsetenv', 'urandom', 'utime', 'wait', 'wait3', 'wait4', 'waitpid', 'write', 'writev', 'makedirs', 'removedirs', 'renames', 'walk', 'execl', 'execle', 'execlp', 'execlpe', 'execvp', 'execvpe', 'getenv', 'supports_bytes_environ', 'environb', 'getenvb', 'P_WAIT', 'P_NOWAIT', 'P_NOWAITO', 'spawnv', 'spawnve', 'spawnvp', 'spawnvpe', 'spawnl', 'spawnle', 'spawnlp', 'spawnlpe']
    

    接下来,让我们看看,Python遇到import都做了哪些事情。当Python遇到import os指令时,它主要做了以下2件事情:

    【1】sys.modules中查找os是否已经导入过,如果导入过,那么将直接把该模块对象返回,并且将该对象赋给当前Python文件中的变量os也就是说在之前的文件start.py中,其内容如下所示:

    import os
    
    print(os.cpu_count())
    

    当Python执行import os完毕,此时会得到一个变量os,该变量的类型是<class 'module'>,你可以通过以下指令来获取任何一个变量的类型:

    type(os)
    

    通过该对象,你可以获取到里面提供的函数,比如cpu_count。另外,你要注意的是sys.modulesdict对象,通过以下指令,你就可以根据模块名为os所对应的sys.modules对象:

    import sys
    
    sys.modules['os']
    

    输出对应的sys.modules对象为以下结果:

    <module 'os' from '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/os.py'>
    

    如果无法在sys.modules中查找到正在导入的模块对象,那么需要执行步骤【2】。

    【2】 这一步,主要执行了Python提供的函数__import__来完成查找和加载正在导入的模块。具体的过程可以参考这篇文章中【The import process】处的内容。在这里,我将重点介绍引入第三方模块时(比如import requests),这一步会用到哪些信息。首先,执行以下指令以便得到requests.py所对应的模块文件:

    import requests
    
    print(requests.__file__)
    

    执行之后,会得到以下结果:

    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/requests/__init__.py
    

    也就是说,当Python执行import requests的时候,它会到以下目录去查找requests模块:

    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
    

    此时,它在这个目录里找到了目录requests,因此,它会把目录requests下的init.py文件与模块requests对应起来,并且读取该文件中的代码以及执行其中的代码,最终得到了一个requests对象。

    这里存在一个问题:它是怎么知道要从以下路径来搜索requests模块的?

    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
    

    它之所以知道从以上路径来搜索requests模块,是因为它会遍历sys.path中所有路径,并在每一个路径下查找是否有requests模块,而在这些路径中,恰巧有一项就是上面的路径。你可以输入以下指令来打印sys.path

    import sys
    
    print(sys.path)
    

    执行结束后,将输出以下内容(其中有一项,包含了以上路径):

    ['/Users/slz/dev/src/digolds_sample', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python38.zip', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload', '/Users/slz/Library/Python/3.8/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages']
    

    这就是为什么你之前通过pip install requests安装模块requests后,就可以在Python文件中直接使用的原因:模块requests会安装到以下目录,引入该模块时,会从该目录下查找。其它第三方模块也遵守相同的规则。

    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
    

    最后,让我们来学习如何引入自定义的模块。假设,你编写了2个文件module_a.pymodule_b.py,后者定义了一个函数add,而前者使用了该函数。它们所在的目录结构如下所示:

    % pwd
    /Users/slz/dev/src/digolds_sample/packages
    
    % find . -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
    .
    |____package_B
    | |____module_b.py
    |____package_A
    | |____module_a.py
    

    这2个文件的内容分别如下所示:

    # module_a.py
    
    import package_B.module_b
    
    print(package_B.module_b.add(1, 2))
    
    # module_b.py
    
    def add(a, b):
        return a + b
    

    假设,你的命令行的当前目录如下所示:

    % pwd
    /Users/slz/dev/src/digolds_sample/packages
    

    你在命令行里执行指令python3 -m package_A.module_a,那么,你将会得到以下结果:

    % python3 -m package_A.module_a
    3
    

    但是,当你执行指令python3 package_A/module_a.py时,得到的结果如下所示:

    % python3 package_A/module_a.py                         
    Traceback (most recent call last):
      File "package_A/module_a.py", line 1, in <module>
        import package_B.module_b
    ModuleNotFoundError: No module named 'package_B'
    

    指令python3 -m package_A.module_apython3 package_A/module_a.py都执行了相同的代码,那么为什么前者执行成功了,而后者确执行失败了?原因出在sys.path上,让我们把以下代码添加到module_a.py上,如下所示:

    import sys
    print('\n'.join(sys.path))
    
    import package_B.module_b
    print(package_B.module_b.add(1, 2))
    

    执行指令python3 -m package_A.module_a,你将看到以下结果:

    % python3 -m package_A.module_a
    /Users/slz/dev/src/digolds_sample/packages
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python38.zip
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload
    /Users/slz/Library/Python/3.8/lib/python/site-packages
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
    3
    

    此时,你会发现sys.path包含了当前目录,也就是说,当Python执行指令import package_B.module_b时,它能够从sys.path中读到当前目录,并能够成功找到模块module_b。接下来,让我们执行另外一句指令python3 package_A/module_a.py,得到的结果如下所示:

    python3 package_A/module_a.py
    /Users/slz/dev/src/digolds_sample/packages/package_A
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python38.zip
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload
    /Users/slz/Library/Python/3.8/lib/python/site-packages
    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
    Traceback (most recent call last):
      File "package_A/module_a.py", line 4, in <module>
        import package_B.module_b
    ModuleNotFoundError: No module named 'package_B'
    

    通过以上的输出,你会发现sys.path包含了以下目录:

    /Users/slz/dev/src/digolds_sample/packages/package_A
    

    但是在该目录下,并没有package_B,因此Python会报出无法找到模块的信息。

    包(Package)

    Python中的包(Package)是由一系列模块组成,它不提供任何功能,而是类似于一个文件夹,你可以把多个模块放到这个文件夹里,形成一个整体。

    包(Package)的用途有2点:

    1. 它能隔离你研发的模块,比如,你研发了一个模块叫sys.py,其他人也研发了一个相同的模块,那么如果你把该模块放到包(Package)里,别人要调用你的模块时,就必须先指定包(Package)
    2. 它能把你研发的所有模块集合在一起,方便发布到网上,供其他人使用

    之前,你通过pip install requests来安装的第三方库,实质上就是一个包,为了查看这个包安装在哪个目录,你可以执行以下命令:

    import requests
    
    print(requests.__file__)
    

    执行结果如下所示:

    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/requests/__init__.py
    

    通过以上输出结果,可以查看文件__init__.py所在的目录都包含了哪些文件,以下是我打印出来的内容:

    __init__.py
    __version__.py
    _internal_utils.py
    adapters.py
    api.py
    auth.py
    certs.py
    compat.py
    cookies.py
    exceptions.py
    help.py
    hooks.py
    models.py
    packages.py
    sessions.py
    status_codes.py
    structures.py
    utils.py
    

    通过以上输出结果可知,pip install requests实质上是把包requests安装到了以下目录:

    /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/requests
    

    而该目录里包含了许多.py文件,每个文件就是一个模块,可见requests包实际上是由许多模块组成。在这些文件中,有一些特殊的.py文件,比如__init__.py,这个文件是有特殊意义的,接下来,让我们来学习它的作用。

    当你在一个目录里创建__init__.py文件,那么,从概念上来讲,这个目录就是一个包。当你执行命令import requests时,实际上会执行__init__.py里的代码,因此,你可以在这个文件里初始化该包里的模块。比如,你打开__init__.py,就会看到以下内容:

    from .api import request, get, head, post, patch, put, delete, options
    

    也就是说,我们之前用的get函数,是来自模块api.py。当Python执行import requests时,get函数会在__init__.py初始化,此时,你便可以通过requests.get来使用该函数。当然,你也可以自己导入api模块,然后通过该模块来使用get函数,如下所示:

    from requests import api
    
    api.get('https://digolds.cn')
    

    但是,我们一般会直接通过包,把所有希望提供出去的模块,在文件__init__.py中定义和初始化。

    参考

    1. How to upload packages to Python Package Index
    2. Hitchhiker's guide to the Python imports
    3. Python3 import and project layout
    4. Python behind the scenes #11: how the Python import system works