从Scratch到Python 21 更复杂的程序结构

我们已经学习了模块,也学习了面向对象编程,以及如何使用第三方库,可以编写规模更大、逻辑更复杂一些的程序了。要编写这类程序,我们必须先掌握一些重要概念,分别是包、命名空间与作用域。

一、包

什么是包呢?包实际上就是一个文件夹。现实中我们文件资料太多,不容易分类查找的时候,我们会用不同的文件夹把它们分类整理;使用计算机时产生的文件,我们也会放到不同的文件夹,比如音乐文件、文档文件、视频文件等;对于编程来说,如果我们要实现的代码很多,我们首先会把它分成模块来编写,每个模块就是一个.py文件。那如果模块多起来怎么办呢?最直接的就是建立不同的文件夹,把不同类型的模块放到各自的文件夹中去,这种存储模块的文件夹就是包。

我们来举个例子,现在假如我们要编写一个学校管理程序,里面有教师相关的功能、学生相关的功能、图书相关的功能,每一个都有不同的模块,该怎么组织我们的代码呢?

我们来建立一个稍微复杂点的项目结构。打开海龟编辑器,点击左上角的项目文件夹按钮:

 

 

这是一个开关按钮,点一下会显示项目文件夹界面,里面有“打开项目”和“新建项目”两个按钮,再点一下这个界面会隐藏。

 

 

其实项目也就是海龟编辑器在我们计算机上专门建立的一个文件夹,以前我们写的程序大多是单文件的,或者是并列的模块,用不着项目来组织。现在既然要用到包,我们就点击“新建项目”来建立一个项目:

 

 

现在海龟编辑器已经为我们在电脑上建立了“我的项目”文件夹(在海龟编辑器自己的工作空间目录),如果你不是第一次建立项目,那就会依次命名为我的项目1、我的项目2……如果你想改项目名称,可以在点击红色区域的最右边那个三个小点按钮,在弹出的菜单中选择“重命名”,重新输入项目名称就可以。我们这里就不修改了。

注意红色框内的按钮,从左向右分别是新建文件、新建文件夹和添加本地文件。我们可以使用这三个按钮向项目中添加必要的代码文件和目录。

首先我可以建立teacher、students、book这三个文件夹,分别用于存储教师管理、学生管理和图书管理的模块:

 

 

注意在新建目录的时候,要保持鼠标的焦点在项目界面的空白区域,不能选中一个文件夹,否则你建立的文件夹会成为这个选中文件夹的下一级,这就是嵌套关系了,和我们今天的演示不一致。

建立好文件夹,我们就在teacher文件夹中添加mod1.py。注意要先选中teacher文件夹再点击新建文件:

 

 

这时双击mod_1.py(注意是双击鼠标),就会在右侧打开这个代码文件,我们输入以下代码:

def fun_a():
    print('This is teacher.mod1.fun_a!')

此时界面如下:

 

 

用同样的方法我们在teacher文件夹中再建立mod2.py,内容为:

def fun_b():
    print('This is teacher.mod2.fun_b!')

 

 

这时,teacher就是一个Python包了。我们如果要在主程序,也就是新建项目时海龟编辑器为我们新建的“我的文件.py“中调用这个包中的代码,可以在”我的文件.py“中写入:

from teacher import mod1,mod2
mod1.fun_a()
mod2.fun_b()

这时运行程序,会发现正确调用了包中的代码:

 

 

这时你可能会想,我能不能像使用绘图的小海龟一样,直接import teacher,然后调用teacher.mod1或者teacher.mod2中的函数呢?可以,不过我们要多做一些工作。

在teacher文件夹下新建一个文件,名字是”init.py“,注意两边是两个英文的下划线,然后在其中写入:

from . import mod1
from . import mod2

然后你就可以修改”我的文件.py“中的代码,直接导入teacher了:

import teacher
teacher.mod1.fun_a()
teacher.mod2.fun_b()

