15 Python面向对象编程

Python从设计之初就是一门面向对象语言,它提供一些语言特性支持面向对象编程。
创建对象是Python的核心概念,本章将介绍如何创建对象,以及多态、封装、方法和继承等概念。

理解面向对象

什么是面向对象编程

Python是一门面向对象编程语言,对面向对象语言编码的过程叫作面向对象编程。
面向对象编程(Object Oriented Programming, OOP)是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含数据和操作数据的函数。
面向对象程序设计把计算机程序视为一组对象的集合,每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
在Python中,所有数据类型都被视为对象,也可以自定义对象。自定义对象数据类型就是面向对象中的类(Class)的概念。

面向对象术语简介

在开始具体介绍面向对象技术之前,我们先了解一些面向对象的术语,以便在后续内容中碰到对应词时能明白这些术语的意思。

  • 类:用来描述具有相同属性和方法的对象的集合。类定义了集合中每个对象共有的属性和方法。对象是类的实例。
  • 类变量(属性):类变量在整个实例化的对象中是公用的。类变量定义在类中,且在方法之外。类变量通常不作为实例变量使用。类变量也称作属性。
  • 数据成员:类变量或实例变量用于处理类及其实例对象的相关数据。
  • 方法重写:如果从父类继承的方法不能满足子类的需求,就可以对其进行改写,这个过程称为方法的覆盖(Override),也称为方法的重写。
  • 实例变量:定义在方法中的变量只作用于当前实例的类。
  • 多态(Polymorphism):对不同类的对象使用同样的操作。
  • 封装(Encapsulation):对外部世界隐藏对象的工作细节。
  • 继承(Inheritance):即一个派生类(derived class)继承基类(base class)的字段和方法。继承允许把一个派生类的对象作为一个基类对象对待,以普通类为基础建立专门的类对象。
  • 实例化(Instance):创建一个类的实例、类的具体对象。
  • 方法:类中定义的函数。
  • 对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。

和其他编程语言相比,Python在尽可能不增加新语法和语义的情况下加入了类机制。
Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类、派生类可以覆盖基类中的任何方法、方法中可以调用基类中的同名方法。
对象可以包含任意数量和类型的数据。

类的定义与使用

1 类的定义

开始介绍前先看一个类的示例:

class MyClass(object):
    i = 123
    def f(self):
         return 'hello world'

由上面的代码可以得知,类定义的语法格式如下:

class ClassName(object):
 
 .
 .
 .
 

由代码片段和类定义我们看到,Python中定义类使用class关键字,class后面紧接着类名,如示例中的MyClass,类名通常是大写开头的单词;紧接着是(object),表示该类是从哪个类继承下来的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。类包含属性(相当于函数中的语句)和方法(类中的方法大体可以理解成函数)。
提示:在类中定义方法的形式和函数差不多,但不称为函数,而称为方法。方法的调用需要绑定到特定对象上,而函数不需要。我们后面会逐步接触方法的调用方式。

2 类的使用

本节简单讲述类的使用。以8.2.1小节的示例为例(别忘了写开头两行),保存并执行(程序编写完成后,需要将文件保存为后缀为.py的文件,在cmd命令窗口下执行.py文件):

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class MyClass(object):
    i = 123
    def f(self):
         return 'hello world'

use_class = MyClass()
print('调用类的属性:',use_class.i)
print('调用类的方法:',use_class.f())

执行结果如下:

调用类的属性: 123
调用类的方法: hello world

由输入代码中的调用方式可知,类的使用比函数调用多了几个操作,调用类时需要执行如下操作:

use_class = MyClass()

这步叫作类的实例化,即创建一个类的实例。此处得到的use_class变量称为类的具体对象。再看后面两行的调用:

print('调用类的属性:',use_class.i)
print('调用类的方法:',use_class.f())

这里第一行后的use_class.i用于调用类的属性,也就是我们前面所说的类变量。第二行后的use_class.f()用于调用类的方法。
在上面的示例中,在类中定义f()方法时带了一个self参数,该参数在方法中并没有被调用,是否可以不要呢?调用f()方法时没有传递参数,是否表示参数可以传递也可以不传递?
对于在类中定义方法的要求:在类中定义方法时,第一个参数必须是self。除第一个参数外,类的方法和普通函数没什么区别,如可以用默认参数、可变参数、关键字参数和命名关键字参数等。
对于在类中调用方法的要求:要调用一个方法,在实例变量上直接调用即可。除了self不用传递,其他参数正常传入。
类对象支持两种操作,即属性引用和实例化。属性引用的标准语法如下:

obj.name

语法中obj代表类对象,name代表属性。

深入类

将深入介绍类的相关内容,如类的构造方法和访问权限。

1 类的构造方法

在开始介绍前,我们对前面的示例做一些改动,代码如下:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class MyClass(object):
    i = 123
    def __init__(self, name):
         self.name = name

    def f(self):
         return 'hello,'+ self.name

use_class = MyClass('xiaoming')
print('调用类的属性:',use_class.i)
print('调用类的方法:',use_class.f())

程序执行结果如下:

调用类的属性: 123
调用类的方法: hello,xiaoming

若类的实例化语句写法和之前一样,即:

use_class = MyClass()

