Python基础教程5--模块和包

本文主要介绍Python模块和包相关知识,通过模块和包,我们就可以将多个脚本文件组织在一起使用。

模块

什么是模块

一个模块就是一个文件,一个脚本就是一个模块。

Python自带了一组标准模块(math/sys/os/...),可以直接使用,另外我们可以编写自已的模块。

在模块中可以定义变量、函数、类型等内容,不需要特别的export,模块中的内容都可以在其它模块中通过import导入使用。

模块被导入时,模块中的代码会被执行。

以下是一个简单模块示例

                
                    #hello.py
                    """
                    This is the module document.
                    This is the doc of the hello module.
                    For testing only
                    """
                    print('py module')          # 当hello模块被import时会执行
                    name = 'hello'

                    def say_hello():
                        """
                        This is the method document.
                        The method is used to print "Hello World!!!"
                        :return:
                        """
                        print('Hello World!!!')

                    def say_bye():
                        print('Bye!!!')
                
            

在其它地方,通过import导入,即可使用

                
                    >>> import hello            # 导入
                    py module                   # 导入模块时,其中的代码被执行了
                    >>> hello.name              # 模块中的变量
                    hello
                    >>> hello.say_hello()       # 调用模块中的函数
                    Hello World!!!
                
            

模块的导入

模块有如下导入方式

  • 模块作为一个对象导入import somemodule

    导入以后通过模块名称(module_name.)来使用模块中的变量、函数等。

                            
                                >>> import hello            # 导入
                                py module                   # 导入模块时,其中的代码被执行了
                                >>> hello.name              # 模块中的变量
                                hello
                                >>> hello.say_hello()       # 调用模块中的函数
                                Hello World!!!
                            
                        

    如果从一个模块中导入的函数较多,强烈建议将模块作为一个整体对象导入,一方面省去了导入多个函数的麻烦,另外一方面意义也更加清晰

                            
                                import rediz # 不用导入n多的函数
                                # from rediz import init_redis, set_add, list_push, set_union, set_members, get_names,\
                                # delete, set_rem, set_card, set_ismember, set_pop, set_move, set_diff, \
                                # set_len, set_scan_iter
    
                                rediz.init_redis('localhost') # 意义更加清晰
                                rediz.set_add('test:letters', 'a', 'b', 'c', 'd', 'e')
                                #...
                            
                        
  • 导入模块中的指定内容from somemodule import fun_a, var_b, cls_c

    变量/函数导入以后,可以直接使用(就像使用当前模块的方法一样),不需要再通过模块名称来引用。

    如果要一次导入一个模块中的多个变量/函数,可以通过逗号(,)隔开。

                            
                                >>> from hello import say_hello, say_bye    # 导入hello模块中的say_hello和say_bye函数,
                                py module                                   # 导入模块时,其中的代码被执行了
                                >>> say_hello()                             # 调用say_hello函数
                                Hello World!!!
                                >>> say_bye()                               # 调用say_bye函数
                                Bye!!!
                            
                        
  • 导入模块中的所有内容from somemodule import *

    和导入指定内容类似,导入以后,可以直接使用模块中的函数/变量/类,而不需要再通过模块名称引用。如果模块指定了__all__,导入的就是__all__中指定的内容。

    除非明确知道想要从给定的模块导入所有功能时,才应该使用,否则不建议使用

                            
                                >>> from hello import *         # 导入hello模块中的所有内容name/say_hello/say_bye,
                                py module                       # 导入模块时,其中的代码被执行了
                                >>> say_hello()                 # 调用say_hello函数
                                Hello World!!!
                                >>> say_bye()                   # 调用say_bye函数
                                Bye!!!
                            
                        
  • 重命名导入(别名)

    可以对导入的模块,或者导入的指定内容通过as进行重命名。

    • 对模块重命名import somemodule as module_a
                                      
                                          >>> import hello as my_hello    # 导入hello并重命名为my_hello
                                          py module
                                          >>> my_hello.name               # 通过my_hello使用模块中的变量
                                          hello
                                          >>> my_hello.say_hello()        # 通过my_hello调用模块中的函数
                                          Hello World!!!
                                      
                                  
    • 对导入的指定内容重命名from somemodule import function_a as fa, function_b as fb
                                      
                                          >>> from hello import say_hello as hello,say_bye as bye     # 导入say_hello并重命名为hello,say_bye重命名为bye
                                          py module
                                          >>> hello()                                                 # 通过hello调用say_hello函数
                                          Hello World!!!
                                          >>> bye()                                                   # 通过bye调用say_bye函数
                                          Bye!!!
                                      
                                  
  • 重名问题

    如果两个模块具有同名函数,则通过指定函数名导入时会有问题,例如以下代码

                            
                                >>> from hello import say_hello
                                >>> from new_hello import say_hello   # 第二次导入的new_hello模块中say_hello会覆盖第一次导入的hello模块中的say_hello
                            
                        

    如果出现这种情况,有两种解决方法,第一种可以通过将模块作为一个对象导入,然后通过对象调用

                            
                                >>> import hello
                                >>> import new_hello
                                >>> hello.say_hello()           # 调用hello模块中的say_hello
                                Hello World!!!
                                >>> new_hello.say_hello()       # 调用new_hello模块中的say_hello
                                Hello World!!! -New
                            
                        

    也可以通过重命名指定导入内容的方法来解决

                            
                                >>> from hello import say_hello as hello            # 导入hello模块中的say_hello并将其命名为hello
                                >>> from new_hello import say_hello as new_hello    # 导入new_hello模块中的say_hello并将其命名为new_hello
                                >>> hello()                  # 调用hello模块中的say_hello
                                Hello World!!!
                                >>> new_hello()              # 调用new_hello模块中的say_hello
                                Hello World!!! -New
                            
                        

