Python:模块和包

9.1模块化编程

对于一个真实的Python编程,不可能自己完成所有的工作,通常需要借助第三方类库。此外,也不可能在一个源文件中编写整个程序的源代码,这些都需要以模块化的方式来组织项目的源代码。

9.1.1 导入模块的语法

import导入模块的语法,实际上import还有更多详细的用法。import语句主要有两种用法:

  1. import 模块名1[as 别名1],模块名2[as 别名2],…:导入整个模块。
  2. from 模块名 import 成员名1[as 别名1],成员名2[as 别名2],…:导入模块中指定成员。

上面两种import语句的区别主要有三点:

  1. 第一种import语句导入整个模块内的所有成员(包括变量、函数、类等);第二种import语句只导入模块内的指定成员(除非使用from模块名import*,但通常不推荐使用这种语法)。
  2. 但使用第一种import语句导入模块中的成员时,必须添加模块名或模块别名前缀;当使用第二种import语句导入模块中的成员时,无须使用任何前缀,直接使用成员名或成员别名即可。
#导入sys整个模块
import sys
#使用sys模块作为前缀来访问模块中的成员
print(sys.argv[0])# main.py

上面程序代码使用最简单的方式导入了sys模块,因此程序中使用sys模块内的成员时,必须添加模块名作为前缀。

在导入整个模块时可以为模块指定别名。

#导入sys整个模块,并指定别名为s
import sys as s
#使用s模块别名作为前缀来访问模块中的成员
print(s.argv[0])#main.py

上面程序在导入sys模块时指定了别名s,因此在程序中使用sys模块内的成员时,必须添加模块别名s作为前缀。

导入整个模块的语法也可以一次性导入多个模块,多模块之间用逗号隔开。#导入sys、os两个模块
import sys,os
#使用前缀名作为前缀访问模块中的成员
print(sys.argv[0])#main.py
#os模块的seo变量代表平台上的路径分隔
print(os.sep)#\

同时,在导入多个模块时也可以为模块指定别名。

#导入sys、os两个模块,并未sys指定别名s,为os指定别名o

import sys as s,os as o

#使用模块名作为前缀来访问模块中的成员

print(s.argv[0])#main.py

print(o.sep)#\

接下来介绍from…import导入模块内指定成员的用法:

#导入sys模块内的argv成员

from sys import argv

#使用导入成员的语法,直接使用成员名访问

print(argv[0])#main.py

使用from…import导入模块成员时也可以为成员指定别名。

#导入sys模块内的argv成员,并为其指定别名v

from sys import argv as v

#使用导入成员的语法,直接使用成员的别名访问

print(v[0])#main.py

在使用from…import导入模块成员时也可以同时导入多个成员。

#导入sys模块内的argv、winver成员

from sys import argv,winver

#使用导入成员的语法,直接使用成员名访问

print(argv[0])#main.py

print(winver)#3.12

在使用from…import同时导入多个模块成员时也可指定别名,同样使用as关键字为成员指定别名。

#导入sys模块中的argv、winver成员,并未其指定别名v、wv

from sys import argv as v,winver as wv

#使用导入成员(指定别名)的语法,直接使用成员的别名访问

print(v[0])#mian.py

print(wv)#3.6

在使用from…import语法时也可以一次导入指定模块内的所有成员:

#导入sys模块内的所有成员

from sys import *

#使用导入成员的语法,直接使用成员的别名访问

print(argv[0])#main.py

print(winver)#3.6

需要说明的时,一般不推荐使用“from 模块 imort *”这种语法导入指定模块内的所有成员,因为它存在潜在的风险。避免导致在不同的模块中存在相同的方法或属性。

9.1.2 定义模块

模块就是Python程序。任何Python程序都可作为模块导入。前面我们写的所以Python程序都可作为模块导入。换而言之,随便写一个Python程序,其实都可作为模块导入。对于任何程序,只要导入了模块,即可使用该模块内的所有成员。

print('这是module 1')

my_book='Python'

def say_hi(user):

    print("%s您好,欢迎学习Python"%user)

class User:

    def __init__(self,name):

        self.name=name

    def walk(self):

        print("%s正在慢慢地走路"%self.name)

    def __repr__(self):

        return"User[name=%s]"%self.name

使用模块地好处在于:如果将程序使用的程序单元定义在模块中,后面不管哪个程序只要导入该模块,该程序即可使用该模块所包含的程序单元,这样就可以提供很好的复用——导入模块,使用模块,从而避免每个程序都需要重新定义这些程序单元。

9.1.3 为模块编写说明文档

与前面介绍的函数、类相同的是,在实际开发中往往也应该为模块编写说明文档;否则,其他开发者将不知道该模块的作用,以及包含哪些功能。