程序执行结果如下:

Traceback (most recent call last):
  File "D:/python/workspace/classdef.py", line 21, in 
    use_class = MyClass()
TypeError: __init__() missing 1 required positional argument: 'name'

从代码和输出结果看到,实例化MyClass类时调用了__init__()方法。这里就奇怪了,我们在代码中并没有指定调用__init__()方法,怎么会报__init__()方法错误呢?
在Python中,__init__()方法是一个特殊方法,在对象实例化时会被调用。__init__()的意思是初始化,是initialization的简写。这个方法的书写方式是:先输入两个下划线,后面接着init,再接着两个下划线。这个方法也叫构造方法。在定义类时,若不显式地定义一个__init__()方法,则程序默认调用一个无参的__init__()方法。比如以下两段代码的使用效果是一样的:
代码一:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class DefaultInit(object):
    def __init__(self):
         print('类实例化时执行我,我是__init__方法。')
    def show(self):
         print ('我是类中定义的方法,需要通过实例化对象调用。')

test = DefaultInit()
print('类实例化结束。')
test.show()

程序执行结果如下:

类实例化时执行我,我是__init__方法。
类实例化结束。
我是类中定义的方法,需要通过实例化对象调用。

代码二:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class DefaultInit(object):
    def show(self):
         print ('我是类中定义的方法,需要通过实例化对象调用。')

test = DefaultInit()
print('类实例化结束。')
test.show()

程序执行结果如下:

类实例化结束。
我是类中定义的方法,需要通过实例化对象调用。

由上面两段代码的输出结果看到,当代码中定义了__init__()方法时,实例化类时会调用该方法;若没有定义__init__()方法,实例化类时也不会报错,此时调用默认的__init__()方法。
在Python中定义类时若没有定义构造方法(__init__()方法),则在类的实例化时系统调用默认的构造方法。另外,__init__()方法可以有参数,参数通过init()传递到类的实例化操作上。

既然__init__()方法是Python中的构造方法,那么是否可以在一个类中定义多个构造方法呢?我们先看如下3段代码:

代码一:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class DefaultInit(object):
      def __init__(self):
            print('我是不带参数的__init__方法。')

DefaultInit()
print('类实例化结束。')

程序执行结果如下:

我是不带参数的__init__方法。
类实例化结束。

在只有一个__init__()方法时,实例化类没有什么顾虑。
代码二:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class DefaultInit(object):
    def __init__(self):
         print('我是不带参数的__init__方法。')

    def __init__(self, param):
         print('我是带一个参数的__init__方法,参数值为:',param)

DefaultInit('hello')
print('类实例化结束。')

程序执行结果如下:

我是带一个参数的__init__方法,参数值为: hello
类实例化结束。

由执行结果看到,调用的是带了一个param参数的构造方法,若把类的实例化语句更改为:

DefaultInit()
执行结果为:

Traceback (most recent call last):
  File "D:/python/workspace/classdef.py", line 59, in 
    DefaultInit()
TypeError: __init__() missing 1 required positional argument: 'param'

或更改为:

DefaultInit('hello', 'world')

执行结果为:

Traceback (most recent call last):
  File "D:/python/workspace/classdef.py", line 61, in 
    DefaultInit('hello', 'world')
TypeError: __init__() takes 2 positional arguments but 3 were given

由执行结果看到,实例化类时只能调用带两个占位参数的构造方法,调用其他构造方法都会报错。
代码三:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class DefaultInit(object):
    def __init__(self, param):
         print('我是带一个参数的__init__方法,参数值为:',param)

    def __init__(self):
         print('我是不带参数的__init__方法。')

DefaultInit()
print('类实例化结束。')

程序执行结果如下:

我是不带参数的__init__方法。
类实例化结束。

由执行结果看到,调用的构造方法除了self外,没有其他参数。若把类的实例化语句更改为如下:

DefaultInit('hello')

执行结果为:

Traceback (most recent call last):
  File "D:/python/workspace/classdef.py", line 60, in 
    DefaultInit('hello')
TypeError: __init__() takes 1 positional argument but 2 were given

或更改为:

DefaultInit('hello', 'world')

执行结果为:

Traceback (most recent call last):
  File "D:/python/workspace/classdef.py", line 61, in 
    DefaultInit('hello', 'world')
TypeError: __init__() takes 2 positional arguments but 3 were given

由执行结果看到,实例化类时只能调用带一个占位参数的构造方法,调用其他构造方法都会报错。
由以上几个示例我们得知:一个类中可定义多个构造方法,但实例化类时只实例化最后的构造方法,即后面的构造方法会覆盖前面的构造方法,并且需要根据最后一个构造方法的形式进行实例化。建议一个类中只定义一个构造函数。

2 类的访问权限

在类内部有属性和方法,外部代码可以通过直接调用实例变量的方法操作数据,这样就隐藏了内部的复杂逻辑,例如:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class Student(object):
    def __init__(self, name, score):
         self.name = name
         self.score = score

    def info(self):
         print('学生:%s;分数: %s' % (self.name, self.score))

stu = Student('xiaomeng',95)
print ('修改前分数:', stu.score)
stu.info()
stu.score=0
print ('修改后分数:', stu.score)
stu.info()

