模块化

前面的课程中我们已经写过很多代码了,能看到代码的规模和复杂性都在不断增长。现在,我们需要更好的方法来管理我们的代码,当然我们之前已经多次用过了函数——可以帮助我们将一些变量和逻辑代码组合在一起以方便重复使用,并且在字符串章节中体验了模块的简单实用。 在本章节中,我们将深入研究模块以便更高效的使用它,并且学习代码重用的终极利器:类和对象的知识。

模块的运行机制

模块简单回顾

从前面学习的知识中我们知道,要使用一个模块,需要先导入它,也就是用import语句。比如以下这个例子,我们导入了两种不同的模块:

import random
random.randint(0,9)
import ch1text
print(ch1text.text)

为什么说这两种模块不同呢?下面这个模块是我们自己创建的,从左侧代码目录中能够很容易得找到,而random在我们的文件夹里并没有,它是一个内置模块!那内置模块是具体是如何生效的呢?这就涉及到Python使用模块的机制了。当我们import一个模块的时候,Python会首先在当前目录去寻找有没有要导入的模块,也就是这个模块名命名的python文件,如果有那就直接使用,比如ch1text;如果没有那就继续寻找,从哪里寻找呢?这里就涉及到Python环境变量的设置问题了,我们需要告诉Python去哪里寻找,打开电脑的高级系统设置,找到环境变量中Python相关的设置,我们把它复制出来用文件管理器打开,这里就是Python寻找模块的地方了,在Lib文件夹中有我们安装Python时自带的模块,比如random,也有我们后来安装的模块,比如pygame这个用于做游戏的模块。那你可能会疑惑,我们之前好像并没有配置过这个东西呀,是的!一般情况下我们安装Python时只要步骤正确,这里就会自动配置好。当然在使用电脑的过程中,有时候这些配置会被不小心或者某些恶意软件给故意修改掉,导致Python出问题,现在学了这个机制之后我们就可以再遇到问题的时候有针对性的去分析和解决问题了。

__name__全局变量

现在我们使用自带的模块或者别人的模块都比较熟练了,那我们写好的功能怎么做成模块分享给别人使用呢,你可能会瞬间想到,直接把源代码文件发给他不就可以啦!当然,我们用的其实就是这种方法,但是把不经优化的代码直接发给别人当做模块来使用可能会有一些问题,比如判断阅读难度这个程序。如果我们import进来之后会发现程序直接就输出了对于这段默认文本的分析结果了,但其实这只是我们自己测试的时候想要的效果,给别人使用的时候我们希望这段代码不要去运行。要达到这个效果也是没问题的,这就是__name__变量,这是一个全局变量,每次执行一个Python文件,在后台Python解释器就会创建这样一个全局变量。这个变量被赋值时有两种可能:如果Python文件直接作为主程序执行,这个变量就设置为字符串"main",我们可以测试一下。如果Python文件是作为模块被import进去的,这个变量就会设置为你的模块的名字,如"analyze"。我们再测试一下。

if __name__ == '__main__':
    print("我是主程序")
else:
    print("我是一个模块")
print(__name__)
import just_a_module

将阅读难度计算器用作模块

如果我们想要把阅读难度计算器用作模块,就不能让它在被其他文件import进去的时候执行计算测试数据的代码了,所以我们需要这么修改:

if __name__ == "__main__":
    import ch1text
    print('Chapter1 Text:')
    compute_readability(ch1text.text)

这样的话我们直接运行程序还是可以计算测试数据的阅读难度的,但是被其他文件import进去的时候,这段代码就不会执行了,完美!我们还是来测试一下。

import analyze
analyze.compute_readability("""
If you've never programmed a computer, you should. There's nothing like it in the
whole world. When you program a computer, it does exactly what you tell it to do.
It's like designing a machine:  any machine, like a car, like a faucet, like a gas
hinge for a door  using math and instructions. It's awesome in the truest sense it
can fill you with awe.
A computer is the most complicated machine you'll ever use. It's made of billions
of micro miniaturized transistors that can be configured to run any program you
can imagine. But when you sit down at the keyboard and write a line of code, those
transistors do what you tell them to.
Most of us will never build a car. Pretty much none of us will ever create an
aviation system. Design a building. Lay out a city.""")