只导入/运行一次

只导入/运行一次有两个场景

  • 只导入一次: 在某个模块中,导入两次同一模块,当再次导入时什么也不会发生,被导入模块中的代码也不会执行。
  • 只运行一次: 一个程序可能有很多模块组成,在程序的执行过程中,多个模块会引入相同的模块,例如

    现在有main主程序,导入了module_a/module_b共同完成一项任务,而module_a/module_b都用到了module_i,并将其导入,所以在main主程序执行时,module_i总共被导入了2次, 但是无论被导入多少次,在程序的一次执行过程中,同一个模块中的代码只会被执行一次

.pyc文件

导入模块的时候,在目录中会看到.pyc后缀的文件,可能会在自动生成的__pycache__目录中(__pycache__/hello.cpython-37.pyc)。

这个文件是编译后的平台无关的文件,已经转换成Python能够更加有效处理的文件。

如果稍后导入同一个模块,Python会使用.pyc文件而不是.py文件,除非.py文件发生改变,这种情况下,会生成新的.pyc文件。

只要.py文件存在,删除.pyc文件不会对程序产生影响,必要的时候系统会自动创建新的.pyc文件。

模块测试

模块被用来定义函数,类和其它一些内容,有时候我们会在模块中加一些测试代码来检查模块是否可以正常工作,比如

                
                    #hello.py
                    print('py module')
                    name = 'hello'

                    def say_hello():
                        print('Hello World!!!')

                    def say_bye():
                        print('Bye!!!')

                    def test():         # 测试函数
                        say_hello()
                        say_bye()
                    test()              # 调用测试函数
                
            

如果我们直接在模块中执行测试相关代码,那么这个模块被其它模块导入时,测试代码都会执行(上例中的test函数),而这并不是我们希望看到的。

我们希望只有在当前模块作为主程序运行时才运行测试代码,而不是模块被导入到其它模块时也执行。

为了实现这一点,我们可以通过模块的__name__内置变量进行判断,如果模块是主程序(程序入口),变量__name__的值是"__main__", 而在导入的模块中,这个值被设定为模块的名字(上例中为:"hello")。

通过__name__变量的这一特性,我们可以添加一个if判断,将测试代码放置在if条件语句块中来让测试代码更好的工作,这样只有模块作为主程序运行时,测试代码才会运行

                
                    #hello.py
                    #test()                     # 不直接调用测试代码
                    if __name__ == '__main_':   # 将测试代码放到if判断语句中,只有当前模块是主程序入口时,test()才会运行
                        test()
                
            

模块探究

可以通过以下方法查看模块相关信息

  • __all__变量,查看模块通过__all__变量定义的公共接口列表
                            
                                >>> import copy
                                >>> copy.__all__
                                ['Error', 'copy', 'deepcopy']
                            
                        

    __all__是在copy模块内部设置的,用于定义模块的公共接口,它告诉解释器,从模块导入所有名字代表什么含义,以下代码来自copy.py源码

                            
                                #copy.py
                                __all__ = ["Error", "copy", "deepcopy"]
                            
                        

    如果 __all__ 包含未定义的名字, 在导入时引起AttributeError。

    通过from copy import *导入copy模块,就能直接使用copy模块中定义的函数和类。

    在编写模块时,设置__all__是非常有用的,可以过滤掉模块中一大堆其它程序不需要的变量、函数和类,如果没有设置__all__,通过import *语句会导入模块中所有不以下划线开头的全局名称。

  • __doc__变量,查看模块或模块中函数的相关文档
                            
                                >>> import hello
                                >>> hello.__doc__
                                '\nThis is the module document.\nThis is the doc of the hello module.\nFor testing only\n'
                                >>> hello.say_hello.__doc__
                                '\n    This is the method document.\n    The method is used to print "Hello World!!!"\n    :return:\n    '
                            
                        

    为代码编写文档是个好习惯

  • __file__变量,查看模块源代码的位置

    如果文件名以.pyc结尾,只要查看对应的.py的文件即可。

                            
                                >>> copy.__file__
                                'C:\\SDK\\Python\\lib\\copy.py'
                            
                        

    编写代码文档是好习惯

  • help函数,获取模块或函数相关信息

    help函数与__doc__变量类似,不过能获取更多信息

                            
                                >>> help(hello)
                                Help on module hello:
                                NAME
                                    hello
                                DESCRIPTION
                                    This is the module document.
                                    This is the doc of the hello module.
                                    For testing only
                                FUNCTIONS
                                    say_bye()
    
                                    say_hello()
                                        This is the method document.
                                        The method is used to print "Hello World!!!"
                                        :return:
    
                                    test()
                                DATA
                                    name = 'hello'
                                FILE
                                    d:\py-study\basic\hello.py
    
                            
                        
  • dir函数,查看模块的所有特性
                            
                                >>> import copy
                                >>> dir(copy)
                                ['Error', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__',
                                '_copy_dispatch', '_copy_immutable', '_deepcopy_atomic', '_deepcopy_dict', '_deepcopy_dispatch', '_deepcopy_list',
                                '_deepcopy_method', '_deepcopy_tuple', '_keep_alive', '_reconstruct', 'copy', 'deepcopy', 'dispatch_table', 'error']
                            
                        