程序执行结果如下:

修改前分数:95
学生:xiaomeng;分数: 95
修改后分数:0
学生:xiaomeng;分数: 0

由代码和输出结果看到,在类中定义的非构造方法可以调用类中构造方法实例变量的属性,调用的方式为self.实例变量属性名,如代码中的self.name和self.score。可以在类的外部修改类的内部属性。如果要让内部属性不被外部访问,该怎么办呢?
要让内部属性不被外部访问,可以在属性名称前加两个下划线__。在Python中,实例的变量名如果以__开头,就会变成私有变量(private),只有内部可以访问,外部不能访问。据此,我们把Student类改一改:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class Student(object):
    def __init__(self, name, score):
         self.__name = name
         self.__score = score

    def info(self):
         print('学生:%s;分数: %s' % (self.__name, self.__score))

stu = Student('xiaomeng',95)
print('修改前分数:', stu.__score)
stu.info()
stu.__score = 0
print('修改后分数:',stu.__score)
stu.info()

程序执行结果如下:

Traceback (most recent call last):
  File "D:/python/workspace/classdef.py", line 81, in 
    print('修改前分数:', stu.__score)
AttributeError: 'Student' object has no attribute '__score'

由执行结果看到,我们已经无法从外部访问实例变量的属性__score了。这样有什么作用呢?
这样可以确保外部代码不能随意修改对象内部的状态,通过访问限制的保护,代码更加安全。比如上面的分数对象是一个比较重要的内部对象,如果外部可以随便更改这个值,大家都随便更改自己成绩表单中的分数,岂不是很混乱。
如果外部代码要获取类中的namescore怎么办呢?
在Python中,可以为类增加get_attrs方法,获取类中的私有变量,例如在上面的示例中添加get_score(name的使用方式类同)方法,代码如下:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class Student(object):
    def __init__(self, name, score):
         self.__name = name
         self.__score = score

    def info(self):
         print('学生:%s;分数: %s' % (self.__name, self.__score))

    def get_score(self):
         return self.__score

stu = Student('xiaomeng',95)
print('修改前分数:', stu.get_score())
stu.info()
print('修改后分数:',stu.get_score())
stu.info()

执行结果如下:

修改前分数: 95
学生:xiaomeng;分数: 95
修改后分数: 95
学生:xiaomeng;分数: 95

由执行结果看到,通过get_score方法已经可以正确得到类内部的属性值。
是否可以通过外部更改内部私有变量的值呢?
在Python中,可以为类增加set_attrs方法,修改类中的私有变量,如更改上面示例中的score属性值,可以添加set_score(name使用方式类同)方法,代码如下:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class Student(object):
    def __init__(self, name, score):
         self.__name = name
         self.__score = score

    def info(self):
         print('学生:%s;分数: %s' % (self.__name, self.__score))

    def get_score(self):
         return self.__score

    def set_score(self, score):
         self.__score = score

stu = Student('xiaomeng',95)
print('修改前分数:', stu.get_score())
stu.info()
stu.set_score(0)
print('修改后分数:',stu.get_score())
stu.info()

程序执行结果如下:

修改前分数: 95
学生:xiaomeng;分数: 95
修改后分数: 0
学生:xiaomeng;分数: 0

由程序执行结果看到,通过set_score方法正确更改了私有变量score的值。这里有个问题,原先stu.score=0这种方式也可以修改score变量,为什么要费这么大周折定义私有变量,还定义set_score方法呢?
在Python中,通过定义私有变量和对应的set方法可以帮助我们做参数检查,避免传入无效的参数,如对上面的示例更改如下:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class Student(object):
    def __init__(self, name, score):
         self.__name = name
         self.__score = score

    def info(self):
         print('学生:%s;分数: %s' % (self.__name, self.__score))

    def get_score(self):
         return self.__score

    if 0<=score<=100:
             self.__score = score
         else:
             print('请输入0 到100 的数字。')

stu = Student('xiaomeng',95)
print('修改前分数:', stu.get_score())
stu.info()
stu.set_score(-10)
print('修改后分数:',stu.get_score())
stu.info()

程序执行结果如下:

修改前分数: 95
学生:xiaomeng;分数: 95
请输入0 到100 的数字。
修改后分数: 95
学生:xiaomeng;分数: 95

由输出结果看到,调用set_score方法时,如果传入的参数不满足条件,就按照不满足条件的程序逻辑执行。
既然类有私有变量的说法,那么类是否有私有方法呢?
答案是肯定的,类也有私有方法。类的私有方法也是以两个下划线开头,声明该方法为私有方法,且不能在类外使用。私有方法的调用方式如下:

self.__private_methods

我们通过下面的示例进一步了解私有方法的使用:

#! /usr/bin/python3
# -*-coding:UTF-8-*-

class PrivatePublicMethod(object):
    def __init__(self):
         pass

    def __foo(self):          # 私有方法
         print('这是私有方法')

    def foo(self):            # 公共方法
         print('这是公共方法')
         print('公共方法中调用私有方法')
         self.__foo()
         print('公共方法调用私有方法结束')


pri_pub = PrivatePublicMethod()
print('开始调用公共方法:')
pri_pub.foo()
print('开始调用私有方法:')
pri_pub.__foo()

