面向对象
面向对象是 Python 最重要的特性,在Python一切数据类型都是面向对象的。本章将介绍面向对象的基础知识,希望大家通过对本章的学习,对面向对象有深入的了解。
1. 面向过程式编程vs面向函数式编程
之前我们讲过面向过程式编程与面向函数式编程的区别,现在我们在回忆一下。
# 面向过程编程 测量对象的元素个个数。
s1 = 'fjdsklafsjda'
count = 0
for i in s1:
count += 1
l1 = [1,2,3,4]
count = 0
for i in l1:
count += 1
#函数式编程实现
def func(s):
count = 0
for i in s:
count += 1
return count
func('fdsafdsa')
func([1,2,3,4])
通过对比可知:函数编程较之面向过程编程最明显的两个特点:
- 减少代码的重用性。
- 增强代码的可读性。
2. 函数式编程vs面向对象编程
# 函数式编程
# auth 认证相关
def login():
pass
def regisgter():
pass
# account 账户相关
def func1():
pass
def func2():
pass
# 购物车相关
def shopping(username,money):
pass
def check_paidgoods(username,money):
pass
def check_unpaidgoods(username,money):
pass
def save(username,money):
pass
#面向对象式编程
class LoginHandler:
def login(self):
pass
def regisgter(self):
pass
class Account:
def func1(self):
pass
def func2(self):
pass
class ShoppingCar:
def shopping(username,money):
pass
def check_paidgoods(username,money):
pass
def check_unpaidgoods(username,money):
pass
def save(username,money):
pass
一、面向对象概述
面向对象的编程思想是,按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。
例如,在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动的是学生个体,即张同学、李同学等,这些具体的个体称为“对象”,对象也称为“实例”。
在现实世界有类和对象,软件世界也有面向对象,只不过它们会以某种计算机语言编写的程序代码形式存在,这就是面向对象编程(Object Oriented Programming OOP)。
二、面向对象三个基本特性
面向对象思想有3个基本特性:封装性、继承性和多态性。
2.1 封装性
在现实世界中封装的例子到处都是。例如,一台计算机内部极其复杂,有主板、 CPU 、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、 CPU 主频、硬盘和内存的大小,于是计算机制造商用机箱把计算机封装起来,对外提供一些接口,如鼠标、键盘和显示器等,这样当用户使用计算机时就变得非常方便。
面向对象的封装与真实世界的目的是一样的。封装能够使外部访问者不能随意存取对象的内部数据,隐藏了对象的内部细节,只保留有限的对外接口。外部访问者不用关心对象的内部细节,操作对象变得简单。
2.2 继承性
在现实世界中继承 是无处不在。例如轮船与客轮之间的关系,客轮是一种特殊的轮船,拥有轮船的全部特征和行为,即数据和操作。在面向对象中,轮船是一般类,客轮是特殊类,特殊类拥有一般类的全部数据和操作,称为特殊类继承一般类 。一般类称为“父类”或“超类”,特殊类称为“子类”或“派生类”。
2.3 多态性
多态性是指在父类中成员被子类继承之后,可以具有不同的状态或表现行为。
三、类和对象
Python中的数据类型都是类,类是组成 Python 程序的基本要素,它封装了一类对象的数据和操作。
3.1 定义类
Python语言中一个类的实现包括类定义和类体。类定义语法格式如下:
class 类名 [ (父类) ]:
类体
# class是声明类的关键字
# “类名”是自定义的类名,应该遵守Python命名规范,采用大驼峰命名法;
# “父类”声明当前类继承的父类,父类可以省略,表示直接继承object类
定义动物(Animal)类代码如下:
class Animal(object):
#类体
pass
上述代码声明了动物类,它继承了object类,object是所有类的根类,在 Python中任何一个动物类都直接或间接继承object,所以 object 部分代码可以省略。
3.2 创建和使用对象
类实例化可生成对象,所以“对象”也称为“实例”。
一个对象的生命周期包括三个阶段:创建、使用和销毁。
销毁对象时Python的垃圾回收机制释放不再使用的对象的内存,不需要程序员负责。程序员只关心创建和使用对象即可。
创建对象很简单,就是在类后面加上一对小括号,表示调用类的构造方法。代码如下:
animal = Animal()
# Animal是上面创建的类,Animal()表达式创建了一个动物对象;
# 并把创建的对象赋值给animal变量;
# animal是指向动物对象的一个引用。通过使用animal变量可以使用刚刚创建的对象
print(animal)
#输出结果如下:
<__main__.Animal object at 0x0000024A18CB90F0>
print 函数打印对象会输出一些很难懂的信息。事实上, print 函数调用了对象的str()
方法输出字符串信息,str()是object 类的一个方法,它会返回有关该对象的描述信息 ,由
于本例中 Animal 的str()方法是默认实现的 ,所以会返回这些难懂的信息,如果要打印
出友好的信息,需要重写str()方法。
3.3 实例变量
在类体中可以包含类的成员,其中包括:成员变量、成员方法和属性,成员变量又可分为实例变量和类变量,成员方法又分为实例方法、类方法和静态方法。
在Python类成员中有attribute和property。
- attribute是类中保存数据的变量,如果需要对attribute进行封装,那么在类的外部为了访问这些attribute,往往会提供一些setter和getter访问器。setter访问器是对attribute赋值的方法,getter访问器是取attribute值的方法;这些方法在创建和调用时都比较麻烦,于是Python又提供了property;
- propery本质上就是setter和getter访问器,是一种方法。一般情况下attribute和property中文都翻译为“属性”,这样很难区分两者的含义。
“实例变量”就是某个实例(或对象)个体特有的“数据”,例如你家狗狗的名字、年龄和性别与邻居家狗狗的名字、年龄和性别是不同的。
Python中定义实例变量的示例代码如下:
class Animal(object): #1
"""定义动物类"""
def __init__(self, age, sex, weight): #2
self.age = age #定义年龄实例变量 #3
self.sex = sex #定义性别实例变量
self.weight = weight #定义体重实例变量
animal = Animal(2, 1, 10.0)
print('年龄:{}'.format(animal.age)) #4
print('性别:{}'.format('雌性' if animal.sex == 0 else '雄性'))
print('体重:{}'.format(animal.weight))
#输出结果
年龄:2
性别:雄性
体重:10.0
- 代码1处是定义Animal动物类
- 代码2处是构造方法,构造方法是用来创建和初始化实例变量的,构造方法中的self指向当前对象实例的引用
- 代码3处是在创建和初始化实例变量age,其中self.age表示对象的age实例变量
- 代码4处是访问age实例变量,实例变量需要通过“实例名.实例变量”的形式访问。
3.4 类变量
“类变量”是所有实例(或对象)共有的变量。例如有一个Account(银行账户)类,它有三个成员变量:amount(账户金额)、interest_rate(利率)和owner(账户名)。在这三个成员变量中,amount和owner会因人而异,对于不同的账户这些内容是不同的,而所有账户的interest_rate都是相同的。amount和owner成员变量与账户个体实例有关,称为“实例变量”,interest_rate成员变量与个体实例无关,或者说是所有账户实例共享的,这种变量称为“类变量”。
类变量示例代码如下:
class Account:
"""定义银行账户类"""
interest_rate = 0.0668 # 类变量利率 #1
def __init__(self, owner, amount):
self.owner = owner #定义实例变量账户名
self.amount = amount #定义实例变量账户金额
account = Account('Tony', 1800000.0)
print('账户名: {}'.format(account.owner)) #2
print('账户金额: {}'.format(account.amount))
print('利率: {}'.format(Account.interest_rate)) #3
#输出结果
账户名: Tony
账户金额: 1800000.0
利率: 0.0668
- 代码1处是创建并初始化类变量。创建类变量和实例变量不同,类变量要在方法之外定义
- 代码2处是访问实例变量,通过“实例名.实例变量”的形式访问
- 代码3处是访问类变量,通过“类名.类变量”的形式访问。“类名.类变量”事实上是有别于包和模块的另外一种形式的命名空间。
注意:不要通过实例存取类变量数据。当通过实例读取变量时,Python解释器会先在实例中找这个变量,如果没有再到类中去找;当通过实例为变量赋值时,无论类中是否有该同名变量,Python解释器都会创建一个同名实例变量。
在类变量实例中添加如下代码:
print('Account 利率:{}'.format(Account.interest_rate))
print('ac1 利率:{}'.format(account.interest_rate)) #1
print('ac1实例所有变量:{}'.format(account.__dict__)) #2
account.interest_rate = 0.01 #3
account.interest_rate2 = 0.01 #4
print('ac1实例所有变量:{}'.format(account.__dict__)) #5
#输出结果如下:
Account 利率:0.0668
ac1 利率:0.0668
ac1实例所有变量:{'owner': 'Tony', 'amount': 1800000.0}
ac1实例所有变量:{'owner': 'Tony', 'amount': 1800000.0, 'interest_rate': 0.01, 'interest_rate2': 0.01}
提示:在代码3处和4处能够在类之外创建实例变量,主要原因是Python的动态语言特性,Python不能从语法层面禁止此事的发生。这样创建实例变量会引起很严重的问题,一方面,类的设计者无法控制一个类中有哪些成员变量;另一方面,这些实例变量无法通过类中的方法访问。
3.5 构造方法
上面例子中使用的init()方法是用来创建和初始化实例变量的,这种方法就是构造方法。init()方法也属于魔法方法。定义时第一个参数应该是self,其后的参数才是用来初始化实例变量的。调用构造方法时不需要传入self。
构造方法实例代码如下:
#!/usr/bin/python3
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0): #1
self.age = age #定义年龄实例变量
self.sex = sex #定义性别实例变量
self.weight = weight #定义体重实例变量
a1 = Animal(2, 1, 10.0) #2
a2 = Animal(1, weight=5.0)
a3 = Animal(1, sex=0) #3
print('a1年龄:{}'.format(a1.age)) #4
print('a3性别:{}'.format('雌性' if a3.sex == 0 else '雄性'))
print('a2体重:{}'.format(a2.weight))
#输出结果如下:
a1年龄:2
a3性别:雌性
a2体重:5.0
- 代码1处是定义构造方法,其中参数除了self外,其他的参数可以有默认值,这也提供了默认值的构造方法,能够给调用者提供多个不同形式的构造方法
- 代码2处和3处是调用构造方法创建Animal对象,其中不需要传入self,只需要提供后面的三个实际参数
3.6 实例方法
实例方法和实例变量一样都是某个实例(或对象)个体特有的。
方法是在类中定义的函数。而定义实例方法时它的第一个参数也应该是self,这个过程是将当前实例与该方法绑定起来,使该方法成为实例方法。
定义实例方法实例:
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0):
self.age = age #定义年龄实例变量
self.sex = sex #定义性别实例变量
self.weight = weight #定义体重实例变量
def eat(self): #1
self.weight += 0.05
print('eat....')
def run(self): #2
self.weight -= 0.01
print('run....')
a1 = Animal(2, 0, 10.0)
print('a1体重:{}'.format(a1.weight))
a1.eat() #3
print('a1体重:{}'.format(a1.weight))
a1.run() #4
print('a1体重:{}'.format(a1.weight))
#运行结果如下:
a1体重:10.0
eat....
a1体重:10.05
run....
a1体重:10.04
- 代码1和2处声明了两个方法,其中第一个参数是self。
- 代码3和4处是调用这些实例方法,注意其中不需要传入self参数。
3.7 类方法
“类方法”与“类变量”类似,是属于类不属于个体实例的方法,类方法不需要与实例绑定,但需要与类绑定,定义时它的第一个参数不是self,而是类的type实例。type是描述Python数据类型的类,Python中所有数据类型都是type的一个实例。
定义类方法示例代码如下:
class Account:
"""定义银行账户类"""
interest_rate = 0.0668 #类变量:利率
def __init__(self, owner, amount):
self.owner = owner #定义实例变量:账户名
self.amount = amount #定义实例变量:账户金额
# 类方法
@classmethod
def interest_by(cls, amt): #1
return cls.interest_rate * amt #2
interest = Account.interest_by(12000.0) #3
print('计算利息:{}'.format(interest))
#运行结果如下:
计算利息:801.6
- 定义类方法有两个关键:
- 第一,方法第一个参数cls是type类型,是当前Account类型的实例;
- 第二,方法使用装饰器@classmethod声明该方法是类方法。
- 代码2处是方法体,在类方法中可以访问其他的类变量和类方法,cls.interest_rate是访问类方法interest_rate。注意:类方法可以访问类变量和其他类方法,但不能访问其他实例方法和实例变量
- 代码3处是调用类方法interest_by(),采用“类名.类方法”形式调用。从语法角度可以通过实例调用类方法,但这不符合规范。
3.8 静态方法
如果定义的方法既不想与实例绑定,也不想与类绑定,只是想把类作为它的命名空间,那么可以定义静态方法。
定义静态方法示例代码如下:
class Account:
"""定义银行账户类"""
interest_rate = 0.0668 #类变量:利率
def __init__(self, owner, amount):
self.owner = owner #定义实例变量:账户名
self.amount = amount #定义实例变量:账户金额
# 类方法
@classmethod
def interest_by(cls, amt):
return cls.interest_rate * amt
# 静态方法
@staticmethod
def interest_with(amt): #1
return Account.interest_by(amt) #2
interest1 = Account.interest_by(12000.0)
print('计算利息:{:.4f}'.format(interest1))
interest2 = Account.interest_with(12000.0)
print('计算利息:{:.4f}'.format(interest2))
#运行结果如下:
计算利息:801.6000
计算利息:801.6000
- 代码1处是定义静态方法,使用了@staticmethod装饰器,声明方法是静态方法,方法参数不指定self和cls。
- 代码2处调用了类方法。调用静态方法与调用类方法类似,都通过类名实现,但也可以通过实例调用。
- 类方法与静态方法在很多场景是类似的,只是在定义时有一些区别。类方法需要绑定类,静态方法不需要绑定类,静态方法与类的耦合度更加松散。在一个类中定义静态方法只是为了提供一个基于类名的命名空间。
四、封装性
封装性是面向对象的三大特性之一,Python语言没有与封装性相关的关键字,它通过特定的名称实现对变量和方法的封装。
4.1 私有变量
默认情况下Python中的变量是公有的,可以在类的外部访问它们。如果想让它们成为私有变量,可以在变量前加上双下划线“__”。
示例代码如下:
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0):
self.age = age #定义年龄实例变量
self.sex = sex #定义性别实例变量
self.__weight = weight #定义体重实例变量 #1
def eat(self):
self.__weight += 0.05
print('eat....')
def run(self):
self.__weight -= 0.01
print('run....')
a1 = Animal(2, 0, 10.0)
print('a1体重:{:.2f}'.format(a1.weight)) #2
a1.eat()
a1.run()
#运行结果如下:
Traceback (most recent call last):
File "7.py", line 19, in <module>
print('a1体重:{:.2f}'.format(a1.weight)) #2
AttributeError: 'Animal' object has no attribute 'weight'
- 代码1处在weight变量前加上双下划线,这会定义私有变量__weight,私有变量在类内部访问没有问题,但是在外部访问则会发生错误。
- Python中并没有严格意义上的封装,所谓的私有变量只是形式上的限制。如果想在类的外部访问这些私有变量也是可以的,这些双下划线“”开头的私有变量其实只是换了一个名字,它们的命令规律为“类名变量”,所以将上述代码a1.weight改成a1.Animal__weight就可以访问了,但这种访问方式并不符合规范,会破坏封装。Python的封装性靠的是程序员的自律,而非强制性的语法。
4.2 私有方法
私有方法与私有变量的封装是类似的,只要在方法前加上双下划线“__”就是私有方法了。
私有方法示例代码如下:
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0):
self.age = age #定义年龄实例变量
self.sex = sex #定义性别实例变量
self.__weight = weight #定义体重实例变量
def eat(self):
self.__weight += 0.05
self.__run()
print('eat....')
def __run(self): #1
self.__weight -= 0.01
print('run....')
a1 = Animal(2, 0, 10.0)
a1.eat()
a1.run() #2
#运行结构如下:
run....
eat....
Traceback (most recent call last):
File "8.py", line 22, in <module>
a1.run() #2
AttributeError: 'Animal' object has no attribute 'run'
- 代码1处run()方法是私有方法,run()方法可以在类的内部方法,不能在类的外部方法,否则会发生错误(代码2处)
- 如果一定要在类的外部访问私有方法也是可以的。与私有变量访问类似,命名规律为“类名_方法”。但这不符合规范,也会破坏封装。
4.3 定义属性
封装通常是对成员变量进行的封装。在严格意义上的面向对象设计中,一个类是不应该有公有的实例成员变量的,这些实例成员变量应该被设计为私有的,然后通过公有的setter和getter访问器访问。
使用setter和getter访问器的示例代码如下:
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0):
self.age = age #定义年龄实例成员变量
self.sex = sex #定义性别实例成员变量
self.__weight = weight #定义体重实例成员私有变量
def set_weight(self, weight): #1
self.__weight = weight
def get_weight(self): #2
return self.__weight
a1 = Animal(2, 0, 10.0)
print('a1 体重: {:.2f}'.format(a1.get_weight())) #3
a1.set_weight(123.45) #4
print('a1 体重: {:.2f}'.format(a1.get_weight()))
#运行结果如下:
a1 体重: 10.00
a1 体重: 123.45
- 代码1处中的set_weight()方法是setter访问器,它有一个参数,用来替换现有成员变量
- 代码2处的get_weight()方法是getter访问器
- 代码3处是调用getter访问器
- 代码4处是调用setter访问器
访问器形式的封装需要一个私有变量,需要提供getter访问器和一个setter访问器,只读变量不用提供setter访问器。
总之,访问器形式的封装在编写代码时比较麻烦。为了解决这个问题,Python中提供了属性(property),定义属性可以使用@property和@属性名.setter装饰器;@property用来修饰getter访问器,@属性名.setter用来修饰setter访问器。
使用属性修改前面的实例代码如下:
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0):
self.age = age #定义年龄实例成员变量
self.sex = sex #定义性别实例成员变量
self.__weight = weight #定义体重实例成员私有变量
@property
def weight(self): #替代get_weight(self): #1
return self.__weight
@weight.setter
def weight(self, weight): #替代set_weight(self, weight): #2
self.__weight = weight
a1 = Animal(2, 0, 10.0)
print('a1体重:{:.2f}'.format(a1.weight)) #3
a1.weight = 123.45 #如同a1.set_weight(123.45) #4
print('a1体重:{:.2f}'.format(a1.weight))
#运行结果如下:
a1体重:10.00
a1体重:123.45
- 代码1处定义属性getter访问器,使用了@property装饰器进行修饰,方法名就是属性名,这样就可以通过属性取值了(代码3处)
- 代码2处是定义属性setter访问器,使用了@weight.setter装饰器进行修饰,weight是属性名,与getter和setter访问器方法名保持一致,可以通过a1.weight = 123.45赋值(代码4处)
属性本质就是两个方法,在方法前加上装饰器使得方法成为属性。属性使用起来类似于公有变量,可以在赋值符“=”左边或右边,左边是被赋值,右边是取值。
提示:定义属性时应该先定义getter访问器,再定义setter访问器,否则会出现错误。这是因为@property修饰getter访问器时,定义了weight属性,这样在后面使用@weight.setter装饰器才是合法的。
五、继承性
类的继承性是面向对象语言的基本特性,多态性的前提是继承性。
5.1 继承概念
为了了解继承性,先看一个这样的场景:一位面向对象的程序员小赵,在编程过程中需要描述和处理个人信息,于是定义了类Person,如下所示:
class Person:
def __init__(self, name, age):
self.name = name #名字
self.age = age #年龄
def info(self):
template = 'Person [name={0}, age={1}]'
s = template.format(self.name, self.age)
return s
一周之后,小赵又遇到了新的需求,需要描述和处理学生信息,于是他又定义了一个新的类Student,如下所示:
class Student:
def __init__(self, name, age, school):
self.name = name #名字
self.age = age #年龄
self.school = school #所在学校
def info(self):
template = 'Student [name={0}, age={1}, school={2}]'
s = template.format(self.name, self.age, self.school)
return s
很多人会认为小赵的做法能够被理解并认为这是可行的,但问题在于Student和Person两个类的结构太接近了,后者只比前者多了一个school实例变量,却要重复定义其他所有的内容。Python提供了解决类似问题的机制,那就是类的继承,代码如下所示:
class Student(Person): #1
def __init__(self, name, age, school): #2
super().__init__(name, age) #3
self.school = school #所在学校 #4
- 代码1处是声明Student类继承Person类,其中小括号中的是父类,如果没有指明父类(一对空的小括号或省略小括号),则默认父类为object,object类是Python的根类。
- 代码2处定义构造方法,子类中定义构造方法时,首先要调用父类的构造方法,初始化父类实例变量
- 代码3处super().init(name,age)语句是调用父类的构造方法,super()函数是返回父类的引用,通过它可以调用父类中的实例变量和方法
- 代码4处是定义school实例变量
提示:子类基础父类时,只是继承父类中公有的成员变量和方法,不能继承私有的成员变量和方法。
5.2 重写方法
如果子类方法名与父类方法名相同,而且参数列表也相同,只是方法体不同,那么子类重写了(override)了父类的方法:
示例代码如下:
class Animal(object):
"""定义动物类"""
def __init__(self, age, sex=1, weight=0.0):
self.age = age #定义年龄实例成员变量
self.sex = sex #定义性别实例成员变量
self.weight = weight #定义体重实例成员变量
def eat(self): #1
self.weight += 0.05
print('动物吃....')
class Dog(Animal):
def eat(self): #2
self.weight += 0.1
print('狗狗吃....')
a1 = Dog(2, 0, 10.0)
a1.eat()
#输出结果如下:
狗狗吃....
- 代码1处是父类中定义eat()方法,子类继承父类并重写了eat()方法(代码2处)。
- 通过子类实例调用eat()方法时,会调用子类重写的eat()。
5.3 多继承
所谓多继承,就是一个子类有多个父类。大部分计算语言如Java、Swift等,只支持单继承,不支持多继承。主要是多继承会发生方法冲突。例如,客轮是轮船也是交通工具,客轮的父类是轮船和交通工具,如果两个父类都定义了run()方法,子类客轮继承哪一个run()方法呢?
Python支持多继承,但Python给出了解决方法名字冲突的方案。这个方案是:当子类实例调用一个方法时,先从子类中查找,如果没有找到则查找父类。父类的查找顺序是按照子类声明的父类列表从左到右查找,如果没有找到再找父类的父类,依次查找下去。
多继承示例代码如下:
class ParentClass1:
def run(self):
print('ParentClass1 run...')
class ParentClass2:
def run(self):
print('ParentClass2 run...')
class SubClass1(ParentClass1, ParentClass2):
pass
class SubClass2(ParentClass2, ParentClass1):
pass
class SubClass3(ParentClass1, ParentClass2):
def run(self):
print('SubClass3 run...')
sub1 = SubClass1()
sub1.run()
sub2 = SubClass2()
sub2.run()
sub3 = SubClass3()
sub3.run()
#输出结果如下:
ParentClass1 run...
ParentClass2 run...
SubClass3 run...
六、多态性
在面向对象程序设计中,多态是一个非常重要的特性,理解多态有利于进行面向对象的分析和设计。
6.1 多态概念
发生多态要有两个前提条件:
- 第一:继承。多态发生一定是子类和父类之间;
- 第二:重写。子类重写了父类的方法
下面通过一个示例解释什么是多态。
代码如下:
# 几何图形
class Figure:
def draw(self):
print('绘制Figure....')
# 椭圆形
class Ellipse(Figure):
def draw(self):
print('绘制Ellipse...')
# 三角形
class Triangle(Figure):
def draw(self):
print('绘制Triangle...')
f1 = Figure() #1
f1.draw()
f2 = Ellipse() #2
f2.draw()
f3 = Triangle() #3
f3.draw()
#输出结果如下:
绘制Figure....
绘制Ellipse...
绘制Triangle...
- 代码2处和3处符合多态的两个前提,因此会发生多态
- 代码1处不符合,没有发生多态
- 多态发生时,Python解释器根据引用指向的实例调用它的方法。
6.2 类型检查
和Java等静态语言相比,多态性对于动态语言Python而言意义不大。多态性优势在于运行期动态特性。无论多态性对Python影响多大,Python作为面向对象的语言多态性是存在的,这一点可以通过运行期类型检查证实,运行期类型检查使用isinstance(object,classinfo)函数,它可以检查object实例是否由classinfo类或classinfo子类所创建的实例。
修改上面的实例代码如下:
class Ellipse(Figure):
def draw(self):
print('绘制Ellipse...')
# 三角形
class Triangle(Figure):
def draw(self):
print('绘制Triangle...')
f1 = Figure() # 没发生多态
f1.draw()
f2 = Ellipse() #发生多态
f2.draw()
f3 = Triangle() #发生多态
f3.draw()
print(isinstance(f1, Triangle)) #False #1
print(isinstance(f2, Triangle)) #False
print(isinstance(f3, Triangle)) #False
print(isinstance(f2, Figure)) #True #2
- 代码2处,f2是Ellipse类创建的实例,Ellipse是Figure类的子类,所以这个表达式返回True,通过这样的表达式可以判断是否发生了多态。
- 还有一个类似的函数issubclass(class, classinfo),可以用来检查class是否是classinfo的子类
print(issubclass(Ellipse, Triangle)) #False
print(issubclass(Ellipse, Figure)) #True
print(issubclass(Triangle, Ellipse)) #False
6.3 鸭子类型
多态性对于动态语言意义不是很大,在动态语言中有一种类型检查称为“鸭子类型”,即一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那它就可以被当做鸭子。鸭子类型不关注变量的类型,而是关注变量具有的方法。鸭子类型像多态一样工作,但是没有继承,只要像“鸭子”一样的行为(方法)就可以了。
鸭子类型示例代码如下:
class Animal:
def run(self):
print('动物跑...')
class Dog(Animal):
def run(self):
print('狗狗跑...')
class Car:
def run(self):
print('汽车跑...')
def go(animal): #接收参数是Animal #1
animal.run()
go(Animal())
go(Dog())
go(Car()) #2
# 运行结果如下
动物跑...
狗狗跑...
汽车跑...
- Dog类继承了Animal类,而Car类与Animal类没有任何关系,只是都有run()方法;
- 代码1处定义的go()函数设计时考虑接收Animal类型参数,但是由于Python解释器不做任何的类型检查,所以可以传入任何的实际参数;
- 代码2处,当给go()函数传入Car实例时,它可以正常执行,这就是“鸭子类型”。
在Python这样的动态语言中使用“鸭子类型”替代多态性设计,能够充分地发挥Python动态语言特点,但是也给软件设计者带来了困难,对程序员的要求也非常高。
七、Python根类——object
Python所有类都直接或间接继承自object类,它是所有类的“祖先”。object类有很多方法。这里重点介绍两个方法:
- str(): 返回该对象的字符串表示;
- eq(other):指示其他某个对象是否与此对象“相等”。
这些方法都是需要在子类中重写的,下面就详细解释一下它们的用法。
str()方法
为了日志输出等处理方便,所有的对象都可以输出自己的描述信息。因此,可以重写str()方法。如果没有重写str()方法,则默认返回对象的类名,以及内存地址等信息,例如下面的信息:
<__main__.Person object at 0x000001FE0F349AC8>
下面开一个示例,重写str()方法代码如下:
class Person:
def __init__(self, name, age):
self.name = name #名字
self.age = age #年龄
def __str__(self): #1
template = 'Person [name={0}, age={1}]'
s = template.format(self.name, self.age)
return s
person = Person('Tony',18)
print(person) #2
#运行结果如下:
Person [name=Tony, age=18]
- 代码1处重写了str()方法。返回什么样的字符串完全是自定义的,只要能够表示当前类和当前对象即可;
- 代码2处是打印person对象,print()函数会将对象的str()方法返回字符串,并打印输出。
7.2 对象比较方法
“”运算符是用来比较两个对象的内容是否相等,当使用运算符“”比较两个对象时,在对象的内部是通过eq()方法进行比较的。
两个人(Person对象)相等是指什么?是名字?是年龄?问题的关键是需要指定相等的规则,就是要指定比较的是哪些实例变量相等。所以为了比较两个Person对象是否相等,则需要重写eq()方法,在该方法中指定比较规则。
修改Person代码如下:
class Person:
def __init__(self, name, age):
self.name = name #名字
self.age = age #年龄
def __str__(self):
template = 'Person [name={0}, age={1}]'
s = template.format(self.name, self.age)
return s
def __eq__(self, other): #1
if self.name == other.name and self.age == other.age: #2
return True
else:
return False
p1 = Person('Tony', 18)
p2 = Person('Tony', 18)
print(p1 == p2) #True
- 代码1处重写了Person类eq()方法;
- 代码2处提供了比较规则,即只有是name和age都相等才认为是两个对象相等;
- 为了比较可以不重写eq()方法,那么p1 == p2 为False。
八、枚举类
枚举是用来管理一组相关的有限个数常量的集合,使用枚举可以提高程序的可读性,使代码更清晰且更易于维护。在Python中提供枚举类型。它本质上是一种类。
8.1 定义枚举类
在Python中定义枚举类的语法格式如下:
class 枚举类名(enum.Enum):
枚举常量列表
枚举类继承自enum.Enum类。枚举中会定义多个常量成员。枚举类WeekDays具体代码如下:
import enum
class WeekDays(enum.Enum): #1
# 枚举常量列表
MONDAY = 1 #2
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 10 #3
day = WeekDays.FRIDAY #4
print(day) # WeekDays.FRIDAY
print(day.value) # 10
print(day.name) # FRIDAY
#输出结果
WeekDays.FRIDAY
10
FRIDAY
- 代码1处是定义WeekDays枚举类,它有5个常量成员,每一个常量成员值都需要进行初始化(代码2和3处);
- 代码4处是实例化枚举类WeekDays,该实例为FRIDAY。注意枚举类实例化与类不同,枚举类不能调用构造方法;
- 枚举实例value属性是返回枚举值,name属性返回枚举名;
- 常量成员值可以是任意类型,多个成员的值也可以相同。
8.2 限制枚举类
为了存储和使用方便,枚举类中的常量成员取值应该是整数,而且每一常量成员应该有不同的取值。为了使枚举类常量成员只能使用整数类型,可以使用enum.IntEnum作为枚举父类。为了防止常量成员值重复,可以为枚举类加上@enum.unique装饰器。
具体代码如下:
import enum
@enum.unique #1
class WeekDays(enum.IntEnum): #2
# 枚举常量列表
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
day = WeekDays.FRIDAY
print(day)
print(day.value)
print(day.name)
#运行结果如下:
WeekDays.FRIDAY
5
FRIDAY
- 代码1处是WeekDays枚举类的装饰器,值不能重复;
- 代码2处是定义枚举类WeekDays,其父类是enum.IntEnum;
- 如果修改成员值为其他数据类型或修改为相同值,则会发生异常。
8.3 使用枚举类
定义枚举类的主要目的是提高程序可读性,特别是在比较时,枚举类非常实用。
示例代码如下:
import enum
@enum.unique
class WeekDays(enum.IntEnum):
# 枚举常量列表
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
day = WeekDays.FRIDAY
if day == WeekDays.MONDAY: #1
print('工作')
elif day == WeekDays.FRIDAY: #2
print('学习')
#程序运行结果
学习
- 代码1处比较day是否为星期一
- 代码2处比较day是否为星期五
- 从中可见,使用枚举成员要好于使用1和5这种无意义的数值。
九、实战案例
9.1 设计药品medicine类
电影《我不是药神》上映后,口碑极高,一种名为“格列宁”的进口药为人们所熟知,医药话题也引起了人们热烈的讨论。下面按照要求定义一个药品medicine类。
medicine类属性如下:
- 药名 name
- 价格 price
- 生产日期 PD
- 失效日期 Exp
medicine类方法如下:
- 获取药品名称 get_name()返回类型:str
- 计算保质期(失效日期和生产日期的间隔时间) get_GP()返回类型:str
程序代码如下:
from datetime import datetime
class Medicine:
def __init__(self, name, price, PD, Exp):
self.name = name
self.price = price
self.PD = PD
self.Exp = Exp
def get_name(self):
return self.name
def get_GP(self):
start = datetime.strptime(self.PD, '%Y-%m-%d')
end = datetime.strptime(self.Exp, '%Y-%m-%d')
return (end - start).days
medicine = Medicine(name='格列宁', price = 18000, PD='2018-5-1', Exp='2020-5-1')
name = medicine.get_name()
GP = medicine.get_GP()
print(f'药品名称:{name}')
print(f'药品保质期:{GP}天')
9.2 模拟银行账户资金交易管理
用类和对象实现一个银行账户的资金交易管理,包括存款、取款和打印交易详情,交易详情中包含每次交易的时间、存款或者取款的金额、每次交易后的余额。
交易日期 | 摘要 | 金额 | 币种 | 余额 |
---|---|---|---|---|
2018-09-10 | 转入 | +80.02 | 人民币 | 512.33 |
2018-09-06 | 消费 | -100.00 | 人民币 | 32.31 |
2018-09-04 | 网转 | -721.00 | 人民币 | 132.31 |
2018-09-04 | 消费 | -17.00 | 人民币 | 853.31 |
2018-09-03 | 转入 | -11.00 | 人民币 | 870.31 |
程序代码如下:
import time
import prettytable as pt #请先安装prettytable模块:pip install prettytable
balance = 0 # 初始化金额
acount_log = [] # 初始化交易日志
class Bank:
def __init__(self):
"""初始化"""
global balance
self.balance = balance
self.acount_log = acount_log
def deposit(self):
"""存款"""
amount = float(input('请输入存款金额:'))
self.balance += amount
self.write_log(amount,'转入')
def withdrawl(self):
"""取款"""
amount = float(input('请输入取款金额:'))
#判断金额
if amount > self.balance:
print('余额不足')
else:
self.balance -= amount
self.write_log(amount, '消费')
def print_log(self):
"""打印交易日志"""
tb = pt.PrettyTable()
tb.field_names = ['交易日期', '摘要', '金额', '币种', '余额'] #设置表格头
for info in self.acount_log:
# 判断转入还是消费,为金额前添加“+”或“-”
if info[1] == '转入':
amount = '+{}'.format(info[2])
else:
amount = '-{}'.format(info[2])
tb.add_row([info[0], info[1], amount,'人民币',info[3]])
print(tb)
def write_log(self,amount,handle):
"""写入日志"""
create_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) #获取创建时间
data = [create_time, handle, amount,self.balance] #组装列表
self.acount_log.append(data) #添加到日志列表
def show_menu():
"""显示菜单"""
menu = '''菜单
0: 退出
1: 存款
2: 取款
3: 打印交易详情
'''
print(menu)
if __name__ == "__main__":
show_menu()
num = float(input('请根据菜单输入操作编号:'))
bank = Bank()
while num != 0 :
if num == 1:
bank.deposit()
elif num == 2:
bank.withdrawl()
elif num == 3:
bank.print_log()
else:
print('您的输入有误!')
show_menu()
num = float(input('请根据菜单输入操作编号:'))
print('您已退出系统')