第十六节 面向对象编程
大家好,目前为止,我们编写的python代码,有一个基本的特征,就是根据需求,围绕函数,设计程序,处理数据。即使面临相对复杂的问题,通过函数、模块和包等解决方案,也能帮助我们解决程序架构和代码复用问题。但是,这样的编程方式还是被称作面向过程的编程。
说简单点,本质上,还是一种线性化的编程方式。虽然python的独特性,足以支撑其面向过程的编程依然会大行其道,并且,在处理很多问题时游刃有余。但是,相比其他高级语言的面向对象的编程方式来说,面向过程的编程方式必然存在一定的局限性。
那么,今天,我们就来讲讲什么是Python面向对象的编程。
1、面向对象编程的基石:类与实例
类与实例是面向对象编程的基石。一个类(Class)能够创建一种新的类型(Type),其中对象(Object)就是类的实例(Instance)。
这个比较抽象的概念,我们可以这样来理解,比如:
2、类的字段、方法、属性
类可以包括字段(Field)和方法(Method)。
在python里,我们可以简单地这样理解这些概念:
字段,就是隶属于类的变量。
方法,就是隶属于类的函数。
关于字段,它有两种类型:
(1)隶属于类的实例(对象)的字段,被称作实例变量(Instance Variables);
(2)从属于某一类本身的字段,被称作类变量(Class Variables)。
关于方法,它有一个特殊的参数self
与普通函数的区别:除了它隶属于某个类,在它的参数列表的开头,还需要添加一个特殊的参数 self ,但是你不用在调用该方法时为这个参数赋值,Python 会为它提供。
事实上,当你调用一个类的方法时,Python 将会自动将self参数转换成 myobject,所以,你无需为其赋值。这同时意味着,如果你的类里面的方法没计划有参数,你依旧必须为它添加 一个self 参数 。
PS:Python中的 self 相当于 C++ 中的 this 指针,Java 与 C# 中的 this 引用。
3、类的创建
通过 class 关键字可以创建一个类。
类的名称后跟一对括号,就创建一个类的实例。
接下来是一个缩进的语句块,代表这个类的主体。
示例16_1:本例中,我们使用 pass 语句创建了一个空代码块。
运行结果显示: myclass 类的 __main__ 模块中拥有了一个实例MyClassObject,并显示了它在内存中存储该对象的地址。当然,你看到的地址应该不会相同,因为 Python 会在它找到的任何空间来存储对象。
4、方法的创建
类定义一个方法(Method)其实很简单,就像定义一个函数一样,唯一的不同在于它的方法还拥有一个额外的 self 参数。
示例16_2
这里我们就能看见 self 的存在了,请注意, 调用say_hi() 这一方法原本并不需要参数,但是我们还是要在方法的定义中提供 self 参数。
5、__init__ 方法
这种前后都带有双下划线的方法,是属于python内置的专用方法。
这里我们来了解一下 __init__ 方法究竟有什么特殊意义?
__init__ 方法会在它的类被实例化(Instantiated)时立即运行。这一方法可以对任何你想进行操作的目标对象进行初始化(Initialization)操作。
这里你要注意在 init 前后加上的双下划线。
代码解析:
(1)、在本例中,我们定义2个name变量,但它们并不相同,更不会造成混淆。因为 self.name 中的点号意味着它的“name”是“self”对象的一部分,而另一个 name 则是一个纯粹的局部变量。
(2)、MyClass 类的实例调用了say_hi()方法,但是,并没有显式地调用__init__() 方法,然而它已经运行了。这正是这个方法的特殊之处。
6、类变量与实例变量
字段和方法都是类的属性。
我们已经知道,方法实现类的功能。那么字段呢?字段则用于存储类的数据。
作为数据的存在形式,字段其实就是绑定(Bound)到类与对象(即类的实例)的命名空间(Namespace)中的普通变量。意思是,我们定义的类的字段,仅在这些类与对象所存在的地方(被称作“上下文中”)有效。再简化一点:字段其实就是绑定到类的命名空间中的普通变量,并且,仅在这些类所存在的地方有效。
字段(Field)的两种类型 —— 类变量与实例变量。
类变量(Class Variable)是共享的(Shared)——它们可以被属于该类的所有实例访问(使用)。该类变量只拥有一个副本,当任何一个对象对类变量作出改变时,发生的变动将在其它所有实例中都会得到体现。
实例变量(Object variable)由类的每一个独立的实例(对象)所拥有。在这种情况下,每个对象都拥有属于它自己的独立字段,也就是说,它们不会被共享,也不会以任何方式与其它不同实例中的相同名称的字段产生关联。
代码解析:
(1)、定义了一个类变量x,它将在类的所有实例中有效,注意:使用时需要前置类名,如MyClass.x。
(2)、在方法一中我们还故意定义了一个同名的局部变量:x=-1。请注意,它并不能因此影响到方法二中的x的值。
7、类的继承
面向对象编程的一个显著特征(也是一大优点)就是对代码的重用(Reuse),而重用的实现方法之一就是继承(Inheritance)。
下面讲一个关于继承的示例,设计需求是:假设一个应用涉及大学的老师和学生。其中一些特征是他们共有的,如:姓名、年龄、地址。而另外一些特征,如:教师的薪水、课程、假期,学生的成绩和学费,则是各自独立拥有的。
解决方案中,可以分别为他们创造两个“独立且笨重”的类,来处理信息。
但有一种更好的方法,是创建一个公共类叫作 SchoolMember ,然后建立两个子类:教师(Teacher )子类 和学生(Student)的子类,并让它继承SchoolMember类。然后,再向这些子类型中添加一些必要的独有的特征。
父类 :SchoolMember ,又被称作基类(Base Class)或是超类(Superclass)。
子类:Teacher 和 Student类会被称作派生类(Derived Classes)或是子类(Subclass)。
(1)、先建一个学校类:SchoolMember,即:父类(基类)
(2)、再建两个子类:Teacher 和 Student。
请注意:它们通过类名后面的圆括号声明父类。
(3)、调用子类
#代码执行,输出:
(Initialized SchoolMember: 张老师)
(Initialized Teacher: 张老师)
(Initialized SchoolMember: 小明)
(Initialized Student: 小明)
Name:"张老师" Age:"40" Salary: "30000"
Name:"小明" Age:"25" Marks: "75"
8、类与类的继承的几个重要特性
(1)、类从基类中继承属性(字段和方法)
实际操作中,我们可以可以通过在子类中的方法名前面加上基类名做前缀,再传入 self 和其余变量,来调用基类的方法。
比如,在 Teacher 和 Student 子类中,我们可以直接用基类中的方法:SchoolMember.tell(self)
(2)、实例会继承所有可读取类(子类和父类)的属性(字段和方法)
案例中,当我们使用 SchoolMember 类的 tell 方法时,你会发现被调用的是子类型的 tell 方法,而不是 SchoolMember 的 tell 方法。这是因为 Python 总会从当前的实例的类型中开始寻找方法。如果找不到,它就会在该类所属的基类中继续查找。
(3)、如果子类中定义了__init__ 方法,将优先被调用,如果此时需要调用基类的__init__ 方法,则需要显式地进行调用。除非子类中未定义__init__ 方法,那么,类的__init__ 方法将自动执行。
比如,我们在 Teacher 和 Student 子类中定义了 __init__ 方法, Python 就不会自动调用基类 SchoolMember 的构造函数,必须自己显式地调用它。相反,如果我们没有在子类中定义 __init__ 方法,Python 将会自动调用基类的该方法。
(4)、修改父类的任何功能,它将自动反映在子类中。相反,子类的修改,则不会影响到其他的子类。
比如,案例中,你可以通过简单地向 SchoolMember 类进行操作,来为所有老师与学生添加一条新的 ID 卡字段。
小结
Python 是高度面向对象的,从长远来看,你不可能永远只做小应用,了解这些概念将对你大有帮助。
预告
下节课,我们将学习如何处理输入与输出,以及如何在 Python 中访问文件,这也是Python中极其重要和应用广泛的一个知识点。