标准库

Python中包含了一些标准库模块,可以方便使用,提高我们的编码效率。

  • sys: 通过该模块可以访问和Python解释器联系紧密的变量和函数,比如命令行参数,查找模块所在目录等。
  • os: 通过该模块可以访问到和操作系统联系紧密的变量和函数,比如环境变量,路径分隔符等。
  • fileinput: 通过该模块可以轻松遍历多个文件和流中的所有行数据
  • re: 正则表达式模块。
  • time: 通过该模块可以获取当前时间,并可进行时间日期操作和格式化。
  • random: 通过该模块和产生随机数。

更多模块信息,可以参考Python标准类库

包package

模块是单个脚本文件,为了更好的组织模块,可以将模块分组为包,包就是模块文件所在的目录。包之间可以进行相关嵌套。

包结构

为了让Python将目录作为包对待,目录下必须包含一个名为__init__.py的文件

一般情况下,__init__.py空着就好,如果包含代码,那将包作为普通模块导入时,__init__.py文件的内容就是模块的内容。

比如有个constants的包,文件constants/__init__.py包含变量PI=3.14,可以通过包名直接使用

                
                    >>> import constants
                    >>> constants.PI
                    3.14
                
            

我们再看一个示例,有如下目录结构的包

文件/目录 描述
~/python PYTHONPATH中的目录
~/python/drawing drawing包目录
~/python/drawing/__init__.py drawing包代码(作为模块使用)
~/python/drawing/colors.py drawing包中的colors模块
~/python/drawing/shapes.py drawing包中的shapes模块

当导入时

                
                    import drawing                  # 导入drawing包,__init__模块中的内容可用,但colors和shapes模块不可用

                    import drawing.colors           # colors模块可用,但只能通过全名drawing.colors来使用

                    from drawing import shapes      # shapes模块可用,可以通过shapes名来使用
                
            

运行包目录

包中可以包含一个__main__.py的文件,如果有__main__.py文件,可以直接通过python命令加包名的方式运行。

                
                    #drawing/__main__.py
                    print("package running")
                
            

解释器会将执行__main__.py文件作为主程序

                
                    >>> python drawing
                    'package running'
                
            

使用相对路径导入

可以使用相对路径的方式导入当前或其它包中的内容,例如有如下包结构

                
                     mypackage/
                        __init__.py
                        A/
                            __init__.py
                            foo.py
                            bar.py
                        B/
                            __init__.py
                            baz.py
                
            

如果mypackage/A/foo.py要导入同目录下的模块bar,有两种导入方式

                
                    from mypackage.A import bar     # 包含顶层包名的绝对路径

                    from . import bar               # 使用相对路径
                
            

如果mypackage/A/foo.py要导入不同目录下的模块baz,也有两种导入方式

                
                    from mypackage.B import baz     # 包含顶层包名的绝对路径

                    from ..B import baz             # 使用相对路径
                
            

可以看到,使用相对路径导入,没有包含顶层包名,而只是使用了点(.),如果使用过操作系统的命令行,应该对点号很熟悉。

使用像mypackage.A的绝对路径,如果想要重命名mypackage包名,可能必须要检查所有文件来修改代码,而相对路径就不会这个问题,所以应该尽量使用相对路径导入。

请注意,相对路径只适用于from import的语法,只有import的导入方式是不适用的。

合并导出

__init__.py可以用于加载子模块的内容,而包作为模块导入时__init__中的内容就是导入的内容。

利用这个特性,可以在__init__.py中将多个子模块或包中的内容合并, 将包中希望对外公开的变量、函数或类统一加载到__init__.py中,然后统一导出。这样就不用再分别导入包下的各个模块了,即可以简化导入操作又可以增加代码重构的灵活度。

                
                    #drawing/__init__.py
                    from .colors import blue
                    from .colors import green
                    from .colors import red

                    from .shapes import *       # 如果和模块的__all__属性联合使用,导出更加方便
                
            

导入时就可以通过from drawing import red来代替from drawing.colors import red。