为什么会这样呢?还记得我们学面向对象时创建类的实例时,它会自动调用init()方法吗?同样的,init.py这个文件中的内容会在你导入包的时候自动运行,里面的from . import mod1意思是从当前目录导入mod1模块。这样,init.py就成了teacher包的一个目录,当你导入包的时候,包的每一个模块也被导入了,你可以在主程序中使用它们,非常方便。

如果teacher文件夹下面还有子文件夹,我们同样应该为它们建立init.py作为目录,当你导入teacher时,会通过递归的方式把每一层的模块都导入进来供主程序使用。

到这里,包的知识我们就讲完了,唯一需要注意的就是这个init.py,它是方便我们使用包的一个重要工具。

二、全局变量与局部变量

看下面这段程序:

def test_func(a,b):
    c = a * b
    return c

print(c)

你觉得这段简单的程序有问题吗?如果你没有把握,可以运行一下。会发现程序报错了:

 

 

错误信息:变量“c”未定义。

这是怎么回事呢?在函数test_func()中,我们明明声明了一个变量c,而且把a*b的值赋值给它。为什么Python认为这个变量还是没有定义呢?

这个问题是我们以前没有提到的,我们的程序还比较简单的时候一般不会遇到这类问题。但如果程序有了一定的复杂度,你就必须了解变量的作用域。

简单地说,在函数内部定义的变量称为“局部变量”,只能在函数内部使用,函数外部调用就会出错。

那么反过来说,函数外部定义的变量,在函数内部能不能用?我们再来试一个例子:

c = 1000
def test_func(a,b):
    c = a * b
    return c
d = test_func(2,3)
print('d=',d)
print('c=',c)

先不要看分析,你觉得程序输出的变量d和变量c的值相同吗?为什么?

答案是不同,尽管你看到我们调用test_func(2,3)的时候,将一个变量“c”赋值为2*3,同时这个结果也返回给了d,但程序的输出是这样的:

 

 

奇怪吗?我在函数内部给变量“c”重新赋值了,为什么它还是等于1000?这是因为,变量“c”是在函数外部定义的,它是一个“全局变量”,在函数内部不能使用。即使你在函数内部又声明了一个变量“c”,Python认为它是另一个局部变量,和全局变量“c”没有关系——Python是不是相当“固执”?没办法,这是为了保证函数内部的代码运行时,不至于影响到全局变量的值,如果许多函数都修改了全局变量,谁也不知道这个变量里面保存了什么值了,程序的逻辑就会混乱。

这时你可能想,我比Python还固执,我就是要在函数里面修改全局变量的值,不行吗?当然可以,这需要你使用 global关键字,告诉Python我要用的就是全局变量,不是一个新的局部变量:

c = 1000
def test_func(a,b):
    global c
    c = a * b
    return c
d = test_func(2,3)
print('d=',d)
print('c=',c)

再次运行,这次的变量c和变量d值相同了,都是6。

 

 

原来是这样!你长吁一口气,打算休息一下了——且慢,还没完。如果变量c是一个列表,会发生什么事?

c = [1,2]
def test_func(a,b):
    c.append(a+b)
    return c
d = test_func(2,3)
print('d=',d)
print('c=',c)

在上面这段程序中,c是一个列表型变量,初始状态下它有1、2两个元素,在函数内部我们直接将a+b的值作为一个新元素添加到c中(在函数内部并没有声明c),结果函数居然没有出错,还修改了全局变量c的值:

 

 

看来,如果是组合类型的变量(比如列表),即使是全局变量,在函数内部也可以修改它。

如果你的原意是想在函数内部使用自己的列表c怎么办呢?只要在函数内部重新声明一下就可以了,它和全局变量c就没关系了:

c = [1,2]
def test_func(a,b):
    c = []
    c.append(a+b)
    return c
d = test_func(2,3)
print('d=',d)
print('c=',c)

 

 