为模块编写说明文档很简单,只要在模块开始处定义一个字符串直接量即可。

'''

这是我们编写的第一个模块,该模块包含以下内容

my_book:字符串变量

say_hi:简单函数

User:代表用户类

'''

这段字符串内容将会作为该模块的说明文档,可提供模块的__doc__属性来访问文档。

9.1.4 为模块编写测试代码

当模块编写完成之后,可能还需要为模块编写一些测试代码,用于测试模块中的每一个单元是否能都能正常运行。

由于模块其实是一个Python程序,因此完全可以直接使用Python命令来解释和执行模块程序只要模块中包含可执行代码。

#===以下部分是测试代码===

def test_my_book():

    print(my_book)

def test_say_hi():

    say_hi('孙悟空')

def test_User():

    u=User('白骨精')

    u.walk()

    print(u)

注意:对于实现开发的项目,对每个函数、类可能都需要使用更多的测试用例进行测试,这样才能达到各种覆盖效果(比如语句覆盖、条件覆盖等)。

如果只是简单地调用上面的测试程序,则会导致一个问题:当其他程序每次导入该模块时,这三个测试函数都会自动运行,这显然不是期望见到结果。此时希望实现的效果是:直接使用Python命令运行该模块(相当于测试),程序应该执行该模块的测试函数;如果是其他程序导入该模块,程序不应该执行该模块的测试函数。

此时可借助所有模块内置的__name__变量进行区分,如果直接使用python命令来运行一个模块,__name__变量的值是__main__;如果该模块导入其他程序中,__name__变量的值就是模块名。因此,如果希望测试函数只是在使用python命令直接运行时才执行,则可在调用测试函数时增加判断:只有当__name__属性为__main__时才调用测试函数。为模块增加如下代码即可:

#当__name__为'__main__"(直接使用Python运行该模块)执行如下代码:

if __name__=='__main__':

    test_my_book()

    test_say_hi()

    test_User()

这时再次使用该程序命令来运行该代码模块时,可以看到如下输出结果。

"""

这是module 1

Python

孙悟空您好,欢迎学习Python

白骨精正在慢慢地走路

User[name=白骨精]

"""

9.1.5 模块的__all__变量

在默认情况下,如果使用“from模块名import*”这样的语句来导入模块,程序会导入该模块中所有不以下划线开头的程序的单元,这是很容易得到的结果。

有时候模块中虽然包含很多成员,但并不希望每个成员都被暴露出来供外界使用,此时可借助于模块的__all__变量,将变量的值设置成一个列表,只有该列表中的程序单元才会被暴露出来。

'测试__all__的模块'

def hello():

    print("Hello Python")

def world():

    print("Python World is funny")

def test():

    print("--test--")

#定义__all__变量,默认只导入hello和world两个程序单元

__all__=['hello','world']

下面的__all__变量指定该模块默认值被导入hello和world两个程序单元。

from main import *

hello()

world()

test()#会提示找不到test()函数
"""

Hello Python

Python World is funny

Traceback (most recent call last):

  File "", line 4, in <module>

    test()#会提示找不到test()函数

    ^^^^

NameError: name 'test' is not defined

"""

从上面输出结果可以看到,通过“from 模块名 import*”语句确实只能导入__all__变量所列出的全部程序单元,没有列出的test就没有被导入进来。

事实上,__all__变量的意义在于为模块定义一个开放的公共接口。通常来讲,只有__all__变量列出的程序单元,才是希望该模块被外界使用的程序单元。因此,为模块设置__all__变量还是比较有用的。比如一个实际的大模块可以包含了大量其他程序不需要使用的变量、函数、类,那么通过__all__变量即可把它们自动过滤掉,这还是非常酷的。

如果确实希望程序使用模块内__all__列表之外的程序单元,有两种解决方法。

  1. 第一种是使用“import模块名”来导入模块。在通过这种方式导入模块之后,总可以通过模块名前缀(如果为模块指定了别名,则可以使用模块的别名作为前缀)来调用模块内的成员。
  2. 第二种是使用“from 模块名 import 程序单元”来导入指定程序单元。在这种方式下,即使想导入的程序单元没有位于__all__列表中,也依然可以导入。

9.2 使用包

对于一个需要实际应用的模块而言,往往会具有很多程序单元,包括变量、函数和类等,如果将整个模块的所有内容都定义在同一个Python源文件内,这个文件将会变得非常庞大,显然并不利于模块化开发。

9.2.1 什么是包

为了更好地管理多个模块源文件,Python通过了包的概念。

  1. 从物理上看,包就是一个文件夹,在该文件夹下包含了额一个__init__.py文件,该文件可用于包含多个模块源文件。
  2. 从逻辑上看,包的本质依然是模块。

