week4 day1 初识模块

一. 模块介绍

1.1 什么是模块?

模块是功能的集合体。在python中,一个py文件就是一个模块,文件名为xx.py模块名则为xxx,导入模块可以引用模块已经写好的功能。如果把开发程序比喻成制造一台电脑,编写模块就像是制造电脑的零部件,准备好零部件以后,剩下的工作就是按照逻辑把它们组装在一起。

将程序模块化会使得程序的组织结构清晰,维护起来更加方便。比起直接开发一个完整的程序,单独开发一个小的模块也会更加简单,并且程序中的模块与电脑中的零部件稍微不同的是:程序中的模块可以被重复使用。所以总结下来,使用模块既保证了代码的重用性,又增强了程序的结构性和可维护性。另外除了自定义模块外,我们还可以导入使用内置或者第三方模块提供的现成功能,这种”“拿来主义”可以极大地提高程序员的开发效率。

模块大致分为四种:

模块分类掌握程度
一个py文件就是一个模块,文件名叫test.py,模块名叫test重点掌握
在python2解释器中,包含__init__.py的文件夹称之为包,包也是模块重点掌握
使用c编写的并连接到python解释器的内置模块。会用即可
已被编译成共享库或DDL的c或c++扩展会用即可

模块的三种来源:

  1. 自带的模块
  2. 第三方模块:pip3 install requests
  3. 自定义模块

1.2 为何要使用模块?

  1. (自带的,第三方模块)拿来主义,提高开发效率
  2. 自定义代码是为了解决代码冗余问题

二. 模块的使用

执行文件只有一个,而模块文件是在文件被执行前提前写好导入要执行的文件中,导入文件的方法有两种。而我们之前知道,执行python文件会开辟名称空间,将文件中定义的名称放到名称空间里面。

导入模块的规范:
通常情况下所有的导入语句都应该写在文件开头,然后分为三部分:
第一部分:先导入自内置的模块
第二部分:导入第三方模块
第三部分:导自定义的模块

import time

import 第三方模块

import spam

如果第三方库或者自定义模块的名字过长,可以给模块重新命名,方便再次调用。

import ifuruiegbirugberu as i
i.不啦不啦

2.1 import

#文件名:foo.py
x=1
def get():
    print(x)
def change():
    global x
    x=0
class Foo:
    def func(self):
       print('from the func')

要想在另外一个py文件中引用foo.py的功能,需要使用import foo,首次导入模块会做三件事情:

  1. 执行源文件代码
  2. 产生一个新的名称空间用于存放源文件执行过程中产生的名字
  3. 在当前执行文件所在的名称空间中得到一个名字foo,该名字指向新创建的模块名称空间,若要引用模块名称空间中的名字,需要加上前缀,如下
import foo #导入模块foo
a=foo.x #引用模块foo中变量x的值赋值给当前名称空间中的名字a
foo.get() #调用模块foo的get函数
foo.change() #调用模块foo中的change函数
obj=foo.Foo() #使用模块foo的类Foo来实例化,进一步可以执行obj.func()

加上foo.作为前缀就相当于指名道姓地说明要引用foo名称空间中的名字,所以肯定不会与当前执行文件所在名称空间中的名字相冲突,并且若当前执行文件的名称空间中存在x,执行foo.get()或者foo.change()操作的都是源文件中的全局变量x。

需要强调一点是,第一次导入模块已经将其加载到内存空间了,之后的重复导入会直接引用内存中已存在的模块,不会重复执行文件,通过import sys,打印sys.modules的值可以看到内存中已经加载的模块名。

提示:

import spam
只有首次导入时会产生名称空间,后续调入时只会重复访问第一次调用时产生的名称空间。

在这里插入图片描述

2.2 from…import…

from...import...import语句基本一致,唯一不同的是:使用import foo导入模块后,引用模块中的名字都需要加上foo.作为前缀,而使用from foo import x,get,change,Foo则可以在当前执行文件中直接引用模块foo中的名字,如下:

from foo import x,get,change #将模块foo中的x和get导入到当前名称空间
a=x #直接使用模块foo中的x赋值给a
get() #直接执行foo中的get函数
change() #即便是当前有重名的x,修改的仍然是源文件中的x

在这里插入图片描述
通过这种方式导入模块中的变量或者函数时,在执行主文件的时候需要注意这个名称是来自哪里(模块/主文件),再回到那个位置查看定义阶段是如何定义的,找作用域关系确定最后的值。

如果我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果,但需要强调的一点是:只能在模块最顶层使用的方式导入,在函数内非法,并且导入的方式会带来一种副作用,即我们无法搞清楚究竟从源文件中导入了哪些名字导当前位置,这极有可能与当前名称空间中的名字产生冲突。模块的编写者可以在自己的文件中定义_all_变量用来控制*代表的意思。

