1、第八章 类与对象 Python支持创建自己的对象,Python从设计之初就是一门面向对象语言,它提供了一些语言特性支持面向对象编程。创建对象是Python的核心概念,本章将介绍如何创建对象,以及多态、封装、方法和继承等概念。Python快乐学习班的同学结束函数乐高积木厅的创意学习后,导游带领他们来到对象动物园。在对象动物园,将为同学们呈现各种动物对象,同学们将在这里了解各种动物所属的类别,各种动物所拥有的技能,以及它们的技能继承自哪里等知识点。现在跟随Python快乐学习班的同学一起进入对象动物园观摩吧!8.1 理解面向对象8.1.1 面向对象编程 Python是一门面向对象编程语言,对面向对
2、象语言编码的过程就叫做面向对象编程。面向对象编程Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。面向对象的程序设计把计算机程序视为一组对象的集合,每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。在Python中,所有数据类型都被视为对象,也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。8.1.2 面向对象术语简介 类(Class):用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共
3、有的属性和方法。对象是类的实例。类变量(或属性):类变量在整个实例化的对象中是公用的。类变量定义在类中且在方法之外。类变量通常不作为实例变量使用。类变量也称作属性。数据成员:类变量或者实例变量用于处理类及其实例对象的相关的数据。方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。实例变量:定义在方法中的变量,只作用于当前实例的类。多态(Polymorphism):对不同类的对象使用同样的操作。封装(Encapsulation):对外部世界隐藏对象的工作细节。继承(Inheritance):即一个派生类(derived
4、class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。以普通的类为基础建立专门的类对象。实例化(Instance):创建一个类的实例,类的具体对象。方法:类中定义的函数。对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,方法中可以调用基类中的同名方法。对象可以包含任意数量和类型的数据。8.2 类的定义与使用8.2.1 类的定义 类定义的语法格式如下:class ClassName(object):.Py
5、thon中定义类使用class关键字,class后面紧跟类名。示例如下(my_class.py):class MyClass(object):i=123 def f(self):return hello world 由代码可以看到,这里定义了一个名为MyClass的类。在Python中,类名一般由以大写字母开头的单词命名,并且若是由多个单词组成的类名,那各个单词的首字母都大写。类名后面紧接着是(object),object表示该类是从哪个类继承的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。类包含属性(相当于函数中的语句)和方法(类中的方法大体可以理解成第7章
6、所学的函数)。在类中定义的方法的形式和函数差不多,但不称为函数,而称为方法。因为方法需要靠类对象去调用,而函数不需要。8.2.2 类的使用 class MyClass(object):i=123 def f(self):return hello world use_class=MyClass()print(调用类的属性:,use_class.i)print(调用类的方法:,use_class.f()类的使用比函数调用多了几个操作,调用需要执行如下操作:use_class=MyClass()这步叫做类的实例化,即创建一个类的实例,此处得到的use_class这个变量称为类的具体对象。再看后面两行
7、的调用:print(f调用类的属性:use_class.i)print(f调用类的方法:use_class.f()这里第一行后面use_class.i部分的作用是调用类的属性,也就是前面所说的类变量。第二行后面use_class.f()部分的作用是调用类的方法。在类中定义方法的要求:在类中定义方法时,第一个参数必须是 self。除第一个参数之外,类的方法和普通函数没什么区别,如可以用默认参数、可变参数、关键字参数和命名关键字参数等。在类中调用方法的要求:要调用一个方法,在实例变量上直接调用即可。除了self不用传递,其他参数正常传入。类对象支持两种操作,属性引用和实例化。实例化方式上面已经介绍
8、过,属性引用的标准语法格式如下:obj.name 语法中,obj代表类对象,name代表属性。8.3 深 入 类 本节将深入介绍类的相关内容,如类的构造方法和访问权限。8.3.1 类的构造方法 对前面的示例做一些改动,改动后代码如下(my_calss_search.py):class MyClass(object):i=123 def _init_(self,name):self.name=name def f(self):return hello,+self.name use_class=MyClass(xiaomeng)print(调用类的属性:,use_class.i)print(调用类
9、的方法:,use_class.f()实例化MyClass这个类时,调用了_init_()这个方法。在Python中,_init_()方法是一个特殊的方法,当对象实例化时会被调用,_init_()的意思是初始化,是initialization的简写。这个方法的书写方式是:先两个下划线,后面接着是init,再接着两个下划线。这个方法也叫构造方法。在定义类时,若不显式定义一个_init_()方法,则程序默认调用一个无参的_init_()方法。在Python中,定义类时,若没有定义构造方法(_init_()方法),在类的实例化时,系统会调用默认的构造方法。另外,_init_()方法可以有参数,参数通过
10、_init_()传递到类的实例化操作上。一个类中可用定义多个构造方法,但类实例化时只实例化其中位于最后的一个构造方法,也即后面的构造方法会覆盖前面的构造方法,并且实例化时需要根据最后一个构造方法的形式进行实例化。建议一个类中只定义一个构造函数。8.3.2 类的访问权限 在类内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。要让内部属性不被外部访问,可以在属性的名称前加上两个下划线_,在Python中,实例的变量名如果以_开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。在Python中,可以给类增加get_at
11、trs这样的方法来获取类中的私有变量。在Python中,可以再给类增加set_attrs这样的方法来修改类中的私有变量。在Python中,通过定义私有变量和定义对应的set方法,可以帮助我们做参数检查,避免传入无效的参数。类还有私有方法。类的私有方法也是以两个下划线开头,声明该方法为私有方法,且不能在类的外使用。8.4 继 承 继承的语法格式如下:class DerivedClassName(BaseClassName):.面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。继承完全可以理解成类之间的类型和子类型关系。在面向对象程序设计中,当我们定义一个clas
12、s的时候,可以从某个现有的class继承,定义的新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。继承语法class子类名(基类名):基类名写在括号里,基本类是在类定义的时候,在元组之中指明的。在python中继承中的一些特点:(1)在继承中,基类的构造方法(_init_()方法)不会被自动调用,它需要在其子类的构造方法中专门调用。(2)在调用基类的方法时,需要加上基类的类名前缀,且需要带上self参数变量。区别于在类中调用普通函数时并不需要带上self参数。(3)Python总是首先查找对应类型的方法,如果不
13、能在子类中找到对应的方法,它才到基类中逐个查找。继承有什么好处?最大的好处是子类获得了父类的全部非私有的功能。class Animal(object):def run(self):print(Animal is running.)定义类:class Dog(Animal):pass class Cat(Animal):pass dog=Dog()dog.run()cat=Cat()cat.run()8.5 8.5 多重多重继承继承 Python还支持多重继承。多重继承的语法格式如下:class DerivedClassName(Base1,Base2,Base3):.可以看到,多重继承就是在定
14、义类时,在类名后面的圆括号中添加多个基类(父类或超类),各个基类之间使用逗号分隔。需要注意圆括号中父类的顺序,若父类中有相同的方法名,在子类使用时未指定,Python会从左到右进行搜索。即若某个方法在子类中没有定义,则子类从左到右查找各个父类中是否包含这个方法。通过类的多重继承,一个子类就可以继承多个父类,并从多个父类中取得所有非私有方法。8.6 多 态 使用继承,可以重复使用代码。但对于继承中的示例,无论是Dog类还是Cat类,调用父类的run()方法时显示的都是Animal is running.。对Dog类和Cat类做如下改进(完整代码见animal_3.py):class Dog(An
15、imal):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()方法,在
16、代码运行的时候,总是会调用子类的run()方法。我们称这个为:多态。多态这个术语来自希腊语,意思是有多种形式。多态意味着就算不知道变量所引用的对象类型是什么,还是能对对象进行操作,而多态也会根据对象(或类)的不同而表现出不同的行为。例如我们上面定义的Animal类,在类中定义了run方法,Dog和Cat类分别继承Animal类,并且分别定义了自己的run方法,最后Dog和Cat调用run方法时,调用的是自己的定义的run方法。多态的好处就是,当我们需要传入Dog、Cat等对象时,我们只需要接收Animal类型就可以了,因为Dog、Cat等都是Animal类型,然后,按照Animal类型进行操作
17、即可。多态的意思是:对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat对象上,由运行时该对象的确切类型决定。多态真正的威力在于:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:对扩展开放:允许新增Animal子类。对修改封闭:不需要修改依赖Animal类型的函数。很多函数和运算符都是多态的你写的绝大多数程序可能也是,即便你并非有意这样。8.7 封 装 封装是对全局作用域中其
18、他区域隐藏多余信息的原则。听起来有些像多态使用对象而不用知道其内部细节,两者概念类似,因为它们都是抽象的原则它们都会帮忙处理程序组件而不用过多关系细节,就像函数一样。但是封装并不等同于多态。多态可以让用户对于不知道什么类(或对象类型)的对象进行方法调用,而封装是可以不用关心对象是如何构建的而直接进行使用。前面几节的示例基本都有用到封装的思想,如在前面定义的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印学生的成绩,我们如下定义(student.py):class Student(object):def _init_(self,nam
19、e,score):self.name=name self.score=score std=Student(xiaozhi,90)def info(std):print(f学生:std.name;分数:std.score)info(std)既然Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。class Student0(object):def _init_(self,name,score):self.name=name self.score=score def info(sel
20、f):print(f学生:self.name;分数:self.score)要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入,执行如下语句:stu=Student0(xiaomeng,95)封装的另一个好处是可以给Student类增加新的方法。8.8 获取对象信息 当我们调用一个方法时,可能需要传递一个参数,这个参数类型我们是知道的,但是对于接收参数的方法,就不一定知道参数是什么类型了。那我们该怎么来得知参数的类型呢?Python为我们提供了如下几种获取对象类型的方法,具体如下:1.使用type()
21、我们前面已经学习过type函数的使用,基本类型都可以用type()判断。如:type(123)type(abc)type(None)2.使用isinstance()要明确class的继承关系,使用type()很不方便,通过判断class的数据类型,来确定class的继承关系,这样要方便的多,这个时候可以使用isinstance()函数。比如对于继承关系是如下的形式:object-Animal-Dog 即Animal继承object,Dog继承Animal,使用isinstance()就可以告诉我们,一个对象是否是某种类型。例如创建如下2种类型的对象:animal=Animal()dog=Dog
22、()对上面2种类型对象,使用isinstance判断如下:isinstance(dog,Dog)True 根据打印结果看到,dog是Dog类型,这个是没有任何疑问的,因为dog变量指向的就是Dog对象。接下来再判断Animal的类型,使用isinstance判断如下:isinstance(dog,Animal)True 根据打印结果看到,dog也是Animal类型。由此我们得知:dog虽然自身是Dog类型,但由于Dog是从Animal继承下来的,所以,dog也还是Animal类型。换句话说,isinstance()判断的是一个对象是否是该类型本身,或者是否是该类型继承类的类型。因此,我们可以确
23、信,dog还是object类型:isinstance(dog,object)True 同时也确信,实际类型是Dog类型的dog,同时也是Animal类型:isinstance(dog,Dog)and isinstance(dog,Animal)True 3.使用dir()如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个字符串的list 如要获得一个str对象的所有属性和方法,使用方式如下:dir(abc)8.9 类的专有方法 Python类还可以定义专用方法,专用方法是在特殊情况下或当使用特别语法时由 Python替你调用的,而不是像普通的方法那样在代码中直接调用。本节我
24、们讲述几个Python比较常用的专有方法。看到类似_init_这种形如_xxx_的变量或者函数名就要注意,这些在Python中是有特殊用途的。_init_我们已经知道怎么用了,Python的class中有许多这种有特殊用途的函数,可以帮助我们定制类。下面来介绍这种特殊类型的函数定制类的方法。1._str_()方法 class Student(object):def _init_(self,name):self.name=name def _str_(self):return 学生名称:%s%self.name print(Student(xiaozhi)输出结果为:结果输出的是一堆字符串,基本
25、没有人看得懂,这样打印出来的结果没有可用性。怎样才能输出读者能看懂,并且是可用的结果?要解决这个问题,需要定义_str_()方法,通过_str_()方法返回一个易懂的字符串就可以了。重新定义上面的示例(student_2.py):class Student(object):def _init_(self,name):self.name=name def _str_(self):return f学生名称:self.name print(Student(xiaozhi)程序输出结果为:学生名称:xiaozhi 如果在交互模式下输入:s=Student(xiaozhi)s 由输出结果可以看到,将实例
26、化的类对象赋给一个变量,输出变量的实例还是和之前一样,是一串基本看不懂的字符。直接显示变量调用的不是_str_()方法,而是_repr_()方法,两者的区别在于_str_()方法返回用户看到的字符串,而_repr_()方法返回程序开发者看到的字符串。也就是说,_repr_()方法是为调试服务的。所以这个问题的解决办法是再定义一个_repr_()方法。通常,_str_()方法和_repr_()方法是一样的,所以有一个巧妙的写法(student_3.py):class Student(object):def _init_(self,name):self.name=name def _str_(se
27、lf):return f学生名称:self.name _repr_=_str_ 在交互模式下执行:s=Student(xiaozhi)s 学生名称:xiaozhi 2._iter_()方法 如果一个类想被用于for.in循环,类似list或tuple那样,就必须实现一个_iter_()方法,该方法返回一个迭代对象,Python的for循环会不断调用该迭代对象的_next_()方法拿(“拿”是否可以更改为“获得”)到循环的下一个值,直到遇到StopIteration错误时退出循环。class Fib(object):def _init_(self):self.a,self.b=0,1#初始化两个
28、计数器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#返回下一个值 3._getitem_()方法 Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,要表现得像list那样按照下标取出元素,需要实现_getitem_()方法,如下实现:class Fib(ob
29、ject):def _getitem_(self,n):a,b=1,1 for x in range(n):a,b=b,a+b return a 4._getattr_()方法 前面示例讲述过,正常情况下,当调用类的方法或属性时,如果不存在,就会报错。要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个_getattr_()方法,动态返回一个属性。如下:class Student(object):def _init_(self):self.name=xiaozhi def _getattr_(self,attr):if attr=score:return
30、95 当调用不存在的属性(如score)时,Python解释器会调用_getattr_(self,score)尝试获得属性,这样就有机会返回score的值。在交互模式下输入:stu=Student()stu.name xiaozhi stu.score 95 由输出结果可以看到,通过调用_getattr_()方法,可以正确输出不存在的属性的值。注意,程序只有在没有找到属性的情况下才调用_getattr_(),若已有属性(如name),则不会在_getattr_()方法中查找。此外,所有_getattr_()方法的调用都有返回值,都会返回None(如stu.abc),定义的_getattr_()
31、默认返回None。5._call_()方法 一个对象实例可以有自己的属性和方法,当调用实例的方法时,采用的方式是使用instance.method()来调用。任何类,只需要定义一个_call_()方法,就可以直接对实例进行调用。如下示例:class Student(object):def _init_(self,name):self.name=name def _call_(self):print(f名称:self.name)_call_()还可以定义参数。在交互模式下输入:stu=Student(xiaomeng)stu()名称:xiaomeng 由输出结果可以看到,通过定义_call_()
32、方法,可以直接对实例进行调用并得到结果。_call_()方法还可以定义参数。对实例进行直接调用就像对一个函数进行调用一样,完全可以把对象看成函数,把函数看成对象,因为这两者本来就没有根本区别。如果把对象看成函数,函数本身就可以在运行期间动态创建出来,因为类的实例都是运行期间创建出来的。这样一来,就模糊了对象和函数的界限。怎么判断一个变量是对象还是函数呢?很多时候,判断一个对象是否能被调用,可以使用callable()函数。比如max()函数和上面定义的带有_call_()方法的Student类实例,示例如下:callable(Student(xiaozhi)True callable(max)
33、True callable(1,2,3)False callable(None)False callable(a)False 由输出结果可以看到,通过callable()函数可以判断一个对象是否为“可调用”对象。8.10 活学活用出行建议 假如你今天想外出一趟,但不清楚今天的天气是否适宜出行。现在需要设计一个帮你提供建议的程序,程序要求输入出行的时间,然后根据出行时间,结合能见度、温度和当前空气湿度给出出行建议,以及比较适合使用的交通工具,需要考虑需求变更的可能。需求分析:使用本章所学的封装、继承、多态,比较容易实现。在父类中封装查看能见度、查看温度和查看湿度的方法,子类继承父类。若有需要,子
34、类可以覆盖父类的方法,做自己的实现。子类也可以自定义方法。定义天气查找类,类中定义3个方法,一个方法根据传入的input_daytime值返回对应的能见度;一个方法根据传入的input_daytime值返回对应的温度;最后一个方法根据传入的input_daytime值返回对应的湿度。具体代码请参考书本。8.11 技巧点拨 在init()方法中初始化对象的全部属性是一个好习惯,可以帮助用户更好地管理类中的属性和对属性值的更改。在程序运行的任何时刻为对象添加属性都是合法的,不过应当避免让对象拥有相同的类型却有不同的属性组。继承会给调试带来新挑战,因为当你调用对象的方法时,可能无法知道调用的是哪一个
35、方法。一旦无法确认程序的运行流程,最简单的解决办法是在适当位置添加一个输出语句,如在相关方法的开头或方法调用开始处等。8.12 8.12 问题探讨问题探讨(1)有办法从外部访问以双下画线开头的实例变量吗?(2)方法与函数有什么区别?(3)使用类的好处?8.13 8.13 章节回顾章节回顾(1)回顾什么是类,类如何使用。(2)回顾构造方法的定义,使用构造方法的好处。(3)回顾类的访问权限有哪些,这些访问权限都怎么使用。(4)回顾继承、多重继承的定义,它们都是怎样实现的。(5)回顾多态的定义与实现。(6)回顾封装的定义与实现。(7)回顾类的专有方法,各自如何使用。8.14 8.14 实战实战演练演练