上面介绍可以得出一个推论 :包的作用是包含多个模块,但包的本质依然是模块,因此包也可用于包含包。典型的,当我们为Python安装了numpy模块之后,可以在Python安装目录的Lib\site-packages目录下找到一个numpy文件夹,它就是安装的numpy模块(其实就是一个包)。

9.2.2 定义包

定义包很简单,主要有两步:

  1. 创建一个文件夹,该文件夹的名字就是该包的包名。
  2. 在该文件夹内添加一个__init__.py文件即可。
'''

这是学习包的一个示例

'''

print('this is first_package')

上面的Python源文件非常简单,该文件开始部分的字符串是该包的说明文档,接下来是一条简单的输出语句。

#导入first-package包(模块)

import first_package

print('==================')

print(first_package.__doc__)

print(type(first_package))

print(first_package)

"""

==================



这是学习包的一个示例


<class 'module'>

<module 'first_package' from ''>

"""

与模块类似的是,包被导入之后,会在包目录下生成以一个__pycahce__文件夹,并在该文件夹内为包生成一个__init__.cpython-312.pyc文件.

由于导入包就相当于导入该包下的__init__.py文件,因此我们完全可以在__init__.py文件中定义变量、函数、类等程序单元,但实际上往往不会这么做。包主要作用是包含多个模块,因此__init__.py文件的主要作用就是导入该包内的其他模块。

下面定义一个更复杂的包,在该包下将会包含多个模块,并使用__init__.py文件来加载这些模块。

新建一个fk_package包,并在该包下包含三个模块文件:

  1. print_shape.py
  2. billing.py
  3. arithmetic_chart.py

fk_package的文件结构如下:

fk_package

   |——arithmetic_chart.py

   |——billing.py

   |——print_shape.py

   |——__init__.py

其中,arithmetic_chart.py模块文件的内容如下:

def print_multipe_chart(n):

    '打印乘法口诀表的函数'

    for i in range(n):

        for j in range(i+1):

            print('%d * %d = %2d' % ((j+1),(i+1),(j+1)*(i+1)),end='  ')

        print('')

billing.py模块文件的内容如下:

class Item:

    '定义代表商品的Item类'

    def __init__(self,price):

        self.price =price

    def __repr__(self):

        return 'Item[price=%g]' % self.price

print_chart.py模块文件内容如下:

def print_blank_tringle(n):

    '打印一个由星号组成的空心的三角形'

    if n<=0:

        raise ValueError('n必须大于0')

    for i in range(n):

        print(' '*(n-i-1),end='')

        print('*',end='')

        if i!=n-1:

            print(' '*(2*i-1),end='')

        else:

            print('*'*(2*i-1),end='')

        if i!=0:

            print('*')

        else:

            print('')

fk_package包下的__init__.py文件暂时为空,不用编写任何内容。

9.2.3 导入包内成员

如果需要使用arithmetic_chart、billing和print_shape这三个模块,则可在程序fk_package_test01.py中执行如下代码:

#导入fk_package包,实际上导入包下的__init__.py文件

import fk_package

#导入fk_package包下的print_shape模块

#实际上就是导入fk_package目录下的print_shape.py

import fk_package.print_shape

#导入fk_package包下的billling模块

#实际上就是导入fk_package目录下的billing.py

from fk_package import billing

#导入fk_package包下的arithmetic_chart模块

#实际上就是导入fk_package目录下的arithmetic_chart.py

import fk_package.arithmetic_chart



fk_package.print_shape.print_blank_tringle(5)

im=billing.Item(4)

print(im)

fk_package.arithmetic_chart.print_multipe_chart(5)

上面程序虽然可以正常运行,但此时存在两个问题:

  1. 为了调用包内模块中的程序单元,需要使用很长的前缀,这实在是太麻烦了。
  2. 包内__init__.py文件的功能完全被忽略了。

将fk_package包下的__init__.py文件编辑成如下形式:

#从当前包中导入print_shape模块

from fk_package import print_shape

#从.print_shape中导入所有程序单元到fk_package中

from fk_package.print_shape import*

#从当前包中导入billing模块

from fk_package import billing

#从.billing中导入所有程序单元到fk_package中

from fk_package.billing import*

#从当前包中导入arithmetic_chart模块

from fk_package import arithmetic_chart

#从.arithmetic_chart中导入所有程序单元到fk_package中

from fk_package.arithmetic_chart import *

在其他的源文件中执行如下代码:

#导入fk_package包,实际上导入包下的__init__.py文件

import fk_package



#直接使用fk_package,前缀即可调用它所包含的模块内的程序单元