程序执行结果如下:

开始调用公共方法:
这是公共方法
公共方法中调用私有方法
这是私有方法
公共方法调用私有方法结束
开始调用私有方法:
Traceback (most recent call last):
File "D:/python/workspace/classdef.py", line 114, in
pri_pub.__foo()
AttributeError: 'PrivatePublicMethod' object has no attribute '__foo'

由输出结果看到,私有方法和私有变量类似,不能通过外部调用。

继承

面向对象编程带来的好处之一是代码的重用,实现重用的方法之一是通过继承机制。继承完全可以理解成类之间类型和子类型的关系。
在面向对象程序设计中,当我们定义一个class时,可以从某个现有的class继承,定义的新class称为子类(Subclass),而被继承的class称为****基类、父类或超类(Base class、Super class)**。
继承的定义如下:

class DerivedClassName(BaseClassName):
    
    .
    .
    

需要注意:继承语法class子类名(基类名)时,//基类名写在括号里,基本类是在定义类时,在元组中指明的。
在Python中,继承有以下特点:
(1)在继承中,基类的构造方法(__init__()方法)不会被自动调用,需要在子类的构造方法中专门调用。
(2)在调用基类的方法时需要加上基类的类名前缀,并带上self参数变量。区别于在类中调用普通函数时不需要带self参数。
(3)在Python中,首先查找对应类型的方法,如果在子类中找不到对应的方法,才到基类中逐个查找。
例如:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Animal(object):
    def run(self):
         print('Animal is running...')

上面定义了一个名为Animal的类,类中定义了一个run()方法直接输出(没有显式定义__init__()方法,会调用默认的构造方法)。在编写Dog和Cat类时,可以直接从Animal类继承,定义如下:

class Dog(Animal):
    pass

class Cat(Animal):
    pass

在这段代码片段中,对于Dog来说,Animal就是它的父类;对于Animal来说,Dog就是它的子类。Cat和Dog类似。
继承有什么好处?
继承最大的好处是子类获得了父类全部非私有的功能。由于在Animial中定义了非私有的run()方法,因此作为Animial的子类,Dog和Cat什么方法都没有定义,自动拥有父类中的run()方法。
执行以上代码:

dog = Dog()
dog.run()

cat = Cat()
cat.run()

程序执行结果如下:

Animal is running...
Animal is running...

由执行结果看到,子类中没有定义任何方法,但都成功执行了run()方法。当然,子类可以拥有一些自己的方法,比如在Dog类中增加一个eat方法:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Dog(Animal):
    def eat(self):
         print('Eating ...')

dog = Dog()
dog.run()
dog.eat()

以上代码执行结果如下:

Animal is running...
Eating ...

由执行结果看到,既执行了父类的方法,又执行了自己定义的方法。
子类不能继承父类中的私有方法,也不能调用父类的私有方法。父类的定义如下:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Animal(object):
    def run(self):
         print('Animal is running...')

    def __run(self):
         print('I am a private method.')

子类定义不变,执行如下调用语句:

dog = Dog()
dog.__run()
执行结果如下:

Traceback (most recent call last):
  File "D:/python/workspace/classextend.py", line 25, in 
    dog.__run()
AttributeError: 'Dog' object has no attribute '__run'

由执行结果看到,子类不能调用父类的私有方法,子类虽然继承了父类,但是调用父类的私有方法相当于从外部调用类中的方法,因而调用不成功。
对于父类中扩展的非私有方法,子类可以拿来即用,如在父类Animal中增加一个jump方法:

class Animal(object):
    def run(self):
         print('Animal is running...')

    def jump(self):
         print('Animal is jumpping....')

    def __run(self):
         print('I am a private method.')

上面我们增加了一个非私有的jump()方法,子类Dog和Cat保持原样,执行如下调用:

dog = Dog()
dog.run()
dog.jump()

cat = Cat()
cat.run()
cat.jump()

执行结果如下:

Animal is running...
Animal is jumpping....
Animal is running...
Animal is jumpping....

由执行结果看到,子类可以立即获取父类增加的非私有方法。

继承可以一级一级继承下来,就好比从爷爷到爸爸再到儿子的关系。所有类最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树,如图所示。
继承树

多态

继承可以帮助我们重复使用代码。但对于继承中的示例,无论是Dog还是Cat,调用父类的run()方法时显示的都是Animal is running…,如果想让结果显示为Dog is running…和Cat is running…,该怎么处理呢?
我们对Dog和Cat类做如下改进:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Dog(Animal):
    def run(self):
         print('Dog is running...')

class Cat(Animal):
def run(self):
    print('Cat is running...')

执行如下语句:

dog = Dog()
print('实例化Dog 类')
dog.run()

cat = Cat()
print('实例化Cat类')
cat.run()

执行结果如下:

实例化Dog 类
Dog is running...
实例化Cat类
Cat is running...
由执行结果看到,分别得到了Dog和Cat各自的running结果。
当子类和父类存在相同的run()方法时,子类的run()方法会覆盖父类的run()方法,在代码运行时总是会调用子类的run()方法,称之为多态。
多态来自于希腊语,意思是有多种形式。多态意味着即使不知道变量所引用的对象类型是什么,也能对对象进行操作,多态会根据对象(或类)的不同而表现出不同的行为。例如,我们在上面的Animal类中定义了run方法,Dog和Cat类分别继承Animal类,并且分别定义了自己的run方法,最后Dog和Cat调用的是自己定义的run方法。
为了更好地理解什么是多态,我们对数据类型再做一点说明。当我们定义一个类时,实际上就定义了一种数据类型。定义的数据类型和Python自带的数据类型(如str、list、dict)没什么两样。

a = list() # a 是list 类型
b = Animal() # b 是Animal类型
c = Dog() # c 是Dog 类型

下面用isinstance()方法判断一个变量是否是某个类型。

print('a 是否为list类型:', isinstance(a, list))
print('b 是否为Animal 类型:', isinstance(b, Animal))
print('c 是否为Dog 类型:', isinstance(c, Dog))

执行结果如下:

a 是否为list 类型: True
b 是否为Animal类型: True
c 是否为Dog 类型: True

由执行结果看到,a、b、c确实分别为list、Animal、Dog三种类型。我们再执行如下语句:

print('c 是否为Dog 类型:', isinstance(c, Dog))
print('c 是否为Animal 类型:',isinstance(c, Animal))

执行结果如下:

c 是否为Dog 类型: True
c 是否为Animal类型: True

