Python学习笔记(九)之面向对象编程(下)
0. 附上Python面向对象学习笔记上的链接:
Python学习笔记(八)之面向对象编程(上)
1. 引言
- 上一节类比java的面向对象学习了Python中的面向对象的基础知识,以及在最后的总结中比较了一些Python和java中的面向对象的不同。
- 在java的学习中我了解到,数据封装、继承和多态是面向对象的三大特点,所以在这一章节中,我将继续学习Python的继承和多态的语法和技巧,更加深入的了解Python的面向对象,依旧是类比java学习。
- 两天没有学习Python面向对象的知识了,所以现在稍微回顾一下上面学到的内容。
- 首先是定义一个类,可以通过
class 类名(继承自)
的方式定义一个新类; - 接着自然是写一个构造函数了,所以
__init__
函数登场了,在类的的函数中,默认会传入参数self
,因为Python的构造函数只能有一个,所以参数的传入要比较谨慎的书写,可以通过关键字参数
传入参数的方式来实现。 - 再者就是
数据封装
这个面向对象的三大特点之一,即将对对象的属性的操作放在类内部的函数中,即类的方法
中,在类的各个方法中实现对属性的操作。 - 接着是
访问限制
问题,这就涉及到和java中的private型的属性一个道理了,当然在Python中实现更加简洁,只要通过属性的名称即可判断是否为private型,即在属性名的开头加上__
双下划线即可,且结尾不能跟着下划线。 - 最后是说道Python中的一些面向对象的注意事项,当我们设置了属性为private型时,Python本身还可以通过其他的方式访问到类中的private型的属性,这就涉及到了通过
_类名__属性名
(实例名前为一个下划线,属性名前为两个下划线)的方式来调用;且在修改属性值时,我们可能错误的用实例名.__属性名= 新的值
的方式来修改实例中的属性值,但这本身并没有真正的修改成功,反而是新建了一个变量且为它赋值了。所以上面的两种方法都是不可靠的,都是禁止使用的。 - 补充一条,就是Python中的setter和getter方法,其实本质上实现是和java中一样对,通过该方法可以访问和修改实例中的private型的属性值。
- 首先是定义一个类,可以通过
- 那么回顾完上次学习到的内容后,就下来就进入正题,接着学习面向对象的其他两个特点,继承和多态。
2. 继承和多态
2.1 继承
2.1.1 继承的含义
- 在OOP(Object Oriented Programming, 即面向对象编程)程序设计中,当我们定义一个class时,我们可以选择该class所要继承的类,在之前写的类中,我们继承的对象都是
object
,这是所有类最后都要继承的对象,而这个新的class我们就称为object的子类(subclass)
,而被继承的object这个class则称为基类
、父类
或超类
(Base class、Super class)。
2.1.2 继承实例
-
来一个简单的实例来说明一下继承的优点:
-
例2.1.2.1:
# !user/bin/python # coding=utf-8 __author__ = "zjw" class Shape(object): def describe(self): print "我是一个形状" class Rectangle(Shape): pass class Circle(Shape): pass if __name__ == '__main__': shape = Shape() rectangle = Rectangle() circle = Circle() # 分别调用三个实例的describe() shape.describe() rectangle.describe() circle.describe()
-
输出:
我是一个形状
我是一个形状
我是一个形状 -
分析:
- 可以看到
Rectangle
和Circle
这两个类继承自Shape
类,我们在父类Shape
中实现了describe方法,但是两个子类中并没有真正的实现该方法。可以在main函数中看到,当我们实例了三个对象后,两个子类对象调用describe方法时,实际上是直接调用了父类的describe方法。 - 这里就体现了继承的好处?最大的好处就是子类获得了父类的全部功能。而在上面例子中的体现就是,子类
Rectangle
和Circle
虽然没有写describe方法,但是他们继承了Shape
,那么在实例化后,他们也可以直接调用父类的describe方法。
- 可以看到
-
-
类比总结:
- 可以看到在Python中的继承实际上和java中的继承是相似的,只不过Python中的类可以什么东西都不写,只要通过pass语句即可实现。
- 在继承语法上有些不同,在java中我们是通过extends关键字实现继承的;而在Python中,只要在类名后面的括号中写入要继承的类即可。
- 在java的学习中,多态其实是继承的另一个优点,那么接下来就来详细说一说多态。
2.2 多态
2.2.1 多态的含义
-
首先我们对上面的例子进行小小的修改,为两个子类加上describe方法:
-
例2.2.1.1:(这里只贴出修改部分)
class Rectangle(Shape): def describe(self): print "我是一个长方形" class Circle(Shape): def describe(self): print "我是一个圆圈"
-
输出:
我是一个形状
我是一个长方形
我是一个圆圈 -
分析:可以看到当我们为子类添加上describe方法后,子类的describe方法就将父类的覆盖掉了,后面当我们实例化子类时,调用到的就是子类的describe。这就说明了继承的另一个好处,
多态
。
-
2.2.2 多态实例
-
先来一个实例看看:
-
例2.2.2.1:
# !user/bin/python # coding=utf-8 __author__ = "zjw" class Shape(object): def describe(self): print "我是一个形状" def area(self): pass def perimeter(self): pass class Rectangle(Shape): def __init__(self, length, width): self.__length = length self.__width = width def describe(self): print "我是一个长方形" def area(self): return self.__length * self.__width def perimeter(self): return 2 * (self.__length + self.__width) if __name__ == '__main__': shape = Shape() shape.describe() print shape.area() print shape.perimeter() rectangle = Rectangle(2, 2) rectangle.describe() print rectangle.area() print rectangle.perimeter()
-
输出:
我是一个形状
None
None
我是一个长方形
4
8 -
分析:
- 在这个实例中,我们可以看到
Shape
为Rectangle
的父类,在父类中的area方法和perimeter方法并没有内容,而在Rectangle方法中我们则写了相应的内容,来输出该形状的面积和周长。在main函数的实践中,可以看到子类的方法已经覆盖了父类的方法,且调用时可以实现不同的功能。 - 可以想象一下,我们学过计算很多中形状的面积和周长,这是当我们想要使用写某种新的形状类时,我们大可以直接继承自
Shape
父类,并重写一下area和perimeter方法,即可轻松实现方法的覆盖。
- 在这个实例中,我们可以看到
-
-
再来一个直观实例展示多态的好处:
-
例2.2.2.2:
# !user/bin/python # coding=utf-8 __author__ = "zjw" class Shape(object): def describe(self): print "我是一个形状" def twice_describe(self): # isinstance来判断类型 print "是否是一个Shape?", isinstance(self, Shape) print "是否是一个Rectangle?", isinstance(self, Rectangle) print "是否是一个Circle?", isinstance(self, Circle) self.describe() self.describe() print class Rectangle(Shape): def describe(self): print "我是一个长方形" class Circle(Shape): def describe(self): print "我是一个圆圈" if __name__ == '__main__': shape = Shape() rectangle = Rectangle() circle = Circle() # 分别调用三个实例的describe() shape.twice_describe() rectangle.twice_describe() circle.twice_describe()
-
输出:
是否是一个Shape? True
是否是一个Rectangle? False
是否是一个Circle? False
我是一个形状
我是一个形状是否是一个Shape? True
是否是一个Rectangle? True
是否是一个Circle? False
我是一个长方形
我是一个长方形是否是一个Shape? True
是否是一个Rectangle? False
是否是一个Circle? True
我是一个圆圈
我是一个圆圈 -
分析:
-
这里请注意看父类中的
twice_describe
方法,在这个方法里,我们首先通过isinstance
函数来判断self的类型,接着我们调用两次self.describe()
来输出此时的形状,最后一个print起到再换行的作用。 -
我们可以看到父类的实例shape只是一个Shape,而子类的rectangle实例不仅是一个Rectangle还是一个Shape,子类的circle实例同理。这让我想起java中继承我们要遵循
子类 is a 父类
原则。 -
接着说这样写的好处:通过该多态形式的实现,我们将每个子类不同的方法写在子类自己的内部,而将统一的方法写在父类中,父类中这个统一的方法可以调用子类中不同的方法,那么我们就可以实现代码的精简。我们不必在子类中再一一写统一的那个方法,直接调用父类的方法即可完美的实现。为什么这么说呢,当我们再想来一个椭圆类,我们想在椭圆类的describe方法中写下一句“你好,我是椭圆形”。这时我们调用父类的twice_describe方法依旧是可行的。就不会说同一个方法,我们在每个子类中都重写一遍,那么有成百上千个子类,那怎么办?那么这就体现了多态的好处啦。
-
通过该例子,我们来说一下多态的真正的威力:调用方只管调用,不管细节,而当我们新增一种
Shape
子类时,我们只要确保describe()
方法编写正确即可,不用管原来的代码是如何调用的。这就是著名的开闭原则
:-
对扩展开放:允许新增
Shape
子类; -
对修改封闭:不需要修改依赖
Shape
类型的twice_describe()
等函数。
-
-
-
3. 实例属性和类属性
-
刚开始看到这两个属性时,有点一头雾水,因为我在java的学习中,从来就只有属性的概念,并没有将属性在细分成实例属性和类属性。但是学完之后便焕然大悟。
-
实例属性:顾名思义,就是实例化后的属性值,所以给实例属性赋值的方法可以通过实例变量,或则是
self
变量实现的。我们前面接触到的属性应都属于实例属性。 -
类属性:则是类中一开始就存在的属性值,有点像java中的静态属性值,它是类内部的变量,所以一旦改变就会影响到整体。
-
实例属性属于各个实例所以,互不干扰;
-
类属性属于类所有,所有实例共享一个属性。
-
来个实例说明一下:
-
例3.1:
# !user/bin/python # coding=utf-8 __author__ = "zjw" class Test(object): count = 0 def __init__(self): Test.count = Test.count + 1 if __name__ == '__main__': test1 = Test() test2 = Test() test3 = Test() test4 = Test() test5 = Test() print test1.count print test2.count print test3.count print test4.count print test5.count print Test.count test1.count = 0 print "此时的test1的count值时:", test1.count
-
输出:
5
5
5
5
5
5此时的test1的count值时: 0
-
分析:
- 可以看到,此时类Test中的count就是一个类属性,我们实现了创建多少个Test实例就计数多少次的功能,并将加1功能放置在构造函数中。我们看到在构造函数中我们并不是用
self.count
来调用类属性count的,因为在上面我们说过,这样调用就是实例的属性了,而通过Test.count
即可调用到此时的类属性count,并进行赋值。 - 在main函数中,我们创建了5个Test实例,并以此输出每个实例的count值,可以发现此时的类属性count是大家所共有的,即是相同的。
- 在看到我们在输出后面有加了一句
test1.count = 0
语句,目的是说明当我们定义了一个实例属性和类属性名发生冲突是,类属性会被覆盖掉,输出的就是实例属性的值。所以要避免类属性名和实例属性名的冲突问题。
- 可以看到,此时类Test中的count就是一个类属性,我们实现了创建多少个Test实例就计数多少次的功能,并将加1功能放置在构造函数中。我们看到在构造函数中我们并不是用
-