类
类是面向对象的重要内容,可以把类当成一种自定义类型,可以使用类来定义变量,也可以使用类来创建对象。
定义类
在面向对象的程序设计过程中有两个重要概念: 类( class )和对象( object ,也被称为实例,instance ),其中类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体。从这个意义上看,日常所说的人,其实都是人这个类的实体,而不是人这个类。
类定义由类头(指 class 关键字和 类名部分)和统一缩进的类体构成。Python 定义类的简单语法如下:
1 | class 类名: |
Python 的类名必须是由一个或多个有意义的单词连缀而成的,每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。
Python 类所包含的最重要的两个成员就是变量和方法,其中类变量属于类本身,用于定义该类本身所包含的属性,比如人类的角色属性是人。而实例变量则属于该类的对象,用于定义对象所包含的属性,比如某个人的姓名、年龄、性别。方法则用于定义该类的对象的行为或功能,比如人类都会走、会说话。类中各成员之间的定义顺序没有任何影响,各成员之间可以相互调用。
下面程序将定义一个 Person 类:
1 | class Person: |
属性引用
在定义了类之后,接下来即可使用该类了。对于类的属性,如果想要引用则使用 类.变量
的格式:
1 | print(Person.role) # 查看人的 role 属性 |
类的实例化
在类中定义的方法默认是实例方法,定义实例方法与定义函数基本相同,只是实例方法的第一个参数会被绑定到方法的调用者(该类的实例)—— 因此实例方法至少应该定义一个参数,该参数通常会被命名为 self
。
在类中有一个特别的方法:__init__
,这个方法被称为构造方法。构造方法用于构造该类的对象, Python 通过调用构造方法返回该类的对象 。如果没有为该类定义任何构造方法,那么 Python 会自动为该类定义一个只包含一个 self
参数的默认的构造方法。
1 | class Person: |
注意: 实例方法的第一个参数并不一定要叫 self
,其实完全可以叫任意参数名,只是约定俗成地把该参数命名为 self
,这样具有最好的可读性。
类名加括号就是实例化,这将会自动触发 __init__
函数的运行,可以用它来为每个实例定制自己的特征。实例化的过程其实就是从类构造出对象的过程。调用某个类的构造方法即可创建这个类的对象:
1 | p = Person() |
对象的使用
原本我们只有一个 Person 类,在这个过程中产生了一个 p 对象,有自己具体的名字、年龄。
查看对象属性的语法是 对象.变量
,调用对象方法的语法是 对象.方法(参数)
。在这种方式中,对象是主调者,用于访问该对象的变量或方法。
1 | # 输出 p 的 name、age 实例变量 |
上面程序开始访问了 p 对象的 name 、age 两个实例变量。这两个变量是何时定义的?留意在 Person 的构造方法中有如下两行代码:
1 | self.name = name |
这两行代码用传入的 name 、age 参数(这两个参数都有默认值)对 self
的 name 、age 变量赋值。由于 Python 的第一个 self 参数是自动绑定的(在构造方法中自动绑定到该构造方法初始化的对象),而这两行代码就是对 self 的 name 、age 两个变量赋值,也就是对该构造方法初始化的对象( p 对象)的 name 、age 变量赋值,即为 p 对象增加了 name 、age 两个实例变量。self 最大的作用就是引用当前方法的调用者
上面代码中通过 Person 对象调用了 say()
方法,在调用方法时必须为方法的形参赋值。但 say ()
方法的第一个形参 self 是自动绑定的,它会被绑定到方法的调用者( p ),因此程序只需要为 say()
方法传入一个字符串作为参数值,这个字符串将被传给 content
参数。
我们定义的类的属性到底存到哪里了?有两种方式查看:
dir(类名)
:查出的是一个名字列表类名.__dict__
:查出的是一个字典,key为属性名,value为属性值
特殊的类属性
1 | 类名.__name__ # 类的名字(字符串) |
对象
假设你现在是一家游戏公司的开发人员,现在需要你开发一款叫做 <人狗大战> 的游戏,这个游戏中至少需要 2 个角色,一个是人, 一个是狗,且人和狗都有不同的技能,比如人拿棍打狗, 狗可以咬人,怎么描述这种不同的角色和功能呢?
人类除了有自己的昵称之外,还应该具备攻击力,生命值等属性:
1 | class Person: |
对象是关于类而实际存在的一个例子,即实例。对象(实例)只有一种作用,即属性引用:
1 | liubei = Person('刘备', 10, 1000) |
当然了,对象也可以引用一个方法,因为方法也是一个属性,只不过是一个类似函数的属性,我们也管它叫动态属性。引用动态属性并不是执行这个方法,要想调用方法和调用函数是一样的,都需要在后面加上括号:
1 | print(liubei.attack) |
可能有些难以理解,下面用函数的实现方式来解释一下类:
1 | def Person(*args, **kwargs): |
理解了对象之后,接下来我们定义一个圆形类,并为其提供计算面积和周长的方法:
1 | from math import pi |
对象之间的交互
现在已经有一个人类了,通过给人类一些具体的属性我们就可以拿到一个实实在在的人。现在要再创建一个狗类,狗就不能打人了,只能咬人,所以我们给狗一个 bite 方法。有了狗类,我们还要实例化一只实实在在的狗出来。然后人和狗就可以打架了。
1 | class Dog: # 定义一个狗类 |
实例化出一只实实在在的二哈:
1 | ha2 = Dog('二愣子', '哈士奇', 10, 1000) # 创造了一只实实在在的狗ha2 |
交互 ha2 与 liubei 打一下:
1 | print(ha2.life_value) # 看看ha2的生命值 |
完整代码如下:
1 | class Person: |
命名空间
创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性。而类有两种属性:
- 静态属性:接在类中定义的变量
- 动态属性:定义在类中的方法
并且类的数据属性是共享给所有对象的:
1 | print(id(ha2.role)) |
类的动态属性是绑定到所有对象的:
1 | print(ha2.bite) |
创建一个对象(实例)就会创建一个对象(实例)的名称空间,存放对象(实例)的名字,称为对象(实例)的属性。
在 obj.name
会先从 obj
自己的名称空间里找 name
,找不到则去类中找,类也找不到就找父类…最后都找不到就抛出异常。
面向对象的组合用法
代码重用的重要方式除了继承之外还有另外一种方式,即组合。在一个类中以另外一个类的对象作为数据属性,称为类的组合。
1 | class Weapon: |
圆环是由两个圆组成的,圆环的面积是外面圆的面积减去内部圆的面积。圆环的周长是内部圆的周长加上外部圆的周长。这个时候,我们就首先实现一个圆形类,计算一个圆的周长和面积。然后在 “环形类” 中组合圆形的实例作为自己的属性来用:
1 | from math import pi |
用组合的方式建立了类与组合的类之间的关系,它是一种 ‘有’ 的关系,比如教授有生日,教授教 Python 课程。
1 | class BirthDate: |
当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好。
类的三大特性之继承
继承是一种创建新类的方式,在 Python 中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类。
Python 中类的继承分为:单继承和多继承。
1 | class ParentClass1: # 定义父类 |
使用类的内置属性 __bases__
即可查看对应的类所有继承的父类:
1 | print(SubClass1.__bases__) |
如果对应的类没有所继承的父类,则 Python 会默认继承 object
类。并且 object
是所有 Python 类的父类,它提供了一些常见方法(如 __str__
)的实现。
1 | print(ParentClass1.__bases__) |
继承与抽象
抽象,即抽取类似或者说比较相像的部分。抽象最主要的作用是划分类别(可以隔离关注点,降低复杂度)。
如上图所示,抽象分成两个层次:
1、将小明和小红这两个对象比较像的部分抽取成人类;
2、将人类,猪类,熊类这三个类比较像的部分抽取成父类。
继承,是基于抽象的结果,通过编程语言去实现它,肯定是实现经历了抽象这个过程,才能通过继承的方式去表达出抽象的结构。
抽象只是在分析和设计的过程中的一个动作或者说一种技巧,通过抽象可以得到类。
继承与代码重用
对于猫类和狗类,我们可以按如下方式定义:
1 | class Cat: |
从上述代码中不难看出,吃和喝是猫类和狗类都具有的功能,但是这共有的功能在上面分别为猫和狗的类中编写了两次。如果使用了类的继承则可以实现代码的重用:
1 | lass Animal: |
这就是常说的软件重用,不仅可以重用自己的类,也可以继承别人的,比如标准库,来定制新的数据类型,这样就是大大缩短了软件开发周期,对大型软件开发来说,意义重大。
派生
子类也可以添加自己新的属性或者自己重新定义这些属性(不会影响到父类)。需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
1 | class Animal: |
在上述代码中,像 ha2.life_value
之类的属性引用,会先从实例的命名空间中找 life_value
,然后去类中找,然后再去父类中找,直到最顶级的父类。
super
在子类中,新建的重名的函数属性,在编辑函数内功能的时候,有可能需要重用父类中重名的那个函数功能,应该是用调用普通函数的方式,即:类名.func()
,此时就与调用普通函数无异了,因此即便是 self
参数也要为其传值。
1 | class A: |
在 Python3 中,子类执行父类的方法也可以直接用 super
方法:
1 | class A: |
再比如人狗大战中的应用:
1 |
|
通过继承建立了派生类与基类之间的关系,它是一种 ‘是’ 的关系,比如白马是马,人是动物。当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如教授是老师。
抽象类与接口类
接口类
继承有两种用途:
一:继承基类的方法,并且做出自己的改变或者扩展(代码重用)。
二:声明某个子类兼容于某基类,定义一个接口类 Interface,接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能。
1 | class Alipay: |
开发中容易出现的问题,当一个程序员写好程序后,由于需求变更需要另外一个程序员补充功能:
1 | class Alipay: |
接口初成:主动抛出报异常 NotImplementedError
来解决开发中遇到的问题:
1 | class Payment: |
借用 abc 模块来实现接口:
1 | from abc import ABCMeta, abstractmethod |
实践中,继承的第一种用途意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。
继承的第二种用途非常重要。它又叫“接口继承”。接口继承实质上是要求“做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的所有对象”——这在程序设计上,叫做归一化。
归一化使得外部使用者可以不加区分地处理所有接口兼容的对象集合——就好象 linux 一样,一切皆文件,不必关心它是内存、磁盘、网络还是屏幕(对于底层设计者,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。
1 | 依赖倒置原则: |
在 Python 中根本就没有一个叫做 interface 的关键字,上面的代码只是看起来像接口,其实并没有起到接口的作用,子类完全可以不用去实现接口 ,如果非要去模仿接口的概念,可以借助第三方模块:http://pypi.python.org/pypi/zope.interface 。twisted 的 twisted\internet\interface.py
里使用 zope.interface
,文档 https://zopeinterface.readthedocs.io/en/latest/ 。设计模式:https://github.com/faif/python-patterns
为什么要用接口呢?
接口提取了一群类共同的函数,可以把接口当做一个函数的集合。然后让子类去实现接口中的函数。
这么做的意义在于归一化,什么叫归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。
归一化,让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。
比如:我们定义一个动物接口,接口里定义了有跑、吃、呼吸等接口函数,这样老鼠的类去实现了该接口,松鼠的类也去实现了该接口,由二者分别产生一只老鼠和一只松鼠送到你面前,即便是你分别不到底哪只是什么鼠你肯定知道他俩都会跑,都会吃,都能呼吸。
再比如:我们有一个汽车接口,里面定义了汽车所有的功能,然后由本田汽车的类,奥迪汽车的类,大众汽车的类,他们都实现了汽车接口,这样就好办了,大家只需要学会了怎么开汽车,那么无论是本田,还是奥迪,还是大众我们都会开了,开的时候根本无需关心开的是哪一类车,操作手法(函数调用)都一样。
抽象类
什么是抽象类
与 Java 一样,Python 也有抽象类的概念。但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化。
为什么要有抽象类
如果说类是从一堆对象中抽取相同的内容而来的,那么抽象类就是从一堆类中抽取相同的内容而来的,内容包括数据属性和函数属性。
比如我们有香蕉的类,有苹果的类,有桃子的类,从这些类抽取相同的内容就是水果这个抽象的类,在吃水果时,要么是吃一个具体的香蕉,要么是吃一个具体的桃子,但永远无法吃到一个叫做水果的东西。
从设计角度去看,如果类是从现实对象抽象而来的,那么抽象类就是基于类抽象而来的。
从实现角度来看,抽象类与普通类的不同之处在于:抽象类中有抽象方法,该类不能被实例化,只能被继承,且子类必须实现抽象方法。这一点与接口有点类似,但其实是不同的。
1 | # 一切皆文件 |
抽象类与接口类
抽象类的本质还是类,指的是一组类的相似性,包括数据属性(如 all_type)和函数属性(如 read、write),而接口只强调函数属性的相似性。
抽象类是一个介于类和接口之间的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计
在 Python 中,并没有接口类这种东西,即便不通过专门的模块定义接口,我们也应该有一些基本的概念。
类的三大特性之多态
多态
多态指的是一类事物有多种形态。比如动物有多种形态:人,狗,猪
1 | import abc |
再比如,文件有多种形态:文本文件,可执行文件:
1 | import abc |
多态性
多态性是指在不考虑实例类型的情况下使用实例(在继承的背景下使用时,有时也称为多态性)。在面向对象方法中一般是这样表述多态性:
向不同的对象发送同一条消息(obj.func()
: 是调用了 obj 的方法 func ,又称为向 obj 发送了一条消息 func ),不同的对象在接收时会产生不同的行为(即方法)。
也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。比如:老师.下课铃响了()
和 学生.下课铃响了()
,老师执行的是下班操作,学生执行的是放学操作,虽然二者消息一样,但是执行的效果不同。
1 | peo = People() |
鸭子类型
Python 崇尚鸭子类型,即 “如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子”
Python 程序员通常根据这种行为来编写程序。例如,如果想编写现有对象的自定义版本,可以继承该对象
也可以创建一个外观和行为很像,但与它无任何关系的全新对象,后者通常用于保存程序组件的松耦合度。
例1:利用标准库中定义的各种 ”与文件类似“ 的对象,尽管这些对象的工作方式像文件,但他们没有继承内置文件对象的方法。
1 | # 二者都像鸭子,二者看起来都像文件,因而就可以当文件一样去用 |
鸭子类型不崇尚根据继承所得来的相似,只是自己实现功能就可以了。如果两个类刚好相似,并不产生父类的子类间兄弟关系,这就是鸭子类型。list 和 tuple 两个类比较相似,他们是在写代码的时候约束的,而不是通过父类约束的。
鸭子类型的优点是松耦合,每个相似的类之间都没有影响;缺点是代码写起来太随意,只能靠自觉。
例2:序列类型有多种形态:字符串,列表,元组,但他们之间没有直接的继承关系。
1 | class List(): |