面向对象

面向对象是 Python 最重要的特性,在Python一切数据类型都是面向对象的。本章将介绍面向对象的基础知识,希望大家通过对本章的学习,对面向对象有深入的了解。

1. 面向过程式编程vs面向函数式编程

之前我们讲过面向过程式编程与面向函数式编程的区别,现在我们在回忆一下。

  1. # 面向过程编程 测量对象的元素个个数。
  2. s1 = 'fjdsklafsjda'
  3. count = 0
  4. for i in s1:
  5. count += 1
  6. l1 = [1,2,3,4]
  7. count = 0
  8. for i in l1:
  9. count += 1
  10. #函数式编程实现
  11. def func(s):
  12. count = 0
  13. for i in s:
  14. count += 1
  15. return count
  16. func('fdsafdsa')
  17. func([1,2,3,4])

通过对比可知:函数编程较之面向过程编程最明显的两个特点:

  1. 减少代码的重用性。
  2. 增强代码的可读性。

2. 函数式编程vs面向对象编程

  1. # 函数式编程
  2. # auth 认证相关
  3. def login():
  4. pass
  5. def regisgter():
  6. pass
  7. # account 账户相关
  8. def func1():
  9. pass
  10. def func2():
  11. pass
  12. # 购物车相关
  13. def shopping(username,money):
  14. pass
  15. def check_paidgoods(username,money):
  16. pass
  17. def check_unpaidgoods(username,money):
  18. pass
  19. def save(username,money):
  20. pass
  21. #面向对象式编程
  22. class LoginHandler:
  23. def login(self):
  24. pass
  25. def regisgter(self):
  26. pass
  27. class Account:
  28. def func1(self):
  29. pass
  30. def func2(self):
  31. pass
  32. class ShoppingCar:
  33. def shopping(username,money):
  34. pass
  35. def check_paidgoods(username,money):
  36. pass
  37. def check_unpaidgoods(username,money):
  38. pass
  39. def save(username,money):
  40. pass

一、面向对象概述

面向对象的编程思想是,按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。

例如,在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动的是学生个体,即张同学、李同学等,这些具体的个体称为“对象”,对象也称为“实例”。

在现实世界有类和对象,软件世界也有面向对象,只不过它们会以某种计算机语言编写的程序代码形式存在,这就是面向对象编程(Object Oriented Programming OOP)。

二、面向对象三个基本特性

面向对象思想有3个基本特性:封装性、继承性和多态性。

2.1 封装性

在现实世界中封装的例子到处都是。例如,一台计算机内部极其复杂,有主板、 CPU 、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、 CPU 主频、硬盘和内存的大小,于是计算机制造商用机箱把计算机封装起来,对外提供一些接口,如鼠标、键盘和显示器等,这样当用户使用计算机时就变得非常方便。

面向对象的封装与真实世界的目的是一样的。封装能够使外部访问者不能随意存取对象的内部数据,隐藏了对象的内部细节,只保留有限的对外接口。外部访问者不用关心对象的内部细节,操作对象变得简单。

2.2 继承性

在现实世界中继承 是无处不在。例如轮船与客轮之间的关系,客轮是一种特殊的轮船,拥有轮船的全部特征和行为,即数据和操作。在面向对象中,轮船是一般类,客轮是特殊类,特殊类拥有一般类的全部数据和操作,称为特殊类继承一般类 。一般类称为“父类”或“超类”,特殊类称为“子类”或“派生类”。

2.3 多态性

多态性是指在父类中成员被子类继承之后,可以具有不同的状态或表现行为。

三、类和对象

Python中的数据类型都是类,类是组成 Python 程序的基本要素,它封装了一类对象的数据和操作。

3.1 定义类

Python语言中一个类的实现包括类定义和类体。类定义语法格式如下:

  1. class 类名 [ (父类) ]:
  2. 类体
  3. # class是声明类的关键字
  4. # “类名”是自定义的类名,应该遵守Python命名规范,采用大驼峰命名法;
  5. # “父类”声明当前类继承的父类,父类可以省略,表示直接继承object类

定义动物(Animal)类代码如下:

  1. class Animal(object):
  2. #类体
  3. pass

上述代码声明了动物类,它继承了object类,object是所有类的根类,在 Python中任何一个动物类都直接或间接继承object,所以 object 部分代码可以省略。

3.2 创建和使用对象

类实例化可生成对象,所以“对象”也称为“实例”。

一个对象的生命周期包括三个阶段:创建、使用和销毁。

销毁对象时Python的垃圾回收机制释放不再使用的对象的内存,不需要程序员负责。程序员只关心创建和使用对象即可。

创建对象很简单,就是在类后面加上一对小括号,表示调用类的构造方法。代码如下:

  1. animal = Animal()
  2. # Animal是上面创建的类,Animal()表达式创建了一个动物对象;
  3. # 并把创建的对象赋值给animal变量;
  4. # animal是指向动物对象的一个引用。通过使用animal变量可以使用刚刚创建的对象
  5. print(animal)
  6. #输出结果如下:
  7. <__main__.Animal object at 0x0000024A18CB90F0>

print 函数打印对象会输出一些很难懂的信息。事实上, print 函数调用了对象的str()
方法输出字符串信息,str()是object 类的一个方法,它会返回有关该对象的描述信息 ,由
于本例中 Animal 的str()方法是默认实现的 ,所以会返回这些难懂的信息,如果要打印
出友好的信息,需要重写str()方法。

