类(class)和实例(instance)
举例:以student类为例
1.在Python中,定义类是通过class关键字来定义的,class后面紧跟的是类名,即Student类,类名通常是大写字母开头,后面是(object),表明该类是从哪个类继承下来的。如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
2.定义好了Student类,就可以根据Student类来创建出Student的实例,创建实例是通过类名+()实现的:
说明:变量bart指向的就是一个Student的实例,后面的
是内存地址,每个object的地址都是不一样的,而Student本身则是一个类
可以自由的给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
由于类可以起到模板的作用,所以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去,通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:
说明:__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__int__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
有了__init__方法,在创建实例的时候,就不能传入空的参数,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去,
和普通函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,在调用时,不用传递该参数,除此之外,类的方法与普通函数没有射门区别,所以仍然可以使用默认参数,可变参数,关键字参数和命名关键字参数。
数据封装及访问限制
生活化的封装:我们在使用电视机时,不用知道电视机里的具体细节,只需要再用的时候,按下遥控器就可以了,这是功能的封装。
在使用支付宝时,不用知道支付宝的接口以及后台出路数据的能力,只需要扫码就可以了,这是方法的封装。
封装的意义:
封装不是单纯意义上的隐藏
封装数据的主要原因是保护隐私
封装方法的主要原因是隔离复杂度
在编程里,对外提供接口,表示这个接口的函数,通常称为接口函数。
封装分为两个层面:
第一层面:创建类和对象时,分别创建两者的名称空间,只能通过类名加“.”或obj.的方式访问里面的名字
第二层面:类中把某些属性和方法隐藏起来,或定义为私有,只在类的内部使用,在类的外部无法访问,或者留下少量的接口(函数)访问。
在python中,使用双下划线的方式实现隐藏属性(设置为私有属性)
举例
用定义的创建一个老师t1和一个学生s1
分别调用老师和学生的姓名和年龄等特征:
返回以下信息
调用老师的教书技能和学生的学习技能
返回信息如下
把这两类的一些属性隐藏下,
在次创建老师和学生实例
调用老师和学生特征:
然后返回时出错了
在调用老师和学生的技能
返回如上,还是能正常返回的。
如何用外部代码获取隐藏信息
可以给Student类增加get_name和get_age或get_course
外部修改代码再给Student类增加set_age方法,可以对参数检查,避免传入无效的参数。
注意:
1.在Python中变量名类似__xxx__的,也就是使用这种方法以双下划线开头,并且以双下划綫结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以不能用__name__、__age__这样的变量。
2.以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以直接访问的,但是我们会默认为是私有变量,不随便访问。
3.双划线开头的实例变量可以通过以下方式访问。但不建议这么干,因为不同版本python解释器可能会把__age改成不同的变量名
先看看t1,s1的名称空间
从返回来看,名称空间名字变了。那我们来访问名称空间的key
从返回来看,我们可以通过‘_类名__属性’的方式来访问其内部属性值
和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然他们都是同一类的不同实例,但拥有的变量名称都可能不同。
继承和多态
继承
在OOP程序设计时,当我们定义一个class的时候,可以从现有的class继承,新的class称为子类,而被继承的class称为基类、父类或超类(Base class、Super class)
举例:我们已经编写了一个名为Father的class,有一个walk()方法可以直接打印:
当我们需要编写Bigson和Twoson类时,就可以直接从Father类继承:
对于Bigson来说,Father就是父类,对于Father来说,Bigson是它的子类。
继承的优点就是子类可以获得父类的全部功能,
由于Father实现了walk()方法,因此Bigson和Twoson作为它的父类也获得了这种方法
运行结果如下:
第二个好处就是修改下代码就可以实现有自己特点的walk()方法。
再次运行
运行结果就有自己的特点了。
多态
当子类和父类都存在walk()的方法时,子类的walk()会覆盖父类的walk(),在代码运行的时候,总是会调用子类的walk()。
这样,就得出了继承的另一个好处:多态
我们在定义class的时候,其实可以理解为我们定义了一个数据类型。
判断一个变量是否是某个类型可以用isinstance()判断:
但是可以试试
返回结果来看,c不只是Bigson还是Father。
可以理解,因为Bigson是从Father继承下来的,所以是Bigson必定也是Father,但反过来就不行了。
多态的第二个优点就是
一个接受父类赋值的函数,可以接受子类的赋值,并且不用做修改就可直接执行。
对于我们赋值的变量,我们只知道它是父类类型,无需确切知道它的子类型,都可以调用相同的方法,而具体调用的某种方法作用在父类或哪个子类上,由运行的该对象的确切类型决定,这就是多态的威力:调用方只管调用,不管细节,当新增一种子类时,只要确保方法编写正确,不用管原来的代码是如何调用的,这就是著名的“开闭”原则:
对扩展开放:允许新增子类
对修改封闭:不需要父类就可执行的函数
继承可以一级一级传下去,任何类都可追溯到根类object。
静态语言 vs 动态语言
对于静态语言(e.g JAVA)来说,如果传入Father类型,则传入的对象必须是Father类型或它的子类,否则,将无法调用walk()方法
对于Python这样的动态语言来说,则不一定需要传入Father类型,我们只需要传入的对象有一个walk()方法就可以了。
这就是动态语言的“鸭子类型”,它并不需要严格的继承体系,一个对象只要看起来“看起来像鸭子,走起路来像鸭子”,那它就可以被看做鸭子。
Python的“file-like object”就是一种鸭子类型,对真正的文件对象,它有一个read()方法,返回内容,但是,许多对象,只要有read()方法都被看做是“file-like object”,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。