fk_package.print_blank_tringle(5)

im=fk_package.Item(4)

print(im)

fk_package.print_multipe_chart(5)

"""

    *

   * *

  *   *

 *     *

*********

Item[price=4]

1 * 1 =  1  

1 * 2 =  2  2 * 2 =  4  

1 * 3 =  3  2 * 3 =  6  3 * 3 =  9  

1 * 4 =  4  2 * 4 =  8  3 * 4 = 12  4 * 4 = 16  

1 * 5 =  5  2 * 5 = 10  3 * 5 = 15  4 * 5 = 20  5 * 5 = 25  

"""

9.3查看模块内容

在导入模块之后,开发者往往需要了解模块包含哪些功能,还希望能查看模块中各成员的帮助信息。

9.3.1 模块包含什么

为了查看包含什么,可以通过如下两种方式:

  1. 使用dir()函数。
  2. 使用模块本身提供的__all__变量。

dir()哈桑农户可用于返回模块或类所包含的全部程序单元(包括变量、函数和方法等),但直接使用dir()函数默认会列出模块内所有的程序单元包括以下划线开头的程序单元,而这些以下划线开头的程序单元其实并不希望被外界使用。

比如在Python的交互解释器中执行如下命令来导入string模块(Python内置的用于丰富字符串功能:

dir(string)

['Formatter', 'Template', '_ChainMap', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_re', '_sentinel_dict', '_string', 'ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace']

很明显,该模块内有大量以下划线开头的程序单元,其实这些程序单元并不希望被其他程序使用,因此这写程序单元意义不大。

为了过滤这些以下划线开头的程序单元,可以使用如下列表推导式来列出模块中的程序单元。

[e for e in dir(string) if not e.startswith('__')]

['Formatter', 'Template', '_ChainMap', '_re', '_sentinel_dict', '_string', 'ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace']

此外,前面还介绍了模块中的__all__变量,该变量相当于该模块开放的功能接口,因此也可通过该模块的__all__变量来查看模块内的程序单元。

string.__all__

['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace', 'Formatter', 'Template']

对比前面列表推导式列出的结果和此处列出的变量完全相同。

注意:并不是所有的模块都会通过__all__变量的,有些模块不提供,在这种情况下,只能使用列表推导式来查看模块中的程序单元。

9.3.2 使用__doc__属性查看文档

前面介绍使用help()函数来查看程序单元的帮助信息:

help(string.capwords)

Help on function capwords in module string:

capwords(s, sep=None)

    capwords(s [,sep]) -> string

    Split the argument into words using split, capitalize each

    word using capitalize, and join the capitalized words using

    join.  If the optional second argument sep is absent or None,

    runs of whitespace characters are replaced by a single space

    and leading and trailing whitespace are removed, otherwise

    sep is used to split and join the words.

通过上面描述可以看出,capwords()函数的作用就是将给定的s字符串中的每个单词首字母变成大写的。该函数可通过sep参数指定分隔符;如果不指定sep参数,该字符串默认以空白作为分隔符。

string.capwords('abc xyz')

'Abc Xyz'

string.capwords('abc;xyz',sep=';')

'Abc;Xyz'

需要说明的是,使用help()函数之所以能查看程序单元的帮助信息,其实完全是因为该程序单元本身有文档信息,也就是__doc__属性。换句话说,使用help()函数查看的其实就是程序单元的__doc__属性值。

print(string.capwords.__doc__)

capwords(s [,sep]) -> string

    Split the argument into words using split, capitalize each

    word using capitalize, and join the capitalized words using

    join.  If the optional second argument sep is absent or None,

    runs of whitespace characters are replaced by a single space

    and leading and trailing whitespace are removed, otherwise

    sep is used to split and join the words.

help()函数就是输出程序单元的__doc__属性值。

9.3.3 使用__file__属性查看模块的源文件路径

除可以查看模块的帮助信息之外,还可以直接阅读模块的源代码功能,提升Python编程能力。

提示:不管学习什么语言,认真阅读哪些优秀的框架、库的源代码都是非常好的学习方法。

通过模块的__file__属性即可查看指定模块的源文件路径。

string.__file__

'D:\\python\\Lib\\string.py'

这说明string模块对应的文件就是保存在D:\\python\\Lib\\string.py文件,开发者完全可以直接打开该文件来查看该模块的全部源代码。

需要说明的是,并不是所有模块都是使用Python语言编写的,有些与底层交互的模块可能是用C语言编写,而且是C程序编译之后的结果,所有这种模块可能没有__file__属性。

  • 22
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

提刀立码,调参炼丹

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

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

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

打赏作者

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

抵扣说明:

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

余额充值