3.3 实例变量

在类体中可以包含类的成员,其中包括:成员变量、成员方法和属性,成员变量又可分为实例变量和类变量,成员方法又分为实例方法、类方法和静态方法。

11. 面向对象进阶 - 图1

在Python类成员中有attribute和property。

  • attribute是类中保存数据的变量,如果需要对attribute进行封装,那么在类的外部为了访问这些attribute,往往会提供一些setter和getter访问器。setter访问器是对attribute赋值的方法,getter访问器是取attribute值的方法;这些方法在创建和调用时都比较麻烦,于是Python又提供了property;
  • propery本质上就是setter和getter访问器,是一种方法。一般情况下attribute和property中文都翻译为“属性”,这样很难区分两者的含义。

实例变量”就是某个实例(或对象)个体特有的“数据”,例如你家狗狗的名字、年龄和性别与邻居家狗狗的名字、年龄和性别是不同的。

Python中定义实例变量的示例代码如下:

  1. class Animal(object): #1
  2. """定义动物类"""
  3. def __init__(self, age, sex, weight): #2
  4. self.age = age #定义年龄实例变量 #3
  5. self.sex = sex #定义性别实例变量
  6. self.weight = weight #定义体重实例变量
  7. animal = Animal(2, 1, 10.0)
  8. print('年龄:{}'.format(animal.age)) #4
  9. print('性别:{}'.format('雌性' if animal.sex == 0 else '雄性'))
  10. print('体重:{}'.format(animal.weight))
  11. #输出结果
  12. 年龄:2
  13. 性别:雄性
  14. 体重: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成员变量与个体实例无关,或者说是所有账户实例共享的,这种变量称为“类变量”。

类变量示例代码如下:

  1. class Account:
  2. """定义银行账户类"""
  3. interest_rate = 0.0668 # 类变量利率 #1
  4. def __init__(self, owner, amount):
  5. self.owner = owner #定义实例变量账户名
  6. self.amount = amount #定义实例变量账户金额
  7. account = Account('Tony', 1800000.0)
  8. print('账户名: {}'.format(account.owner)) #2
  9. print('账户金额: {}'.format(account.amount))
  10. print('利率: {}'.format(Account.interest_rate)) #3
  11. #输出结果
  12. 账户名: Tony
  13. 账户金额: 1800000.0
  14. 利率: 0.0668
  • 代码1处是创建并初始化类变量。创建类变量和实例变量不同,类变量要在方法之外定义
  • 代码2处是访问实例变量,通过“实例名.实例变量”的形式访问
  • 代码3处是访问类变量,通过“类名.类变量”的形式访问。“类名.类变量”事实上是有别于包和模块的另外一种形式的命名空间。

注意:不要通过实例存取类变量数据。当通过实例读取变量时,Python解释器会先在实例中找这个变量,如果没有再到类中去找;当通过实例为变量赋值时,无论类中是否有该同名变量,Python解释器都会创建一个同名实例变量。

在类变量实例中添加如下代码:

  1. print('Account 利率:{}'.format(Account.interest_rate))
  2. print('ac1 利率:{}'.format(account.interest_rate)) #1
  3. print('ac1实例所有变量:{}'.format(account.__dict__)) #2
  4. account.interest_rate = 0.01 #3
  5. account.interest_rate2 = 0.01 #4
  6. print('ac1实例所有变量:{}'.format(account.__dict__)) #5
  7. #输出结果如下:
  8. Account 利率:0.0668
  9. ac1 利率:0.0668
  10. ac1实例所有变量:{'owner': 'Tony', 'amount': 1800000.0}
  11. ac1实例所有变量:{'owner': 'Tony', 'amount': 1800000.0, 'interest_rate': 0.01, 'interest_rate2': 0.01}

提示:在代码3处和4处能够在类之外创建实例变量,主要原因是Python的动态语言特性,Python不能从语法层面禁止此事的发生。这样创建实例变量会引起很严重的问题,一方面,类的设计者无法控制一个类中有哪些成员变量;另一方面,这些实例变量无法通过类中的方法访问。

3.5 构造方法

上面例子中使用的init()方法是用来创建和初始化实例变量的,这种方法就是构造方法。init()方法也属于魔法方法。定义时第一个参数应该是self,其后的参数才是用来初始化实例变量的。调用构造方法时不需要传入self。

构造方法实例代码如下:

  1. #!/usr/bin/python3
  2. class Animal(object):
  3. """定义动物类"""
  4. def __init__(self, age, sex=1, weight=0.0): #1
  5. self.age = age #定义年龄实例变量
  6. self.sex = sex #定义性别实例变量
  7. self.weight = weight #定义体重实例变量
  8. a1 = Animal(2, 1, 10.0) #2
  9. a2 = Animal(1, weight=5.0)
  10. a3 = Animal(1, sex=0) #3
  11. print('a1年龄:{}'.format(a1.age)) #4
  12. print('a3性别:{}'.format('雌性' if a3.sex == 0 else '雄性'))
  13. print('a2体重:{}'.format(a2.weight))
  14. #输出结果如下:
  15. a1年龄:2
  16. a3性别:雌性
  17. 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类属性如下:

  1. 药名 name
  2. 价格 price
  3. 生产日期 PD
  4. 失效日期 Exp

medicine类方法如下:

  1. 获取药品名称 get_name()返回类型:str
  2. 计算保质期(失效日期和生产日期的间隔时间) 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('您已退出系统')