这样别人使用起来就比较方便了。但如果是一个完全没接触过这个功能的人可能在拿到这个模块的时候还是会有点蒙,除非他再去把程序阅读一遍理解之后才知道怎么用,有没有什么方法能直接把这个模块具有的功能,也就是有哪些函数很便捷的告诉使用者呢。当然有,我们之前是不是就学过了注释?接下来我们学习一个注释的高级用法。

给模块添加注释

我们在模块程序中以这种形式添加注释的话,就能被Python的帮助函数help()所捕获,像VSCode这样的高级编辑器也可以获取到这部分注释,这样的话使用模块的人就会比较容易理解模块的功能和使用方法了。

"""
  这个analyze模块运用Flesch-Kincaid阅读系数测试法
  来分析文章并生成一个阅读系数,
  然后基于阅读系数来生成一个基于年级的阅读建议。
"""
def count_syllables(words):
    """
        这个方法会把输入的单词列表中的音节数量计算后汇总返回。
    """
    count = 0

到这里我们对模块的理解就比较深入了,Python之所以特别流行有一个很重要的原因就是它有丰富的模块库,世界上很多优秀的工程师、科学家选择把他们的程序包装成模块分享给大家,比如做游戏的pygame,做网络爬虫的requests,以及我们之前了解过的小海龟Turtle,这里我们不能全部涉及到,你如果感兴趣可以到网上搜搜看,后面我们也会挑一些比较有趣的模块来讲解一下。

海龟模块

海龟简单回顾

我们其实已经用过海龟模块了,这是一个Python内置的模块,目的是方便初学者可以快速掌握编程的基础知识。之前在讲解循环知识的时候小海龟已经帮过我们很多次了,接下来我们首先回顾一下小海龟的基本用法,然后再让它帮我们深入理解下函数并引出类和对象的知识。

import turtle
slowpoke = turtle.Turtle()
slowpoke.shape('turtle')
slowpoke.forward(100)
slowpoke.right(90)
slowpoke.forward(100)
slowpoke.right(90)
slowpoke.forward(100)
slowpoke.right(90)
slowpoke.forward(100)
slowpoke.right(90)
turtle.mainloop()

程序还是比较简单的,但是注意给文件起名字的时候不要用turtle.py,为什么呢?我们在刚才random的例子中是不是已经尝试过了,这样会导致Python找不到正确的turtle模块了。

用函数优化

在刚才的基础上,我们可以将画画的过程抽象成一个函数,然后再引入第2个小海龟一起来绘图。

import turtle # turtle:乌龟
slowpoke = turtle.Turtle() # slowpoke:乌龟1的名字
slowpoke.shape('turtle')   # shape:形状
pokey = turtle.Turtle()    # pokey:乌龟2的名字
pokey.shape('turtle')
pokey.color('red')         # color:颜色  red:红色
def make_square(the_turtle):  # make:制作 square:正方形
    for i in range(0,4):
        the_turtle.forward(100)  # forward: 前进
        the_turtle.right(90)     # right: 右转
make_square(slowpoke)
pokey.right(45)
make_square(pokey)
turtle.mainloop()

如果想要画新的图形,我们只要创建一个新的画画函数就可以啦

import turtle # turtle:乌龟
slowpoke = turtle.Turtle() #slowpoke:生成的乌龟的名字
slowpoke.shape('turtle') #shape:形状
pokey = turtle.Turtle() #pokey:生成的乌龟的名字
pokey.shape('turtle')
slowpoke.color('#BBFFFF') #blue:蓝色
pokey.color('#FF69B4') #red:红色
def make_square(the_turtle): #make:制作 square:正方形
    for i in range(0,4):
        the_turtle.forward(100) #forward:前进
        the_turtle.right(90) #right:向右转
def make_spiral(the_turtle): #spiral:螺旋形
    the_turtle.speed(0)
    for i in range(0, 36):
        make_square(the_turtle)
        the_turtle.right(10)