你可能有点晕,不过没办法,谁让你的水平提升,开始做更复杂的程序了呢?这就需要了解更多的技术细节。这里我们把全局变量和局部变量的内容总结一下:

  • 简单数据类型的局部变量只能在函数内部创建和使用
  • 简单数据类型的局部变量用global关键字声明后可以当作全局变量使用
  • 组合数据类型的全局变量(如列表),在函数内部没有创建的情况下,可以修改全局变量
  • 组合数据类型的全局变量(如列表),在函数创建同名变量的情况下,函数只修改局部变量,不影响全局列表的值

三、命名空间及作用域

从全局变量和局部变量的使用规则我们可以看出,变量名字重复是挺麻烦的。那么如果一个程序由多个模块组成,每个模块又是不同的程序员开发的,万一他们用的变量名字重复了怎么办呢?会不会出错?

这一点你可以放心,因为Python中有“命名空间”的概念。命名空间是名称到对象的对应关系,一个模块里所有的名称属于同一个命名空间,所以命名不能重复;但模块1和模块2就是两个不同的命名空间,假如两个模块中都有同一个变量a,也不会相互影响。在现实中有一个对应的例子——假如你们班有位同学叫李磊,隔壁班也有个同学叫李磊,大家在各自的班级不会混淆;如果校长想找李磊,他只要带上命名空间就行,比如一二班的李磊,或者一三班的李磊,也能分得清。

Python中有下面三类命名空间:

  • 内置名称:包括Python内置的函数和异常名称等,如str,print,NameError;
  • 全局名称:包括在程序中使用的名称和各种导入模块的名称。这个命名空间在程序创建时被创建,程序运行结束时终止;
  • 局部名称:包含一个函数中定义的名称,包括了函数的参数和局部变量的名称等。

Python查找特定名称时,会遵循从近即远的原则,即先找局部名称,如果找不到才会去全局名称中找;全局名称中没有,再去内置名称中寻找。看下面的例子:

# 调用内部函数print
print('hello')
# 定义一个与内部函数print重名的函数
def print(a):
    # pass 代表“什么也不做”
    pass
# 再次调用print
print('world')

在这个例子里我们定义了一个与内置函数print()重名的函数,当你在它后面调用print(‘world’)的时候,它调用的就是全局名称,而不是Python内置名称,所以调用了我们自己的print()函数,这个函数什么也不做(只有一行pass语句),所以程序只输出了’hello’,没有’world’。

 

 

了解了命名空间的概念,我们再回顾一下全局变量和局部变量。在前面的代码中,你在函数里定义的局部变量,它的有效范围就是在函数内部,这个范围就是作用域。超出作用域的代码是无法访问这个变量的。在Python中一共有四种作用域,从内向外分别是:

局部作用域(Local):包括局部变量,是最内层的作用域,比如自定义函数内部区域;

嵌套作用域(Enclosing):包含非全局变量,比如你在函数a中又定义了一个函数b(Python是允许这么定义的,尽管我们还没用过,你了解即可),那么对于函数b中的变量来说,函数a的作用域就是嵌套作用域;

全局作用域(Global):当前所运行程序的最外层,包括该程序中导入的模块的全局变量;

内置作用域(Build-in):包含内置变量和关键字等。

如果内部作用域想修改外部作用域的变量时,就要用到 global 和 nonlocal 关键字了。前者用来修改全局作用域变量,后者是修改嵌套作用域变量的。前面讲全局变量时我们已经举例说明了global关键字的用法,它就是用来跨作用域的。

对于初学Python的你来说,不需要对四种作用域死记硬背,在未来的编程学习中,我们会不断遇到这方面的示例,你可以在实际运用中掌握它们,这里留下印象即可。

四、课后作业

请参照第一部分内容,在book目录下建立mod1和mod2两个模块,并分别包括fun_a和fun_b函数,在book目录下建立init.py导入本目录的模块,利用主程序导入book包并运行mod1、mod2中的函数。

给TA赞助
共{{data.count}}人
人已赞助
综合资讯

从Scratch到Python 20 用好现成的“轮子”

2023-6-6 11:29:47

综合资讯

信奥赛助力中高考,哪些孩子适合信息学奥赛

2023-6-7 10:23:06

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索