CH08_类和对象
CH08_类和对象
本章目标
- 掌握面向对象的思想
- 掌握类的定义和使用
- 掌握构造函数的定义和使用
- 掌握static关键词的使用
面向对象
概述
面向对象编程是在面向过程编程的基础上发展来的,它比面向过程编程具有更强的灵活性和扩展性。面向对象编程是程序员发展的分水岭,很多初学者会因无法理解面向对象而放弃学习编程。
面向对象编程(Object-oriented Programming,简称 OOP),是一种封装代码的方法。其实,在前面章节的学习中,我们已经接触了封装,比如说,将乱七八糟的数据扔进列表中,这就是一种简单的封装,是数据层面的封装;把常用的代码块打包成一个函数,这也是一种封装,是语句层面的封装。
面向对象中,常用术语包括:
- 类:可以理解是一个模板,通过它可以创建出无数个具体实例。比如,前面编写的 tortoise 表示的只是乌龟这个物种,通过它可以创建出无数个实例来代表各种不同特征的乌龟(这一过程又称为类的实例化)。
- 对象:类并不能直接使用,通过类创建出的实例(又称对象)才能使用。这有点像汽车图纸和汽车的关系,图纸本身(类)并不能为人们使用,通过图纸创建出的一辆辆车(对象)才能使用。
- 属性:类中的所有变量称为属性。例如,tortoise 这个类中,bodyColor、footNum、weight、hasShell 都是这个类拥有的属性。
- 方法:类中的所有函数通常称为方法。不过,和函数所有不同的是,类方法至少要包含一个 self 参数(后续会做详细介绍)。例如,tortoise 类中,crawl()、eat()、sleep()、protect() 都是这个类所拥有的方法,类方法无法单独使用,只能和类的对象一起使用。
定义类
语法
Python 中定义一个类使用 class 关键字实现,其基本语法格式如下:
1 | class 类名: |
注意,无论是类属性还是类方法,对于类来说,它们都不是必需的,可以有也可以没有。另外,Python 类中属性和方法所在的位置是任意的,即它们之间并没有固定的前后次序。
和变量名一样,类名本质上就是一个标识符,因此我们在给类起名字时,必须让其符合 Python 的语法。有读者可能会问,用 a、b、c 作为类的类名可以吗?从 Python 语法上讲,是完全没有问题的,但作为一名合格的程序员,我们必须还要考虑程序的可读性。
案例:定义一个 TheFirstDemo 类
1 | class TheFirstDemo: |
和函数一样,我们也可以为类定义说明文档,其要放到类头之后,类体之前的位置,如上面程序中第二行的字符串,就是 TheFirstDemo 这个类的说明文档。
案例: 创建一个没有任何类属性和类方法的类
1 | class Empty: |
可以看到,如果一个类没有任何类属性和类方法,那么可以直接用 pass 关键字作为类体即可。但在实际应用中,很少会创建空类,因为空类没有任何实际意义。
定义构造函数
概述
在创建类时,我们可以手动添加一个 init() 方法,该方法是一个特殊的类实例方法,称为构造方法(或构造函数)。
构造方法用于创建对象时使用,每当创建一个类的实例对象时,Python 解释器都会自动调用它。Python 类中,手动添加构造方法的语法格式如下:
1 | def __init__(self,...): |
注意,此方法的方法名中,开头和结尾各有 2 个下划线,且中间不能有空格。Python 中很多这种以双下划线开头、双下划线结尾的方法,都具有特殊的意义.
另外,init() 方法可以包含多个参数,但必须包含一个名为 self 的参数,且必须作为第一个参数。也就是说,类的构造方法最少也要有一个 self 参数。例如,仍以 TheFirstDemo 类为例,添加构造方法的代码如下所示:
1 | class TheFirstDemo: |
这行代码的含义是创建一个名为 zhangsan 的 TheFirstDemo 类对象。运行代码可看到如下结果:
1 | 调用构造方法 |
注意,即便不手动为类添加任何构造方法,Python 也会自动为类添加一个仅包含 self 参数的构造方法。
仅包含 self 参数的 init() 构造方法,又称为类的默认构造方法。
不仅如此,在 init() 构造方法中,除了 self 参数外,还可以自定义一些参数,参数之间使用逗号“,”进行分割。例如,下面的代码在创建 init() 方法时,额外指定了 2 个参数:
1 | class Shopping: |
注意,由于创建对象时会调用类的构造方法,如果构造函数有多个参数时,需要手动传递参数,传递方式如代码中所示(后续章节会做详细讲解)。
运行以上代码,执行结果为:
1 | 京东的网站为:http://www.jd.com |
可以看到,虽然构造方法中有 self、name、add 3 个参数,但实际需要传参的仅有 name 和 add,也就是说,self 不需要手动传递参数。
关于 self 参数,后续章节会做详细介绍,这里只需要知道,在创建类对象时,无需给 self 传参即可。
对象的创建和使用
对象的实例化
对已定义好的类进行实例化,其语法格式如下:
1 | 类名(参数) |
定义类时,如果没有手动添加 init() 构造方法,又或者添加的 init() 中仅有一个 self 参数,则创建类对象时的参数可以省略不写。
例如,如下代码创建了名为 shopping的类,并对其进行了实例化:
1 | class Shopping : |
在上面的程序中,由于构造方法除 self 参数外,还包含 2 个参数,且这 2 个参数没有设置默认参数,因此在实例化类对象时,需要传入相应的 name 值和 add 值(self 参数是特殊参数,不需要手动传值,Python 会自动传给它值)。
对象的使用
定义的类只有进行实例化,也就是使用该类创建对象之后,才能得到利用。总的来说,实例化后的类对象可以执行以下操作:
- 访问或修改类对象具有的实例变量,甚至可以添加新的实例变量或者删除已有的实例变量;
- 调用类对象的方法,包括调用现有的方法,以及给类对象动态添加方法。
类对象访问变量或方法
使用已创建好的类对象访问类中实例变量的语法格式如下:
1 | 类对象名.变量名 |
使用类对象调用类中方法的语法格式如下:
1 | 对象名.方法名(参数) |
注意,对象名和变量名以及方法名之间用点 “.” 连接。
例如,下面代码演示了如何通过 shopping对象调用类中的实例变量和方法:
1 | #输出name和add实例变量的值 |
运行结果:
1 | 淘宝网 网址为: http://www.taobao.com |
給类对象动态添加、删除变量
Python 支持为已创建好的对象动态增加实例变量,方法也很简单,举个例子:
1 | # 为clanguage对象增加一个money实例变量 |
运行结果:
1 | 159.9 |
可以看到,通过直接增加一个新的实例变量并为其赋值,就成功地为 shopping对象添加了 money 变量。
既然能动态添加,那么是否能动态删除呢?答案是肯定的,使用 del 语句即可实现,例如:
1 | #删除新添加的 money 实例变量 |
运行程序会发现,结果显示 AttributeError 错误:
1 | Traceback (most recent call last): |
給类对象动态添加、删除方法
Python 也允许为对象动态增加方法。以本节开头的 Shopping类为例,由于其内部只包含一个 say() 方法,因此该类实例化出的 shopping对象也只包含一个 say() 方法。但其实,我们还可以为 shopping对象动态添加其它方法。
需要注意的一点是,为 shopping对象动态增加的方法,Python 不会自动将调用者自动绑定到第一个参数(即使将第一个参数命名为 self 也没用)。例如如下代码:
1 | # 先定义一个函数 |
通过借助 types 模块下的 MethodType 可以实现动态添加方法:
1 | def info(self,content): |
可以看到,由于使用 MethodType 包装 info() 函数时,已经将该函数的 self 参数绑定为 clanguage,因此后续再使用 info() 函数时,就不用再给 self 参数绑定值了。
self
在定义类的过程中,无论是显式创建类的构造方法,还是向类中添加实例方法,都要求将 self 参数作为方法的第一个参数。例如,定义一个 Person 类:
1 | class Person: |
那么,self 到底扮演着什么样的角色呢?本节就对 self 参数做详细的介绍。
事实上,Python 只是规定,无论是构造方法还是实例方法,最少要包含一个参数,并没有规定该参数的具体名称。之所以将其命名为 self,只是程序员之间约定俗成的一种习惯,遵守这个约定,可以使我们编写的代码具有更好的可读性(大家一看到 self,就知道它的作用)。
那么,self 参数的具体作用是什么呢?打个比方,如果把类比作造房子的图纸,那么类实例化后的对象是真正可以住的房子。根据一张图纸(类),我们可以设计出成千上万的房子(类对象),每个房子长相都是类似的(都有相同的类变量和类方法),但它们都有各自的主人,那么如何对它们进行区分呢?
当然是通过 self 参数,它就相当于每个房子的门钥匙,可以保证每个房子的主人仅能进入自己的房子(每个类对象只能调用自己的类变量和类方法)。
也就是说,同一个类可以产生多个对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法,换句话说,Python 会自动绑定类方法的第一个参数指向调用该方法的对象。如此,Python解释器就能知道到底要操作哪个对象的方法了。
因此,程序在调用实例方法和构造方法时,不需要手动为第一个参数传值。例如,更改前面的 Person 类,如下所示:
1 | class Person: |
上面代码中,study() 中的 self 代表该方法的调用者,即谁调用该方法,那么 self 就代表谁。因此,该程序的运行结果为:
1 | 正在执行构造方法 |
另外,对于构造函数中的 self 参数,其代表的是当前正在初始化的类对象。举个例子:
1 | class Person: |
运行结果:
1 | zhangsan |
可以看到,zhangsan 在进行初始化时,调用的构造函数中 self 代表的是 zhangsan;而 lisi 在进行初始化时,调用的构造函数中 self 代表的是 lisi。
值得一提的是,除了类对象可以直接调用类方法,还有一种函数调用的方式,例如:
1 | class Person: |
运行结果:
1 | <__main__.Person object at 0x0000025C26F021D0> |
显然,无论采用哪种方法,self 所表示的都是实际调用该方法的对象。
总之,无论是类中的构造函数还是普通的类方法,实际调用它们的谁,则第一个参数 self 就代表谁。
类属性和实例属性
无论是类属性还是类方法,都无法像普通变量或者函数那样,在类的外部直接使用它们。我们可以将类看做一个独立的空间,则类属性其实就是在类体中定义的变量,类方法是在类体中定义的函数。
前面章节提到过,在类体中,根据变量定义的位置不同,以及定义的方式不同,类属性又可细分为以下 3 种类型:
- 类体中、所有函数之外:此范围定义的变量,称为类属性或类变量;
- 类体中,所有函数内部:以“self.变量名”的方式定义的变量,称为实例属性或实例变量;
- 类体中,所有函数内部:以“变量名=变量值”的方式定义的变量,称为局部变量。
类变量(类属性)
类变量指的是在类中,但在各个类方法外定义的变量。举个例子:
1 | class Shopping : |
上面程序中,name 和 add 就属于类变量。
类变量的特点是,所有类的实例化对象都同时共享类变量,也就是说,类变量在所有实例化对象中是作为公用资源存在的。类方法的调用方式有 2 种,既可以使用类名直接调用,也可以使用类的实例化对象调用。
比如,在 Shopping类的外部,添加如下代码:
1 | #使用类名直接调用 |
运行结果:
1 | 淘宝网 |
当然,也可以使用类对象来调用所属类中的类变量。
1 | shop = Shopping() |
运行结果:
1 | 淘宝网 |
注意,因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象。例如,在 Shopping类体外部,添加如下代码:
1 | print("修改前,各类对象中类变量的值:") |
运行结果:
1 | 修改前,各类对象中类变量的值: |
显然,通过类名修改类变量,会作用到所有的实例化对象(例如这里的 shop1和 shop2)。
注意,通过类对象是无法修改类变量的。通过类对象对类变量赋值,其本质将不再是修改类变量的值,而是在给该对象定义新的实例变量
值得一提的是,除了可以通过类名访问类变量之外,还可以动态地为类和对象添加类变量。例如,在 Shopping类的基础上,添加以下代码:
1 | shop = Shopping() |
运行结果:
1 | 13 |
实例变量(实例属性)
实例变量指的是在任意类方法内部,以“self.变量名”的方式定义的变量,其特点是只作用于调用方法的对象。另外,实例变量只能通过对象名访问,无法通过类名访问。
举个例子:
1 | class Shopping : |
此 Shopping 类中,name、add 以及 catalog 都是实例变量。其中,由于 init() 函数在创建类对象时会自动调用,而 say() 方法需要类对象手动调用。因此,Shopping类的类对象都会包含 name 和 add 实例变量,而只有调用了 say() 方法的类对象,才包含 catalog 实例变量。
例如,在上面代码的基础上,添加如下语句:
1 | shop = Shopping() |
运行结果:
1 | 淘宝网 |
类中,实例变量和类变量可以同名,但这种情况下使用类对象将无法调用类变量,它会首选实例变量,这也是不推荐“类变量使用对象名调用”的原因。
另外,和类变量不同,通过某个对象修改实例变量的值,不会影响类的其它实例化对象,更不会影响同名的类变量。例如:
1 | class Shopping : |
运行结果:
1 | 百度 |
不仅如此,Python 只支持为特定的对象添加实例变量。例如,在之前代码的基础上,为 clang 对象添加 money 实例变量,实现代码为:
1 | shop1.money = 30 |
局部变量
除了实例变量,类方法中还可以定义局部变量。和前者不同,局部变量直接以“变量名=值”的方式进行定义,例如:
1 | class Shopping : |
通常情况下,定义局部变量是为了所在类方法功能的实现。需要注意的一点是,局部变量只能用于所在函数中,函数执行完成后,局部变量也会被销毁。
方法
实例方法
通常情况下,在类中定义的方法默认都是实例方法。前面章节中,我们已经定义了不只一个实例方法。不仅如此,类的构造方法理论上也属于实例方法,只不过它比较特殊。
比如,下面的类中就用到了实例方法:
1 | class Shopping: |
实例方法最大的特点就是,它最少也要包含一个 self 参数,用于绑定调用此方法的实例对象(Python 会自动完成绑定)。实例方法通常会用类对象直接调用,例如:
1 | shop = Shopping() |
运行结果:
1 | 正在调用 say() 实例方法 |
当然,Python 也支持使用类名调用实例方法,但此方式需要手动给 self 参数传值。例如:
1 | #类名调用实例方法,需手动给 self 参数传值 |
运行结果:
1 | 正在调用 say() 实例方法 |
类方法
Python 类方法和实例方法相似,它最少也要包含一个参数,只不过类方法中通常将其命名为 cls,Python 会自动将类本身绑定给 cls 参数(注意,绑定的不是类对象)。也就是说,我们在调用类方法时,无需显式为 cls 参数传参。
和 self 一样,cls 参数的命名也不是规定的(可以随意命名),只是 Python 程序员约定俗称的习惯而已。
和实例方法最大的不同在于,类方法需要使用@classmethod
修饰符进行修饰,例如:
1 | class Shopping: |
注意,如果没有 @classmethod,则 Python 解释器会将 fly() 方法认定为实例方法,而不是类方法。
类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)。例如,在上面 Shopping类的基础上,在该类外部添加如下代码:
1 | #使用类名直接调用类方法 |
运行结果:
1 | 正在调用类方法 <class '__main__.Shopping'> |
静态方法
静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。
静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。
静态方法需要使用@staticmethod
修饰,例如:
1 | class Shopping: |
静态方法的调用,既可以使用类名,也可以使用类对象,例如:
1 | #使用类名直接调用静态方法 |
运行结果:
1 | 百度 http://www.baidu.com |
在实际编程中,几乎不会用到类方法和静态方法,因为我们完全可以使用函数代替它们实现想要的功能,但在一些特殊的场景中(例如工厂模式中),使用类方法和静态方法也是很不错的选择。
课后作业
1.略