由执行结果看到,c既是Dog类型又是Animal类型。这怎么理解呢?
因为Dog是从Animal继承下来的,当我们创建Dog的实例c时,我们认为c的数据类型是Dog,但c同时也是Animal, Dog本来就是Animal`的一种。
在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以看作是父类。但是反过来就不行,例如以下语句:

b = Animal()
print('b 是否为Dog 类型:', isinstance(b, Dog))

执行结果如下:

b 是否为Dog 类型:False

由输出结果看到,变量b是Animal的实例化对象,是Animal类型,但不是Dog类型,也就是Dog可以看成Animal,但Animal不可以看成Dog。
我们再看一个示例。编写一个函数,这个函数接收一个Animal类型的变量,定义并执行如下函数,执行时传入Animal的实例:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

def run_two_times(animal):
    animal.run()
    animal.run()

run_two_times(Animal())

执行结果如下:

Animal is running...
Animal is running...

若执行函数时传入Dog的实例,操作如下:

run_two_times(Dog())

得到执行结果如下:

Dog is running...
Dog is running...

若传入Cat的实例,操作如下:

run_two_times(Cat())

得到执行结果如下:

Cat is running...
Cat is running...

看上去没有什么特殊的地方,已经正确输出预期结果了,但是仔细想想,如果再定义一个Bird类型,也继承Animal类,定义如下:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Bird(Animal):
    def run(self):
         print('Bird is flying the sky...')

run_two_times(Bird())

程序执行结果如下:

Bird is flying the sky...
Bird is flying the sky...

由执行结果我们发现,新增的Animal子类不必对run_two_times()方法做任何修改。实际上,任何依赖Animal作为参数的函数或方法都可以不加修改地正常运行,原因就在于多态。
多态的好处是:当我们需要传入Dog、Cat、Bird等对象时,只需要接收Animal类型就可以了,因为Dog、Cat、Bird等都是Animal类型,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此传入的类型只要是Animal类或继承自Animal类,都会自动调用实际类型的run()方法。
多态的意思是:对于一个变量,我们只需要知道它是Animal类型,无须确切知道它的子类型,就可以放心调用run()方法。具体调用的run()方法作用于Animal、Dog、Cat或Bird对象,由运行时该对象的确切类型决定。
多态真正的威力在于:调用方只管调用,不管细节。当我们新增一种Animal的子类时,只要确保run()方法编写正确即可,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:对于扩展开放,允许新增Animal子类;对于修改封闭,不需要修改依赖Animal类型的run_two_times()等函数。
很多函数和运算符都是多态的,你写的绝大多数程序也可能是,即便你并非有意这样。只要使用多态函数和运算符,多态就会消除。唯一能够毁掉多态的是使用函数显式地检查类型,如type、isinstance函数等。如果有可能,就尽量避免使用这些毁掉多态的方式,重要的是如何让对象按照我们希望的方式工作,无论它是否是正确类型或类。

封装

前面我们讲述了Python对象中两个重点——继承和多态,这里将讲述第3个重点——封装
封装是全局作用域中其他区域隐藏多余信息的原则。听起来有些像多态,使用对象而不用知道其内部细节。它们都是抽象原则,都会帮忙处理程序组件而不用过多关心细节,就像函数一样。
封装并不等同于多态。多态可以让用户对不知道类(或对象类型)的对象进行方法调用,而封装可以不用关心对象是如何构建的,直接使用即可。
前面几节的示例基本都用到封装的思想,如前面定义的Student类中,每个实例都拥有各自的name和score数据。我们可以通过函数访问这些数据,如输出学生的成绩,可以如下定义并执行:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Student(object):
    def __init__(self, name, score):
         self.name = name
         self.score = score

std = Student('xiaozhi',90)
def info(std):
    print('学生:%s;分数: %s' % (std.name, std.score))
info(std)

执行结果为:

学生:xiaozhi;分数: 90

由输出结果看到,可以通过函数调用类并得到结果。
既然Student实例本身就拥有这些数据,要访问这些数据就没有必要从外面的函数访问,可以直接在Student类内部定义访问数据的函数,这样就把“数据”封装起来了。这些封装数据的函数和Student类本身是相关联的,我们称之为类的方法。于是就有了前面所写类的形式:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Student0(object):
    def __init__(self, name, score):
         self.name = name
    self.score = score

def info(self):
    print('学生:%s;分数: %s' % (self.name, self.score))

要定义一个方法,除了第一个参数是self外,其他参数和普通函数一样。要调用一个方法,在实例变量上直接调用即可。除了self不用传递,其他参数正常传入,执行如下语句:

stu = Student0('xiaomeng',95)

执行结果为:

学生:xiaomeng;分数: 95

这样一来,我们从外部看Student类,只需要知道创建实例需要给出的name和score,如何输出是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
封装的另一个好处是可以给Student类增加新方法,比如我们在类的访问权限中所讲述的get_score()方法和set_score()方法。使用这些方法时,我们无须知道内部实现细节,直接调用即可。

多重继承

上面讲述的是单继承,Python还支持多重继承。多重继承的类定义如下:

class DerivedClassName(Base1, Base2, Base3):
    
    .
    .
    

可以看到,多重继承就是有多个基类(父类或超类)。
需要注意圆括号中父类的顺序,若父类中有相同的方法名,在子类使用时未指定,Python会从左到右搜索。若方法在子类中未找到,则从左到右查找父类中是否包含方法。
继续以前面的Animal类为例,假设要实现4种动物:Dog(狗)、Bat(蝙蝠)、Parrot(鹦鹉)、Ostrich(鸵鸟)。
如果按照哺乳动物和鸟类分类,我们可以设计按哺乳动物分类的类层次图,如图8-2所示。如果按照“能跑”和“能飞”分类,我们可以设计按行为功能分类的类层次图,如图所示。
如果要把上面的两种分类都包含进来,就得设计更多层次:
哺乳类:包括能跑的哺乳类和能飞的哺乳类。

按哺乳动物分类的类层次图

按行为功能分类的类层次图

鸟类:包括能跑的鸟类和能飞的鸟类。这么一来,类的层次就复杂了。下图所示为更复杂的类层次图。
更复杂的类层次图

如果还要增加“宠物类”和“非宠物类”,类的数量就会呈指数增长,很明显这样设计是不行的。
正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计,设计代码如下:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Animal(object):
    pass

# 大类:
class Mammal(Animal):
    pass

class Bird(Animal):
    pass

# 各种动物:
class Dog(Mammal):
    pass

class Bat(Mammal):
    pass

class Parrot(Bird):
    pass

class Ostrich(Bird):
    pass

接下来,给动物加上Runnable和Flyable功能。我们先定义好Runnable和Flyable类:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Runnable(object):
    def run(self):
         print('Running...')

class Flyable(object):
    def fly(self):
         print('Flying...')

大类定义好后,对需要Runnable功能的动物添加对Runnable的继承,如Dog:

class Dog(Mammal, Runnable):
    pass

对需要Flyable功能的动物添加对Flyable的继承,如Bat:

class Bat(Mammal, Flyable):
    pass

这样,通过上面的多重继承,一个子类就可以继承多个父类,同时获得多个父类所有非私有功能。

获取对象信息

当我们调用方法时可能需要传递一个参数,这个参数类型我们知道,但是对于接收参数的方法,就不一定知道是什么参数类型了。我们该怎么得知参数的类型呢?
Python为我们提供了以下3种获取对象类型的方法。

1. 使用type()函数

我们前面已经学习过type()函数的使用,基本类型都可以用type()判断,例如:

>>> type(123)

>>> type('abc')

>>> type(None)

如果一个变量指向函数或类,用type()函数返回的是什么类型?在交互模式下输入:

>>> type(abs)

>>> type(pri_pub)   #上面定义的PrivatePublicMethod 类

由输出结果看到,返回的是对应的Class类型。
如果我们要在if语句中判断并比较两个变量的type类型是否相同,应如下操作:

>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False

通过操作我们看到,判断基本数据类型可以直接写int、str等。怎么判断一个对象是否是函数呢?
可以使用types模块中定义的常量,在交互模式下输入:

>>> import types
>>> def func():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

由执行结果看到,函数的判断方式需要借助types模块的帮助。

2. 使用isinstance()函数

要明确class的继承关系,使用type()很不方便,通过判断class的数据类型确定class的继承关系要方便得多,这个时候可以使用isinstance()函数。
例如,继承关系是如下形式:
object -> Animal -> Dog
即Animal继承object、Dog继承Animal。使用isinstance()可以告诉我们一个对象是否是某种类型。
例如,创建如下两种类型的对象:

>>> animal = Animal()
>>> dog = Dog()
对上面两种类型的对象,使用isinstance进行判断:
>>> isinstance(dog, Dog)
True

根据输出结果看到,dog是Dog类型,这个没有任何疑问,因为dog变量指向的就是Dog对象。接下来判断Animal类型,使用isinstance判断如下:

>>> isinstance(dog, Animal)
True

根据输出结果看到,dog也是Animal类型。
由此我们得知:尽管dog是Dog类型,不过由于Dog是从Animal继承下来的,因此dog也是Animal类型。换句话说,isinstance()判断的是一个对象是否为该类型本身,或者是否为该类型继承类的类型。
我们可以确信,dog还是object类型:

>>> isinstance(dog, object)
True
同时确信,实际类型是Dog类型的dog,同时也是Animal类型:
>>> isinstance(dog, Dog) and isinstance(dog, Animal)
True
不过animal不是Dog类型,这个我们在8.5节已经讲述过:
>>> isinstance(animal,Dog )
False

提醒一点,能用type()判断的基本类型也可以用isinstance()判断。这个可以自己进行验证。
isinstance()可以判断一个变量是否为某些类型中的一种,判断变量是否为listtuple的方式如下:

>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True

3. 使用dir()

如果要获得一个对象的所有属性和方法,就可以使用dir()函数。dir()函数返回一个字符串的list。例如,获得一个str对象的所有属性和方法的方式如下:

     >>> dir('abc')
     ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__',
'__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__',
'__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode',
'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier',
'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition',
'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title',
'translate', 'upper', 'zfill']

由输出结果看到,str对象包含许多属性和方法。

类的专有方法

我们前面讲述了类的访问权限、私有变量和私有方法,除了自定义私有变量和方法外,Python类还可以定义专有方法。专有方法是在特殊情况下或使用特别语法时由Python调用的,而不是像普通方法一样在代码中直接调用。本节讲述几个Python常用的专有方法。
看到形如__xxx__的变量或函数名就要注意,这在Python中是有特殊用途的。
__init__我们已经知道怎么用了,Python的class中有许多这种有特殊用途的函数,可以帮助我们定制类。下面介绍这种特殊类型的函数定制类的方法。

1.__str__

开始介绍之前,我们先定义一个Student类,定义如下:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Student(object):
    def __init__(self, name):
         self.name = name

print(Student('xiaozhi'))

执行结果如下:
<__main__.Student object at 0x0000000000D64198>
执行结果输出一堆字符串,一般人看不懂,没有什么可用性,也不好看。怎样才能输出得好看呢?
只需要我们定义好__str__()方法,返回一个好看的字符串就可以了。重新定义上面的示例:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Student(object):
    def __init__(self, name):
         self.name = name

    def __str__(self):
         return '学生名称: %s' % self.name

print(Student('xiaozhi'))

执行结果为:

学生名称: xiaozhi

由执行结果看到,这样输出的实例不但好看,而且是我们想要的。
如果在交互模式下输入如下:

>>> s = Student('xiaozhi')
>>> s
<__main__.Student object at 0x00000000030EC550>

由执行结果看到,输出的实例还跟之前一样,不容易识别。
这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别在于__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串。也就是说,__repr__()是为调试服务的。
解决办法是再定义一个__repr__()。通常,__str__()__repr__()代码是一样的,所以有一个偷懒的写法:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Student(object):
    def __init__(self, name):
         self.name = name

    def __str__(self):
         return '学生名称: %s' % self.name
    __repr__ = __str__

在交互模式下执行:

>>> s = Student('xiaozhi')
>>> s
学生名称: xiaozhi

可以看到,已经得到满意的结果了。

2.__iter__

如果想将一个类用于for ... in循环,类似list或tuple一样,就必须实现一个__iter__()方法。该方法返回一个迭代对象,Python的for循环会不断调用该迭代对象的__next__()方法,获得循环的下一个值,直到遇到StopIteration错误时退出循环。
我们以斐波那契数列为例,写一个可以作用于for循环的Fib类:

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class Fib(object):
    def __init__(self):
         self.a, self.b = 0, 1 # 初始化两个计数器a、b

    def __iter__(self):
         return self # 实例本身就是迭代对象,故返回自己

    def __next__(self):
         self.a, self.b = self.b, self.a + self.b # 计算下一个值
         if self.a > 100000: # 退出循环的条件
             raise StopIteration();
         return self.a # 返回下一个值

下面我们把Fib实例作用于for循环。

>>> for n in Fib():
...    print(n)
...
1
1
2
3
5
. . .
89

3. __getitem__

Fib实例虽然能够作用于for循环,和list有点像,但是不能将它当成list使用。比如取第3个元素:

>>> Fib()[3]
Traceback (most recent call last):
  File "", line 1, in 
    Fib()[3]
TypeError: 'Fib' object does not support indexing

由执行结果看到,取元素时报错了。怎么办呢?
要像list一样按照下标取出元素,需要实现__getitem__()方法,代码如下:

class Fib(object):
    def __getitem__(self, n):
         a, b = 1, 1
         for x in range(n):
             a, b = b, a + b
         return a

下面尝试取得数列的值:

>>> fib = Fib()
>>> fib[3]
3
>>> fib[10]
89

由执行结果看到,可以成功获取对应数列的值了。

4. __getattr__

正常情况下,调用类的方法或属性时,如果类的方法或属性不存在就会报错。比如定义Student类:

class Student(object):
    def __init__(self, name):
         self.name = 'xiaozhi'

对于上面的代码,调用name属性不会有任何问题,但是调用不存在的score属性就会报错。
执行以下代码:

>>> stu = Student()
>>> print(stu.name)
Xiaozhi
>>> print(stu.score)
Traceback (most recent call last):
  File "", line 1, in 
    print(stu.score)
AttributeError: 'Student' object has no attribute 'score'

由输出结果看到,错误信息告诉我们没有找到score属性。对于这种情况,有什么解决方法吗?
要避免这个错误,除了可以添加一个score属性外,Python还提供了另一种机制,就是写一个__getattr__()方法,动态返回一个属性。上面的代码修改如下:

class Student(object):

    def __init__(self):
         self.name = 'xiaozhi'

    def __getattr__(self, attr):
         if attr=='score':
             return 95

当调用不存在的属性时(如score),Python解释器会调用__getattr__(self, 'score')尝试获得属性,这样就有机会返回score的值。在交互模式下输入如下:

>>> stu = Student()
>>> stu.name
xiaozhi
>>> stu.score
95

由输出结果看到,可以正确输出不存在的属性的值了。
注意,只有在没有找到属性的情况下才调用__getattr__,已有的属性(如name),不会在__getattr__中查找。此外,如果所有调用都会返回None(如stu.abc),就是定义的__getattr__默认返回None。

5. __call__

一个对象实例可以有自己的属性和方法,调用实例的方法时使用instance.method()调用。能不能直接在实例本身调用呢?答案是可以。
任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用,例如:

class Student(object):
    def __init__(self, name):
         self.name = name

    def __call__(self):
         print('名称:%s' % self.name)

在交互模式下输入如下:

>>> stu = Student('xiaomeng')
>>> stu()
名称:xiaomeng

由输出结果看到,可以直接对实例进行调用并得到结果。
__call__()还可以定义参数。对实例进行直接调用就像对一个函数调用一样,完全可以把对象看成函数,把函数看成对象,因为这两者本来就有根本区别。
如果把对象看成函数,函数本身就可以在运行期间动态创建出来,因为类的实例都是运行期间创建出来的。这样一来,就模糊了对象和函数的界限。
怎么判断一个变量是对象还是函数呢?
很多时候判断一个对象是否能被调用,可以使用Callable()函数,比如函数和上面定义的带有__call__()的类实例。输入如下:

>>> callable(Student('xiaozhi'))
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('a')
False

由操作结果看到,通过callable()函数可以判断一个对象是否为“可调用”对象。

牛刀小试——出行建议

小智今天想出去,但不清楚今天的天气是否适宜出行,需要一个帮他提供建议的程序,程序要求输入daytime和night,根据可见度和温度给出出行建议和使用的交通工具,需要考虑需求变更的可能。
需求分析:使用本章所学的封装、继承、多态比较容易实现,由父类封装查看可见度和查看温度的方法,子类继承父类。若有需要,子类可以覆盖父类的方法,做自己的实现。子类也可以自定义方法。
定义天气查找类,类中定义两个方法,一个方法根据传入的input_daytime值返回对应的可见度;另一个方法根据传入的input_daytime值返回对应的温度。

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class WeatherSearch(object):
    def __init__(self, input_daytime):
         self.input_daytime = input_daytime

    def seach_visibility(self):
         visible_leave = 0
         if self.input_daytime == 'daytime':
             visible_leave = 2
         if self.input_daytime == 'night':
             visible_leave = 9
         return visible_leave

    def seach_temperature(self):
         temperature = 0
         if self.input_daytime == 'daytime':
             temperature = 26
         if self.input_daytime == 'night':
             temperature = 16
         return temperature

定义建议类,该类继承WeatherSearch类。类中定义两个方法,一个覆盖父类的温度查找方法,具有传入的input_daytime的值,返回建议使用的交通工具;另一个方法返回整体的建议。

#! /usr/bin/python3
# -*- coding:UTF-8 -*-

class OutAdvice(WeatherSearch):
    def __init__(self, input_daytime):
         WeatherSearch.__init__(self, input_daytime)

    def seach_temperature(self):
         vehicle = ''
         if self.input_daytime == 'daytime':
             vehicle = 'bike'
         if self.input_daytime == 'night':
             vehicle = 'taxi'
         return vehicle

def out_advice(self):
    visible_leave = self.seach_visibility()
    if visible_leave == 2:
         print('The weather is good,suitable for use %s.' % self.seach_temperature())
    elif visible_leave == 9:
         print('The weather is bad,you should use %s.' % self.seach_temperature())
    else:
         print('The weather is beyond my scope,I can not give you any advice')

程序调用如下:

check = OutAdvice('daytime')
check.out_advice()

结果如下:

The weather is good,suitable for use bike.

调试

在程序运行的任何时刻为对象添加属性都是合法的,不过应当避免让对象拥有相同的类型却有不同的属性组。
在init方法中初始化对象的全部属性是一个好习惯,可以帮助你用户更好地管理类中的属性和对属性值的更改。
继承会给调试带来新挑战,因为当你调用对象的方法时,可能无法知道调用的是哪一个方法。一旦无法确认程序的运行流程,最简单的解决办法是在适当位置添加一个输出语句,如在相关方法的开头或方法调用开始处等。

你可能感兴趣的:(15 Python面向对象编程)