#foo.py文件:
__all__=['x','get'] #该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字
x=1
def get():
    print(x)
def change():
    global x
    x=0
class Foo:
    def func(self):
       print('from the func')

# 想导入模块foo中功能的文件:
from foo import * #此时的*只代表x和get

x #可用
get() #可用
change() #不可用
Foo() #不可用

2.3 其他导入语法(as)

我们还可以在当前位置为导入的模块起一个别名

import foo as f #为导入的模块foo在当前位置起别名f,以后再使用时就用这个别名f
f.x
f.get()
还可以为导入的一个名字起别名
from foo import get as get_x
get_x()

通常在被导入的名字过长时采用起别名的方式来精简代码,另外为被导入的名字起别名可以很好地避免与当前名字发生冲突,还有很重要的一点就是:可以保持调用方式的一致性,例如我们有两个模块jsonpickle同时实现了load方法,作用是从一个打开的文件中解析出结构化的数据,但解析的格式不同,可以用下述代码有选择性地加载不同的模块

if data_format == 'json':
    import json as serialize #如果数据格式是json,那么导入json模块并命名为serialize
elif data_format == 'pickle':
    import pickle as serialize #如果数据格式是pickle,那么导入pickle模块并命名为serialize

data=serialize.load(fn) #最终调用的方式是一致的

2.4 循环导入问题

循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,究其根源就是在python中,同一个模块只会在第一次导入时执行其内部代码,再次导入该模块时,即便是该模块尚未完全加载完毕也不会去重复执行内部代码。

即两个模块文件相互导入彼此。尽量不要出现这种情况!!!如果实在出现了这种情况,有一种解决思想:把调用模块的语句放在最后执行,先在各自创建的名称空间里面添加完所有的名称,再运行导入模块那句话。

我们以下述文件为例,来详细分析循环/嵌套导入出现异常的原因以及解决的方案

# 文件m1.py:
print('正在导入m1')
from m2 import y

x='m1'

# 文件m2.py:
print('正在导入m2')
from m1 import x

y='m2'

# 文件run.py:
import m1

测试一:

#1、执行run.py会抛出异常
正在导入m1
正在导入m2
Traceback (most recent call last):
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/aa.py", line 1, in <module>
    import m1
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
    from m1 import x
ImportError: cannot import name 'x'

#2、分析
先执行run.py--->执行import m1,开始导入m1并运行其内部代码--->打印内容"正在导入m1"
--->执行from m2 import y 开始导入m2并运行其内部代码--->打印内容“正在导入m2”--->执行from m1 import x,由于m1已经被导入过了,所以不会重新导入,所以直接去m1中拿x,然而x此时并没有存在于m1中,所以报错

测试二:

#1、执行文件不等于导入文件,比如执行m1.py不等于导入了m1
直接执行m1.py抛出异常
正在导入m1
正在导入m2
正在导入m1
Traceback (most recent call last):
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
    from m1 import x
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
ImportError: cannot import name 'y'

#2、分析
执行m1.py,打印“正在导入m1”,执行from m2 import y ,导入m2进而执行m2.py内部代码--->打印"正在导入m2",执行from m1 import x,此时m1是第一次被导入,执行m1.py并不等于导入了m1,于是开始导入m1并执行其内部代码--->打印"正在导入m1",执行from m1 import y,由于m1已经被导入过了,所以无需继续导入而直接问m2要y,然而y此时并没有存在于m2中所以报错

解决方案:

# 方案一:导入语句放到最后,保证在导入时,所有名字都已经加载过
# 文件:m1.py
print('正在导入m1')

x='m1'

from m2 import y

# 文件:m2.py
print('正在导入m2')
y='m2'

from m1 import x

# 文件:run.py内容如下,执行该文件,可以正常使用
import m1
print(m1.x)
print(m1.y)

# 方案二:导入语句放到函数中,只有在调用函数时才会执行其内部代码
# 文件:m1.py
print('正在导入m1')

def f1():
    from m2 import y
    print(x,y)

x = 'm1'

# 文件:m2.py
print('正在导入m2')

def f2():
    from m1 import x
    print(x,y)

y = 'm2'

# 文件:run.py内容如下,执行该文件,可以正常使用
import m1

m1.f1()

注意:循环导入问题大多数情况是因为程序设计失误导致,上述解决方案也只是在屎上雕花的无奈之举,在我们的程序中应该尽量避免出现循环/嵌套导入,如果多个模块确实都需要共享某些数据,可以将共享的数据集中存放到某一个地方,然后进行导入

2.5 模块的搜索路径和优先级

当主文件调用一个模块的时候,是按照这样的顺序搜索模块的:

  1. 搜索内存中的(如果模块第一次导入后,文件执行结束前在内存中存在模块的名称空间)
  2. 在内置的模块中搜索
  3. 在sys.path的目录中挨个搜索模块的名字的py文件
