通过几个实例来了解python怎么面向对象编程
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同
定义一个类
定义
假如我们想定义一个名为Student的类,定义类要通过class关键字
1 | class Student(object): |
class后面紧接着是类名,类名通常首字母要大写,然后是(object),表示该类是从哪个类继承下来的,如果没有合适的继承类,用object类就行,它是python中一切类的基类
类的实例化
类可以看作是一个模板,对象便是类的实例,类的实例化可以使用类名+(),例如
1 | student = Student() |
这样就完成了类的实例化,student对象便是Student类的一个实例,我们可以自由给它添加一些相应的属性,例如
1 | student.name = "Lisa" |
由于类可以起到模板的作用,我们可以在创建实例时,把一些我们认为必须绑定的属性强制填写进去,以Student类为例,通过定义一个特殊的__init__方法,可以将name与score等属性绑定
1 | class Student(object): |
这时候再使用student = Student()便会报错
1 | TypeError: __init__() missing 2 required positional arguments: 'name' and 'score' |
必须在实例化时将name与grade参数传入
1 | class Student(object): |
运行结果为
1 | Lisa |
我们也可以使用hasattr()函数来判断某个对象是否包含对应属性
hasattr语法:
1 | hasattr(object, name) |
实例:
1 | class Student(object): |
输出结果为
1 | True |
想要判断一个对象是从哪个类创建而来的话,可以使用__class__属性,例如
1 | class Student(object): |
输出结果为
1 | <class '__main__.Student'> |
注意:__init__方法两边各有两个下划线,方法的第一个参数必须为self,代表这个实例本身,因此在__init__方法内部,可以将各种属性绑定到self上
定义类的方法
除了特殊的__init__方法外,也可以定义其他函数,以Student类为例,可以定义一个打印出自身成绩的函数
1 | class Student(object): |
可以看到我们在类的内部定义了一个print_score函数,用于打印自身成绩,该函数有一个参数self,该参数是类内部的每个函数都需要添加的,代表该实例本身,不过实际调用类内部函数时self这个参数不需要传入
运行结果为
1 | Lisa的成绩为90 |
在类内部定义的函数与其他函数仅有一点区别,就是第一个参数永远是实例变量self,并且,在调用时不需要传入该参数,除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、可选参数等
封装、继承与多态
封装
封装就是将类内部的信息对外界隐藏起来,从外部无法看见,最典型的一点就是将类的属性设置为私有,在属性名前面加两个下划线便代表将改属性设置为私有,从外面无法直接调用
1 | class Student(object): |
这时候再输出__name属性与__grade属性
1 | student = Student("Lisa", 90) |
可以看见出现错误
1 | AttributeError: 'Student' object has no attribute '__name' |
值得注意的一点是在外部为类添加属性,是无法设置为私有的
1 | student = Student("Lisa", 90) |
结果仍然会直接输出该属性
1 | 12 |
继承
继承通俗的来说,就比如A类继承自B类,则A是B的子类,B是A的父类,子类会获得父类的全部属性和方法,即A获得了B的全部属性和方法
继承又分为单继承与多继承,多继承就是一个子类继承自多个父类,会获得多个父类的属性和方法
想使用继承,只需在定义类时,类名后的括号内填入继承的父类即可
下面以一个家庭为例,一个家庭中有一个父亲,一个母亲,一个儿子与一个女儿,父亲可以说话,母亲可以写作,假设儿子继承自父亲,女儿继承自母亲,那么可以这样定义
1 | # 父亲类(基类) |
可以看到son和daughet可以直接调用他们父类的方法,结果为
1 | I can speak! |
我们可以再做下修改,在Son这个类中定义自己的speak方法,看看输出结果
1 | # 父亲类(基类) |
结果为
1 | I can speak2! |
从结果可以看出,当子类和父类都拥有speak方法时,子类的speak方法覆盖了父类的speak方法,在代码运行的时候,总是会调用子类的speak(),这样就引出了多态
多态
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型,判断一个变量是否是某种类型可以用isinstance()判断
1 | son = Son() |
结果为
1 | True |
看来son,daughter分别对应着Son,Daughter这两种类型
但是再试一试
1 | son = Son() |
结果为
1 | True |
看来son还是Father类型,daughter还是Father与Mother类型
所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行
1 | father = Father() |
结果为
1 | False |
Son可以看作Father,但Father不可以看作Son
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Father类型的变量
1 | def say(father): |
当我们传入Father的实例时
1 | father = Father() |
say()就打印出
1 | I can speak! |
当我们传入Son的实例时
1 | son = Son() |
say()就打印出
1 | I can speak2! |
看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Son2类型,也从Father派生:
1 | class Son2(Father): |
当我们调用say()时,传入Son2的实例:
1 | son2 = Son2() |
say()就打印出
1 | I can speak3! |
你会发现,新增一个Father的子类,不必对say()做任何修改,实际上,任何依赖Father作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态
多态的好处就是,当我们需要传入Son、Son2、Daughter……时,我们只需要接收Father类型就可以了,因为Son、Son2、Daughter……都是Father类型,然后,按照Father类型进行操作即可。由于Father类型有speak()方法,因此,传入的任意类型,只要是Father类或者子类,就会自动调用实际类型的say()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Father类型,无需确切地知道它的子类型,就可以放心地调用speak()方法,而具体调用的speak()方法是作用在哪个对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Father的子类时,只要确保speak()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Father子类;
对修改封闭:不需要修改依赖Father类型的say()等函数
静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Father类型,则传入的对象必须是Father类型或者它的子类,否则,将无法调用say()方法。
对于Python这样的动态语言来说,则不一定需要传入Father类型。我们只需要保证传入的对象有一个say()方法就可以了:
1 | class Timer(object): |
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象