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
了。这样有什么作用呢?
这样可以确保外部代码不能随意修改对象内部的状态,通过访问限制的保护,代码更加安全。比如上面的分数对象是一个比较重要的内部对象,如果外部可以随便更改这个值,大家都随便更改自己成绩表单中的分数,岂不是很混乱。
如果外部代码要获取类中的name和score怎么办呢?
在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....
由执行结果看到,子类可以立即获取父类增加的非私有方法。
多态
继承可以帮助我们重复使用代码。但对于继承中的示例,无论是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()
可以判断一个变量是否为某些类型中的一种,判断变量是否为list或tuple的方式如下:
>>> 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方法中初始化对象的全部属性是一个好习惯,可以帮助你用户更好地管理类中的属性和对属性值的更改。
继承会给调试带来新挑战,因为当你调用对象的方法时,可能无法知道调用的是哪一个方法。一旦无法确认程序的运行流程,最简单的解决办法是在适当位置添加一个输出语句,如在相关方法的开头或方法调用开始处等。