import sys 
print(sys.path)----->['E:\\pycharm\\notes and homework\\w4 1.4', 'E:\\pycharm\\notes and homework', 'E:\\pycharm\\PyCharm 2020.1.2\\plugins\\python\\helpers\\pycharm_display'...]
会得到一个列表,列表的第一个文件路径是当前执行文件的文件夹路径,第二个文件路径是当前执行文件所在的项目文件夹目录的路径,后面都是pycharm内置的存放路径。

如果想导入的模块不在内存,内置以及环境变量(sys.path)下,两种导入方法(eg m1在aaa文件夹下):

# 方法一:
import aaa.m1 # .在导入语句中,是子文件夹的意思,在调用语句中,是内置变量/函数的意思
aaa.m1.f1()

# 方法二:
import sys
sys.path.append(r'文件路径')

导入的第三方库或者自定义模块不应与内置模块名字发生冲突。

2.6 区分py文件的两种用途

一个Python文件有两种用途,一种被当主程序/脚本执行,另一种被当模块导入,为了区别同一个文件的不同用途,每个py文件都内置了__name__变量,该变量在py文件被当做脚本执行时赋值为“__main__”,在py文件被当做模块导入时赋值为模块名

作为模块foo.py的开发者,可以在文件末尾基于__name__在不同应用场景下值的不同来控制文件执行不同的逻辑

#foo.py
...
if __name__ == '__main__':
    foo.py被当做脚本执行时运行的代码
else:
    foo.py被当做模块导入时运行的代码

通常我们会在if的子代码块中编写针对模块功能的测试代码,这样foo.py在被当做脚本运行时,就会执行测试代码,而被当做模块导入时则不用执行测试代码。

2.7 编写一个规范的模块

我们在编写py文件时,需要时刻提醒自己,该文件既是给自己用的,也有可能会被其他人使用,因而代码的可读性与易维护性显得十分重要,为此我们在编写一个模块时最好按照统一的规范去编写,如下

#!/usr/bin/env python #通常只在类unix环境有效,作用是可以使用脚本名来执行,而无需直接调用解释器。

"The module is used to..." #模块的文档描述

import sys #导入模块

x=1 #定义全局变量,如果非必须,则最好使用局部变量,这样可以提高代码的易维护性,并且可以节省内存提高性能

class Foo: #定义类,并写好类的注释
    'Class Foo is used to...'
    pass

def test(): #定义函数,并写好函数的注释
    'Function test is used to…'
    pass

if __name__ == '__main__': #主程序
    test() #在被当做脚本执行时,执行此处的代码

三. 文件目录开发规范

我们之前学过的书写代码是把所有的代码写到一个文件里面,这样写会使修改文件变得非常的麻烦。程序设计者写完代码后,将文件发送给使用者,使用者使用错误中出错也会很难做出修改,使用者的使用也非常麻烦。因此,我们需要使用更清晰合理的结构来整理我们写好的“面条型”代码。这也就是软件目录开发的规范。

如下图所示,如果我们需要将不同功能的代码块放到不同的文件里面,方便我们的使用和管理。

文件夹名文件夹里面的文件文件的功能
binstart.py运行程序的代码,唯一的执行文件,eg run()
coresrc.py存放业务逻辑相关代码,即主体代码,eg atm项目中各项功能的定义
confsettings.py主体文件中的配置内容,eg atm项目中日志的格式和程序根目录
dbdb_handler.py里面存放对于数据的操作,主要用于与数据库的交互,包括读和写
libcommon存放创建日志的方法
logaccess.log存放写入的日志信息
interfaceinterface.py存放接口文件,接口主要用于为业务逻辑提供数据操作。

在这里插入图片描述
这样设置目录完,我们将原本在“面条型”代码里面的看起来有些乱的代码都拆分到了各个文件里面,但这些文件是彼此独立的,因此我们需要一根线,把所有文件串到一起。线 就是执行文件start.py,而线就是我们的start.py文件。把其他的文件都当作模块导入。

先导入内置模块sys和os。sys模块下的path方法是参照执行文件的环境变量进行配置,所以要拿到当前(执行)文件的根目录,从根目录的不同文件中获得被拆开的代码文件。需要BASE_DIR=os.path.dirname(os.path.dirname(__file__))拿到文件的根目录,然后把根目录添加到环境变量中。(这里使用了两次获取文件路径是因为当前start.py的父级是bin,bin的父级是ATM,也就是程序根文件夹)sys.path.append(BASE_DIR),这样就可以从其他文件夹中导入其他文件来使用。
例如:

from core import src
from conf import settings
from db import db_handler
from lib import common
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值