make_spiral(slowpoke)
pokey.right(5)
make_spiral(pokey)
turtle.mainloop()

更多海龟试验

海龟还可以画出更多有趣的图案,这里列出来几个,在运行之前你通过看代码能猜出来小海龟会画什么吗?试试看,然后运行看看跟你猜的一样不一样。而且还可以试着改变一些值,看输出会有什么样的变化?

import turtle
slowpoke = turtle.Turtle()
slowpoke.shape('turtle')
for i in range(5):
    slowpoke.forward(100)
    slowpoke.right(144)
turtle.mainloop()
import turtle
slowpoke = turtle.Turtle()
slowpoke.shape('turtle')
slowpoke.pencolor('blue')
slowpoke.penup()
slowpoke.setposition(-120, 0)
slowpoke.pendown()
slowpoke.circle(50)
slowpoke.pencolor('red')
slowpoke.penup()
slowpoke.setposition(120, 0)
slowpoke.pendown()
slowpoke.circle(50)
turtle.mainloop()
import turtle
slowpoke = turtle.Turtle()
slowpoke.shape('turtle')
def make_shape(t, sides):
    angle = 360/sides
    for i in range(0, sides):
        t.forward(100)
        t.right(angle)
make_shape(slowpoke, 3)
make_shape(slowpoke, 5)
make_shape(slowpoke, 8)
make_shape(slowpoke, 10)
turtle.mainloop()

turtle到底是什么

画了这么多,让我们回到最初创建小海龟的代码。乍一看就像是我们调用了turtle模块中的一个名为Turtle的函数:

slowpoke = turtle.Turtle()

那么turtle模块有一个创建海龟的函数吗?另外,创建的turtle到底是什么类型呢?我们已经了解整数、字符串、列表和布尔类型,这里面可没有turtle类型,这是个什么新类型吗?为了更深入的了解,我们可以使用下Python的帮助函数。

help(turtle.Turtle)

从这个描述中我们能看到很多之前没接触到的概念,Turtle并不是传统的类型,而是一个class,这个我们一般翻译成类。这就引出了我们本章节的下一个重要的概念:类和对象。

类和对象

什么是类和对象

类是建造对象的一个蓝图。类会告诉Python如何构造这种特定类型的对象。有这个类构造的各个对象可以有它自己的状态值。比如,可以使用Turtle类构造好几个不同的海龟,每个海龟有自己的颜色、大小、性状、位置和画笔设置(向上或者向下)等等。同时,由同一个类创建的所有海龟会共享相同的行为,而不需要重复去创建函数,比如前进、后退和转向这些功能。 类比到Scratch,我们角色的本体相当于一个Python中的类,通过这个本体,我们可以创造很多不同的克隆体,这个相当于Python中的对象。这里只是一个简单的类比,便于我们理解通过类可以创造很多个对象这样一个概念,当然随着学习的深入我们会发现Scratch中的角色和克隆体跟Python中的类和对象还是有一些差别的,这个暂时不展开啦。

类告诉我们对象有什么以及能够做什么

我们通过一个典型的游戏:飞机大战来讲解这个概念,在游戏中想要有很多各式各样的飞机,我们首先要有一个飞机类:

class Player(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.transform.scale(player_img,(50,40))
        self.image.set_colorkey(BLACK)

        self.rect = self.image.get_rect()
        self.radius = 23
        #pygame.draw.circle(self.image,RED,self.rect.center,self.radius)

        self.rect.centerx = WIDTH/2
        self.rect.bottom = HEIGHT - 20

        self.speedx = 8
        self.health = 100
        self.lives = 3
        self.hidden = False
        self.gun = 1
    
    def update(self):
        now = pygame.time.get_ticks()
        key_pressed = pygame.key.get_pressed()
        if key_pressed[pygame.K_RIGHT]:
            self.rect.x += self.speedx
        if key_pressed[pygame.K_LEFT]:
            self.rect.x -= self.speedx
        
        if self.rect.right > WIDTH:
            self.rect.right = WIDTH
        if self.rect.left < 0:
            self.rect.left = 0

        if self.hidden and now - self.hide_time > 1000:
            self.hidden = False
            self.rect.centerx = WIDTH/2
            self.rect.bottom = HEIGHT - 20
        
        if self.gun > 1 and now - self.gun_time > 5000:
            self.gun = 1

    def shoot(self):
        if not (self.hidden):
            if self.gun == 1:
                bullet = Bullet(self.rect.centerx,self.rect.centery)
                all_sprites.add(bullet)
                bullets.add(bullet)
                shoot_sound.play()
            elif self.gun >= 2:
                bullet1 = Bullet(self.rect.left,self.rect.centery)
                bullet2 = Bullet(self.rect.right,self.rect.centery)
                all_sprites.add(bullet1)
                all_sprites.add(bullet2)
                bullets.add(bullet1)
                bullets.add(bullet2)
                shoot_sound.play()

    def hide(self):
        self.hidden = True
        self.hide_time = pygame.time.get_ticks()
        self.rect.center = (WIDTH/2,HEIGHT+500)
    
    def gunup(self):
        self.gun = self.gun + 1
        self.gun_time = pygame.time.get_ticks()

在这个飞机类中我们看到了之后根据它可以生成的具体飞机对象的一些事情,主要是两种:对象有什么以及对象能够做什么。 对象有什么称为属性:也就是各种变量所代表的状态,比如血量、速度、生命等等; 对象能做什么称为方法:也就是各种函数所代表的功能,比如射击、移动等等; 这里重点提一下,不同编程语言可能会有些许差异,但通常情况下我们把类里面所定义的有特定含义的变量成为属性,类里面所定义的有特定含义的函数称为方法。

如何使用属性和方法

在使用类的属性和方法之前,首先我们要根据类来生成一个具体的对象,这个过程一般称为“实例化”:

slowpoke = turtle.Turtle()

虽然这里只有一行代码,但其实背后做了很多工作。 下面我们来具体分析一下这个代码。 首先我们要知道:我们在访问turtle模块中的Turtle类。这是这里使用点记法的原因:在这个语句中,点与属性或方法没有任何关系。 接下来我们要调用这个类,就好像它是一个函数一样。这里发生了什么?这里的工作是这样的:每个类都有一个特殊的方法,称为它的构造函数。构造函数可以建立对象,并设置它需要的所有默认属性值。构造函数通常还有另一个重要的功能:它会返回新创建的对象,也可以成为实例。所以这个过程也被叫做实例化。 所以创建、初始化和返回对象时,会把它赋值为变量slowpoke. 一旦有了一个对象,就可以自由地调用它的方法了:

slowpoke.turn(90)
slowpoke.forward(100)

那么属性呢?我们还没有见过获取或设置属性值的代码。要访问一个对象的一个属性,需要使用点记法。假设Turtle类有一个名为shape的属性(实际上并没有这个属性,不过暂且假设有),可以用如下方法访问或设置shape属性的值:

slowpoke.shape = 'turtle'
print(slowpoke.shape)

这里你可能会疑惑,我们确实一直用shape来设置海龟的性状,为什么Turtle对象没有一个shape属性呢?这是因为Turtle类经过很多年发展已经相当复杂了,使用了很多类和对象设计中的高级技巧,这其中有一个“封装”的概念,就是让工程师尽可能把对属性的直接修改放在类的代码中运行而不允许使用的人来修改,那使用的人就不能修改属性了吗?不是的,封装之后一般会提供一个函数,也就是方法来让使用的人可以修改属性,比如:

slowpoke.shape('circle')
print(slowpoke.shape())

当然,在刚开始学习这个概念时还没有必要封装的太多,比如我们飞机大战的源码中就可以比较简单的获取到飞机的属性。

类和方法无处不在

在了解了这么多类和对象的知识之后,我们回过头来重新审视Python世界,我们说Python是一个非常面向对象的语言,实际上对象就在你身边。来看下面的代码:

my_list = list()
my_list.append('first')
my_list.append('second')
my_list.reverse()
print(my_list)

列表实际上就是一个Python类,只不过为了方便理解,我们一般都是用my_list = []的形式来做初始化,或者说实例化。但其实Python在后台会默默的把这行代码重写为my_list = list()来执行。除了列表:其实字符串、数字都是Python内置的类,只不过不需要向普通类一样显式地调用构造函数来做实例化,这些工作Python在后台默默地帮你处理啦。下节课我们根据所学到的类和变量的知识来做一个小的游戏巩固一下这些知识。

海龟赛跑

规划游戏

  1. 创建游戏:创建一些海龟,每个海龟分别有自己的颜色,另外在起跑线上有不同的位置。
  2. 开始比赛:将变量winner设置为False。 while winner为False: For 每个乌龟: i. 选择一个随机的前进量,例如0~2之间的一个随机数,前进; ii. 查看乌龟的位置是否已经通过终点线,如果是:设置winner为True;
  3. 游戏结束:宣布获胜者的名字!

具体代码

import turtle
import random
turtles = list()
def setup():
    global turtles
    startline = -620
    screen = turtle.Screen()
    screen.setup(1290,720)
    screen.bgpic('pavement.gif')
    turtle_ycor = [-40, -20, 0, 20, 40]
    turtle_color = ['blue', 'red', 'purple', 'brown', 'green']
    for i in range(0, len(turtle_ycor)):
        new_turtle = turtle.Turtle()
        new_turtle.shape('turtle')
        new_turtle.penup()
        new_turtle.setpos(startline, turtle_ycor[i])
        new_turtle.color(turtle_color[i])
        new_turtle.pendown()
        turtles.append(new_turtle)
def race():
    global turtles
    winner = False
    finishline = 590
    while not winner:
        for current_turtle in turtles:
            move = random.randint(0,2)
            current_turtle.forward(move)
           
            xcor = current_turtle.xcor()
            if (xcor >= finishline):
                winner = True
                winner_color = current_turtle.color()
                print('The winner is', winner_color[0])
setup()
race()
turtle.mainloop()

奇怪的海龟

在类的高级用法中有一个是继承机制,这里我们以海龟赛跑简单讲解一下,我们已经有了一个海龟类,他有自己的前进方法,如果我们想要再设计一个跑的更快的海龟的话是不是得从头写一个新的海龟类呢?这种方法当然可以,但是是不是有点太麻烦了,类的继承机制指的就是我们可以以某个类为基类来创建新的类,比如这里我们以Turtle类为基类创建新的超级海龟,他们的不同点在于超级海龟重新设计了前进的方法:在原有前进的基础之上每次都多移动了5个像素,是不是就跑的更快了呢? 好的,本章节的内容到这里就结束了,我们讲解了Python中模块、类和对象的知识。下一个章节我们来引入新的知识:递归和字典。

import turtle
import random
turtles = list()
class SuperTurtle(turtle.Turtle):
    def forward(self, distance):
        cheat_distance = distance + 5
        turtle.Turtle.forward(self, cheat_distance)
def setup():
    global turtles
    startline = -620
    screen = turtle.Screen()
    screen.setup(1290,720)
    screen.bgpic('pavement.gif')
    turtle_ycor = [-40, -20, 0, 20, 40]
    turtle_color = ['blue', 'red', 'purple', 'brown', 'green']
    for i in range(0, len(turtle_ycor)):
        if i == 4:
            new_turtle = SuperTurtle()
        else:
            new_turtle = turtle.Turtle()
        #new_turtle = turtle.Turtle()
        new_turtle.shape('turtle')
        new_turtle.penup()
        new_turtle.setpos(startline, turtle_ycor[i])
        new_turtle.color(turtle_color[i])
        new_turtle.pendown()
        turtles.append(new_turtle)
def race():
    global turtles
    winner = False
    finishline = 590
    while not winner:
        for current_turtle in turtles:
            move = random.randint(0,2)
            current_turtle.forward(move)
            xcor = current_turtle.xcor()
            if (xcor >= finishline):
                winner = True
                winner_color = current_turtle.color()
                print('The winner is', winner_color[0])
setup()
race()
turtle.mainloop()
Last Updated:
Contributors: houlinsen, 爱博卡鲁