“笨办法”学Python 3基础篇系列文章
“笨办法”学Python 3基础篇 第一部分-打印与输入
“笨办法”学Python 3基础篇 第二部分-文件操作
“笨办法”学Python 3基础篇 第三部分-函数
“笨办法”学Python 3基础篇 第四部分-数据容器与程序结构
“笨办法”学Python 3基础篇 第五部分-面向对象的类
“笨办法”学Python 3基础篇 第六部分-项目骨架与自动测试
“笨办法”学Python 3基础篇 第七部分-搭建简易的网站
面向对象的类
前言
本节介绍面向对象的类的使用方法。以数据容器-字典为引子,通过对比字典的用法引出类的用法,从而进一步阐述基于类的继承和组合。
5.1 数据容器-字典
上一节简单总结了数据容器-列表的一些基本操作和应用,本节总结Python独有的一种数据容器(至少我在C++中没见过)- 字典。当使用列表时,我们是通过数值来获取列表中的项,譬如:
上图定义了名为hairs的列表,访问列表中元素的方法则是通过数值的方式:hairs[0]、hairs[1]、hairs[2]实现的。不同于列表,字典的访问方法是让你可以通过任何东西(不只是数值)找到元素。这样的特性就使得字典作为一种存储数据的容器能够更方便的使用。
5.1.1 键(Key)与值(Value)
字典采用键(Key)和值(Value)这一配对来实现元素的访问。其中,键值可以不局限于数值,还可以是字符串。使用者通过键(Key)在字典中查找对应的值(Value)。打个形象的比喻,这里的键(Key)就像是图书馆的索引书号,值(Value)就是对应的图书。
了解了字典的 “键-值” 概念后,我们就需要知道字典的基本语法结构:
字典名 = {key0: value0, key1: value1, key2: value2,…}
字典是通过一对“{ }”来定义的,每一项元素由键、冒号、值组成,元素和元素彼此之间用逗号隔开。值得注意的是,键值必须是不可变的,如字符串,数字,元组(这个也是一种数据容器,但是受保护的,不能修改元素)等,但不可以为列表。值可以取任何数据类型。
5.1.2 字典的简单操作
接下来,我们通过一段代码来熟悉字典的创建、访问、修改、删除功能。具体代码如下:
#字典的创建与访问
user0 = {'ID': 'Tom', 'age': 7, 'male/female': 'male'}
print("user0's ID is: ", user0['ID'])
#字典的修改
user0['age'] = 18
user0['weight'] = 150
print("Tom's age is revised as: ", user0['age'])
print("Tom's weight is added as: ", user0['weight'])
#字典的删除
print("Before del weight,the user0 is: ", str(user0))
del user0['weight']
print("After del weight, the user0 is: ", str(user0))
user0.clear()
print("After clear, the user0 is: ", str(user0))
del user0
print("After del, the user0 is: ", str(user0))
在这段代码中,第一行代码是根据字典的语法格式定义了一个名为user0的字典,该字典有三个键(字符串型):ID、age、male\female,对应的值分别为字符串和整数。第二行代码通过 “字典名[键]” 的方式访问了字典的ID键对应的值"Tom"。第三行代码则是通过键‘age’修改了对应的值,第四行代码则是通过添加一个新键’weight’增加了字典user0的元素。第五行、第六行代码分别打印出字典修改的值。第七行打印了没删除weight前的字典,通过str()将字典以可打印的字符串表示。第八到第十一行则分别执行了删除weight键-值和清空字典的命令,这里用到了del 字典[键] 和 字典.clear() 两个命令。最后两行则是删除了字典user0并打印user0,此时python会报错,因为当执行完del user0后,字典已经不存在了,此时再用print命令打印str(user0)会提示找不到字典。具体的执行结果为:
5.1.3 为口算训练器做一个简易登陆程序
一个简易的登陆程序应该包含如下功能:
- 欢迎界面,这个可以用输出来解决
- 创建一个字典存储账号和密码,用于登录账号匹配
- 循环结构遍历字典进行登陆账号判断和做出决定
- 登陆成功,进入口算界面
- 登陆失败,返回登陆界面继续等待输入
在这个登陆程序中,字典被用来程序的账号密码信息,并据此进行登陆信息的比对。具体程序代码为:
from sys import exit
def welcome():
print("""Welcome to Caculater.
Do you have an account? [Y/N]
""")
decide = input('> ')
if decide == 'Y':
return login()
else:
print("Please contact with the administrator!")
exit(0)
def login():
print("Please enter your ID and password: ")
id = input('ID> ')
passw = input('Password> ')
id_char = f"{id} {passw}"
account = {'user0': 'admin 123456', 'user1': 'guest1 000000', 'user2': 'guest2 111111'}
for key in account:
if id_char != account[key]:
continue
else:
print("Logining success!")
return gen()
print("Your account is not correct.")
return login()
def gen():
pass
welcome()
程序的主体由两个函数welcome和login组成。welcome函数主要是用分支结构来切换登陆界面或退出程序。login 函数功能为:
- 获取用户输入的账号和密码
- 将账号和密码按照字典的字符串存储格式进行格式化:账号+空格+密码
- 利用for循环,实现字典键的遍历,通过键访问字典中对应的账号和密码。在该程序中,账号是固定的,存储在字典account中,分别为账号admin,密码123456;账号guest1, 密码000000;账号guest2,密码111111
- 在键迭代过程中,通过分支结构来判断输入的账号和密码是否与字典中的账号密码匹配
- 匹配成功,退出循环(break),进入计算器界面(用了pass进行了gen()函数功能代码块的省略);匹配失败(continue完成循环),返回登陆界面,重新等待输入。
利用PowerShell运行,可得结果为:
5.2 面向对象的类
首先,抛砖引玉,我先按照我的理解来解释下类和对象(可能是并不是完全正确)。
5.2.1 什么是对象和类?
之前听说过在Python中有这么一句话:“万物皆对象。”一开始,不甚理解。随着慢慢的接触,后来明白过来。比如,简单的 a = 1 这样一个命令,Python完成的工作是创建一个值为1的整数对象,并且将这个对象赋值给变量a。因此,对象就好比我儿子幼儿园时使用的积木,小孩子搭积木不会关心积木的材质等内部细节,只关心积木的形状和大小是否是所需要的零件。我们在使用python编写程序时,做的就是把对象一个一个组合使用构成期望的功能,不必关心对象内部的细节。当然如果要创建自己的对象或者改变对象的内容时,就需要关心对象的内部细节。
对象既包含变量(属性)也包含函数(方法),是某一类具体事物的特殊实例。例如,整数1和整数2就是包含了加法、乘法之类方法的两个整数对象,它们都属于一个公共的类,即整数类。因此,类是对象的共有特性。更直观点的理解-类是图纸,图纸上有各种设计指标,而对象则是根据图纸生产出的产品。一张图纸可以生产出无数个产品出来,这些产品均具备相同的设计指标。因此,要生成对象的前提是一定要定义该对象对应的类。如果还是不理解,可以看下图。考试成绩是一个类,它有一定的属性。学生A成绩、学生B成绩、学生C成绩就是考试成绩这个类中生成的对象,他们都具备考试成绩这个类具备的数学、语文、英语三门考试成绩的属性。
5.2.2 类与对象的语法结构和用法
类的基本语法结构如下图所示:
#伪代码
class ClassName(object):
'类的帮助信息'
ClassSuite # 类成员,方法或数据属性
ObjectName = ClassName(params)
类的定义以关键字class开始,后接类名ClassName、(object)和冒号:。值得注意的是,(object)中的object代表了对象,是一种显示写法,在Python 3中可以省略。类体ClassSuite可以为类的成员(变量),方法(函数)或数据属性。通过 ObjectName = ClassName(params) 实现了基于ClassName这个类创建了名为ObjectName的对象,括号中的参数params是创建对象ObjectName的默认参数。是不是有点晕了(((φ(◎ロ◎;)φ)))?
接下来,我们以上文提到的考试成绩为例子,来创建考试成绩这个类和三个对象,具体代码为:
class Scores(object):
'考试成绩的类'
ScoreSum = 0
#初始化函数,math, chinese, english是创建对象时所需的默认参数
def __init__(self, math, chinese, english):
self.math = math #self.math代表正在创建的对象中的类变量math,后一个math代表创建对象时设置的默认参数
self.chinese = chinese
self.english = english
Scores.ScoreSum = math + chinese + english
def displayScore(self):
print(f"math: {self.math}, chinese: {self.chinese}, english{self.english}")
def displaySum(self):
print(f"Total score is: {Scores.ScoreSum}.")
#用Scores类创建3个对象,学生A、学生B、学生C
studentA = Scores(78, 82, 90) #创建学生A时,初始化学生A的数学、语文和应用成绩
studentB = Scores(88, 92, 90)
studentC = Scores(100, 92, 90)
print("学生A的各科成绩:")
studentA.displayScore() #不需要写self
studentA.displaySum()
print("学生B的各科成绩:")
studentB.displayScore()
studentB.displaySum()
print("学生C的各科成绩:")
studentC.displayScore()
studentC.displaySum()
在Scores这个类中,ScoreSum是一个类变量,它的值可以在这个类的所有生成的对象之间共享,也可以通过内部类或外部类使用Scores.ScoreSum访问。__ init__() 是Python中的一种特殊的函数名,称为类的构造函数或初始化方法,当创建这个类的对象时就会调用 __ init__()。在上述代码中,类的参数为self, math,chinese, english。最令人疑惑的是self参数,它是Python中特有的一个参数,指向类当前正在生成的对象,在定义类的函数时是必须有的,用于区分类函数和全局函数。但self不是Python关键字,你可以定义成任何你喜欢的标识符(大家都这么用,还是最好默认用self)。 类中所有变量的调用都是通过self这个对象+操作符"."+变量或函数实现的。
以studentB.displaySum()为例,另一个角度理解self的方法是了解Python的工作原理,实际上studentB.displaySum()可以看成displaySum(studentB),具体为:
- Python将self指向当前操作对象studentB
- 通过对象studentB找到对应的类Scores
- 在Scores类中找到该类函数displaySum(self),这里的self就是studentB
- 执行displaySum的代码块
运行PowerShell,结果为:
从结果可知,类变量ScoreSum是被studentA、studentB、studentC三个对象共享的一个类成员。因此,在每次定义一个对象时,ScoreSum的值形成了覆盖。最终从三个对象中分别调用ScoreSum时,得到的答案都是最后一次生成studentC对象时产生的值。
5.2.3 字典、模块、类对比
字典是一种数据容器,能够让我们通过键(key)来获取值(value),实现“从Y获取X”的功能。
模块是包含函数和变量的Python脚本,通过导入该脚本实现调用,通过 .操作符 访问模块中的函数和变量,实现“从Y获取X”的功能。
类也可以看成是一种数据容器,包含一组函数和数据,通过生成对象进行访问,也是通过 .操作符 访问类中的变量和函数,实现“从Y获取X”的功能。
为了方便对比,我做了下面的表格:
数据结构 | 特征 | 访问方法 |
---|---|---|
字典 | 通过键-值访问 | 字典名[键] |
模块 | import 导入 | 模块名 . 变量或函数 |
类 | 生成对象 | 对象 . 变量或函数 |
所以,综合对比字典、模块、类,可以发现Python里有一个非常常用的模式:
- 拿一个类似“键 - 值”风格的容器
- 通过“键”的名称获取其中的“值”
对于字典,键是一个字符串,获得值的方法是“[key]”;对于模块和函数,“键”是函数或者变量,获得“值”的方法是“.key”,但模块只能导入一次,而类生成的对象可以无数个,且互相独立互不干涉。
5.2.4 三种数据容器解决同一个问题
我们分别用字典、模块和类来实现同样的功能:
打印出一位名叫Tom的小朋友头发的颜色
具体代码如下:
创建一个名为pupil的字典:
pupil = {'name': 'Tom', 'hair': 'black', 'alter': 'brown'}
name = pupil['name']
color = pupil['hair']
color_alter = pupil['alter']
print(f"{name}'s hair is {color}.")
print(f"Change {name}'s hair color as {color_alter}")
创建一个名为pupil.py模块:
def change_color():
color = 'brown'
return color
name = 'Tom'
color = 'black'
通过import pupil.py实现数据容器的访问:
#导入pupil模块
import pupil
#访问pupil模块中的color变量
origin_color = pupil.color
print(f"{pupil.name}'s hair is {origin_color}.")
#访问pupil模块中的change_color函数
new_color = pupil.change_color()
print(f"Change {pupil.name}'s hair color as {new_color}")
创建一个名为pupil的类:
#pupil类定义
class pupil(object):
#生成对象时,初始化函数
def __init__(self):
self.name = 'Tom'
self.hair = 'black'
# 换头发颜色函数
def change(self):
self.hair = 'brown'
return self.hair
#生成一个名叫boy的pupil类对象,初始化采用默认__init__配置
boy = pupil()
print(f"{boy.name}'s hair is {boy.hair}.")
#通过对象boy调用pupil类中的change函数
new_color = boy.change()
print(f"Change {boy.name}'s hair color as {new_color}.")
通过PowerShell分别实现上述三种数据容器的结果均为:
三种数据容器,没有绝对的好用和不好用,怎么合理使用要靠代码喂出来,还得时间和精力总结经验。
5.3 类的继承与组合
很多书上都说要尽量避免使用继承,因为在面向对象编程中,继承会增加程序的复杂度(或者说时纵深度),相当于给自己挖坑,一不小心就会逻辑混乱,而多重继承更是要极力避免的。当有时使用继承又非常方便,比如通用的功能可以放在父类,减少代码冗余。继承可以看作是一把双刃剑。
5.3.1 什么是继承
继承就是用来指明一个类的大部分或全部功能都是从一个父类中获得的。父类和子类有3种交互方式:
- 子类上的动作完全等同于父类上的动作
- 子类上的动作完全覆盖了父类上的动作
- 子类上的动作部分替换了父类上的动作
5.3.1.1 继承方法1-隐性继承
首先看下面一段代码:
class pupil(object):
'小学生-父类'
def displayinf(self):
print("I'm a pupil.")
class boy(pupil):
'男生-子类'
pass
class girl(pupil):
'女生-子类'
pass
student1 = boy()
student2 = girl()
student1.displayinf()
student2.displayinf()
在这段代码中,我们通过
class 子类名(父类):
的格式实现了类的继承。父类为pupil,子类为boy和girl,子类的代码块使用了pass,是空类。但子类生成的对象student1和student2自动继承了父类pupil中的所有函数功能,即使子类中没有定义过相同的类函数。运行结果为:
I am a pupil.
I am a pupil.
5.3.1.2 继承方法2-显性覆盖
有时候,我们希望子类里的函数有不同于父类的行为,这时使用隐性继承就没有用了,需要在子类中定义一个同名的函数,实现覆盖父类。例如,仍然以小学生这个例子为例:
class pupil(object):
'小学生-父类'
def __init__(self, name, age):
self.name = name
self.age = age
def displayinf(self):
print("I'm a pupil. My name is", self.name)
class boy(pupil):
'男生-子类'
def displayinf(self):
print("I'm a boy. My name is", self.name)
class girl(pupil):
'女生-子类'
def displayinf(self):
print("I'm a girl. My name is", self.name)
student1 = boy('Tom', 12)
student2 = girl('Lily', 8)
student1.displayinf()
student2.displayinf()
子类boy对象student1调用类函数displayinf()时,调用的是子类boy的displayinf()函数,覆盖了父类pupil的displayinf()函数。代码运行结果为:
I’m a boy. My name is Tom
I’m a girl. My name is Lily
5.3.1.3 继承方法3-不完全覆盖
所谓的不完全覆盖是指在子类的同名类函数中调用父类的函数功能,需要用到super()。语法格式为: super(子类,self).父类函数或成员, 该功能能够知道子类的继承关系,并且访问到父类中的函数或成员。例如
class pupil(object):
'小学生-父类'
def __init__(self, name, age):
self.name = name
self.age = age
def displayinf(self):
print("I'm a pupil.")
class boy(pupil):
'男生-子类'
def displayinf(self):
super(boy, self).displayinf()
print("I'm also a boy. My name is", self.name)
class girl(pupil):
'女生-子类'
def displayinf(self):
super(girl, self).displayinf()
print("I'm also a girl. My name is", self.name)
student1 = boy('Tom', 12)
student2 = girl('Lily', 8)
student1.displayinf()
student2.displayinf()
在执行 student1.displayinf()时,python 先在boy类中找到了displayinf()类函数,然后执行super(boy, self).displayinf(),于是python回到boy的父类pupil中执行父类中的dispalyinf()。当完成打印功能后,继续返回子类boy中执行当前的打印功能。具体关系为:
PowerShell运行结果为:
5.3.2 类的组合
继承实际上解决的是“A是B”的问题,而组合则是解决了“A里有B”的关系。通过直接在一个类中使用别的类和模块,实现继承的功能。例如:
class pupil(object):
'小学生-父类'
def __init__(self, name, age):
self.name = name
self.age = age
def displayinf(self):
print("I'm a pupil.")
class boy(object):
'男生-类'
def __init__(self, name, age):
self.obj = pupil(name, age)
def displayinf(self):
self.obj.displayinf()
print("I'm also a boy. My name is", self.obj.name)
class girl(object):
'女生-类'
def __init__(self, name, age):
self.obj = pupil(name, age)
def displayinf(self):
self.obj.displayinf()
print("I'm also a girl. My name is", self.obj.name)
student1 = boy('Tom', 12)
student2 = girl('Lily', 8)
student1.displayinf()
student2.displayinf()
在这段代码中,我们通过在boy和girl类中的初始化函数 self. obj = pupil(name, age) 实现了在boy和girl类中拥有pupil类的对象 self.obj。于是想使用pupil中的任何功能时,只需要通过调用self.obj这个对象即可。
总结
本来还想写一些多重继承和案例的,但右下角提示已经2万多字符了,就省略不写了。面向对象是python程序设计的基础,而类是对象的基础。难点在于如何从问题出发总结出类关系图。这点是往后在学习别人代码时需要注意留心的。