前言
打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 有关面向对象编程中的类和实例的相关内容;
本文为作者的原创作品,转载需注明出处;
类
类的定义
1 | class Student(object): |
- object 是 Student 的父类,表示 Student 继承自 object 类
__init__
是 Student 的构造函数
构造函数的第一个参数self
表示 this
self.name 和 self.score 分别表示 Student 的成员变量 name 和 score;- print_score 表示类的实例方法;
类的类型
1 | Student |
可见,Student 本身是一个 class 对象,其签名是 ‘__main__.Student’;这里可以对比 Java,Java 是将 Class 对象存放在其内存模型的方法区
中,是一个一旦初始化后基本上就不再发生变化的固定内存区域;可以预见的是,Python 一定也会将相关的类的信息存储在内存中的某个区域中。
1 | type(Student) |
class 本身也是一个对象,既然是对象就有其对应的类型,从上述的输出结果可以看到,class 的类型是type
,有关type
的相关内容将会在后续元类的章节中深入描述;
类变量和实例变量
变量分为两种,一种是类变量,一种是实例变量;看一个例子,
1 | class Dog: |
上面分别定义了两个变量,一个是类变量kind
,一个是实例变量name
;
1 | 'Fido') d = Dog( |
类变量相当于 Java 类的静态变量,所有实例所共有的,也可以直接通过类进行调用;实例变量专属于实例本身;
再谈类变量
再谈类变量,类变量的确是所有实例所共有的,但是类变量在赋值过程中,是值拷贝
,什么意思,你想告诉我什么东西?看下面这个例子,
1 | class User: |
直接跳转到第 10 行和第 11 行,我们为实例 u1 进行赋值,将类变量 name 改为 ‘lisi’,但是,你会惊奇的发现,它并不影响 u2 和 User 中的类变量 name;这个和其它语言中的类变量(静态变量)的定义不同,只要一处修改,处处都会修改,因为类似 Java 语言,采用的是引用拷贝
的方式,而这里采用的是值拷贝
的方式,所以,导致 u1 对类变量的修改在其它地方并不会生效;这等价于什么?其实,当通过 User() 得到实例 _u1_ 和 _u2_ 以后,通过值拷贝
,类变量 name 实际上已经变为实例 _u1_ 和 _u2_ 的实例变量了;OK,上面我们看到了类变量转变为实例变量的过程,但是,正规教程中教导我们,实例变量要通过 __init__() 方法来为实例变量赋值,为了彻底搞清楚两种实例变量的形成过程,我们来看看下面这个测试用例,
1 | class User: |
执行,
1 | User.name |
可见,通过 __init__() 方法所生成的实例变量 name 会覆盖通过类变量值拷贝
生成的实例变量 name;
最后,要补充的是,通过类变量构造实例变量的正确姿势是,
1 | 'lisi') u1 = User(name= |
在构造实例的时候,直接通过关键字参数进行初始化;
静态方法、类方法和实例方法
类的方法定义总共分为三种,实例方法、静态方法以及类方法;看下面这个例子,
1 | class Student(object): |
上述的 Student 类分别定义了上述三种类的方法,实例方法、静态方法以及类方法,它们分别是 print_score、print_school_name 以及 print_class_name 方法;实例方法比较好理解,就是必须通过实例来进行调用,并且方法内部通过关键字 self 获取对象本身;那么类方法和静态方法呢?它们的区别是什么?严格说来,类方法也是静态方法,同样可以通过类进行直接调用,但是,唯一的区别是,类方法所接收的第一个参数是当前类既 class 本身,这样,在调用类方法的时候,有个好处就是可以获取类的信息;看看下面执行的结果就一目了然了,
1 | 'Linda', 100) linda = Student( |
动态扩展属性和方法
新增
假设我们要为类的定义小节中的 Student 类动态出一个属性 gender 该如何做呢?
1 | 'Linda Boston', 100) linda = Student( |
可以看到,默认情况下,Student 的实例 linda 是没有属性 gender 的,那么下面,我们为 Student 动态扩展出这样一个属性,(其实,当笔者在写这个测试用例到这里的时候,经验告诉我,这里一定扩展出来的是一个静态的变量而不是一个实例变量,但是,当笔者执行到后面的时候,发现自己彻底错误了,它定义的是一个实例变量,这一点和其它动态语言是有本质区别的,其它动态语言,比如 Javascript,这样动态扩展出来的就是一个静态变量,由所有的实例所共有;)
1 | 'female' Student.gender = |
再次使用 linda 实例进行调用,
1 | linda.gender |
并且,该动态扩展的属性,适用于任何 Student 的实例当中;(另外需要特别注意的是,通过这种方式所动态扩展出来的变量,于实例而言,不是静态变量,而是实例变量,后续通过动态扩展实例方法的时候,大家可以看到这个特性,这就是 Python 的强大之处了)
1 | 'Sam Borge', 60) sam = Student( |
但是,Sam 明明是位男性呀,这样可不太好;Ok,下面笔者将带领大家动态的为 Student 添加一个实例方法;实例方法?你说的是实例方法
,是的,Python 厉害的地方就是,可以直接在 class 上动态扩展出实例方法;
首先,定义方法 set_gender
1 | def set_gender(self, gender): |
注意,定义方法的是时候,记得将第一个参数设置为 self;
再次,将该方法直接赋值给 Student,实现方法的动态扩展;
1 | Student.set_gender = set_gender |
好了,这里我们来思考一下 Javascript 方法级别的动态扩展,Javascript 通过在 prototype 上动态扩展出方法和属性,但是这些方法和属性都是静态的,是所有 Javascript 实例所共享的;那么笔者下面将要演示的是,Python 的强大之处,它可以动态扩展出实例的方法和变量;
1 | 'male') sam.set_gender( |
如果读者读懂了上述的代码,一定会有所惊讶,我们就这样扩展出了 sam 的实例方法 set_gender,并且,成功的通过语句self.gender = gender
将male
赋值给了实例 sam 的成员变量 gender;是的,看到这里,我仍将半信半疑,你能保证 sam.gender 中的变量真的是成员变量(实例变量)而不是全局变量(静态变量)?是的,可以保证,看看下面这行简单的调用,便一目了然了….
1 | linda.gender |
What… linda 的 gender 仍然是 female
,可见在类级别上扩展出来的 gender 变量的确是实例属性;
由此,我们可以总结得到,直接在类上面所动态扩展出来的属性和方法,统统都是实例方法
和实例属性
;这点,尤其需要注意;
删除
可以动态新增变量和方法,同样也可以动态的删除变量和方法,紧接新增小节的内容,让我们来看看如何动态删除属性和方法,
1 | del Student.gender |
这样,我们就删除了 Student 的属性 gender;
1 | linda.gender |
可见属性 gender 已经从 Student 以及其相关的实例中删除了;
实例
初始化
以小节类定定义小节中的 Student 为例,
1 | 'Linda Boston', 100) linda = Student( |
如果为类定义了构造函数,那么初始化的时候,必须输入构造函数中的参数;否则会提示错误;1
2
3
4linda = Student()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'name' and 'score'
实例的类型
1 | linda |
可以看到,linda
是一个 Student 类的一个实例(object
),0x10b78c880 是该实例对象所在的内存地址;
1 | type(linda) |
一个实例本身也是一个对象,对象有自己的类型,从上述输出中可以看到,linda 实例的类型是 Student class;
动态扩展属性和方法
新增
Python 的初始化比较好玩,它可以动态的给一个实例追加属性和方法,首先,我们定义一个空的类,
1 | class Student(object): |
然后,我们初始化得到一个没有任何属性的实例 linda,
1 | linda = Student() |
然后,我们动态的为 linda 实例扩展出相应的属性,
1 | 'Linda Boston' linda.name = |
看到这里,如果学过 Javascript 的同学,一定会惊呼,简直和 Javascript 一模一样;其实也没什么好奇怪的,这个就是动态语言的特性和共性;
最后,我们看看一个最好玩的东西,就是动态扩展出一个方法,首先参照类定定义定义一个方法,
1 | def print_score(self): |
然后,我们有两种方式可以对其进行扩展,
直接赋值的方式,扩展出一个静态方法
我们将这个方法直接赋值给该实例 linda,并将其当做实例方法一样去调用1
2
3
4
5linda.print_score = print_score
linda.print_score()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: print_score() missing 1 required positional argument: 'self'可以看到,如果把 print_score 方法当做是 linda 的实例方法去调用,会报错,提示没有输入位置参数 self;正确的调用方式如下,
1
2linda.print_score(linda)
Student name is Linda Boston and sore is 100可以看到,通过直接赋值的方式动态扩展出来的方法是无法成为实例方法(或称作成员方法)的,这里,也就等价于为 linda 实例添加了一个静态方法 print_score;
借助
types
某块的MethodType
方法,扩展出一个实例方法
那么,如果我们的确需要扩展出一个 linda 的实例方法呢?该怎么操作呢?这里就需要借助types
模块的MethodType
方法了;将上述直接赋值的方式改为如下的方式,1
2
3
4from types import MethodType
linda.print_score = MethodType(print_score, linda)
linda.print_score()
Student name is Linda Boston and sore is 100这样,我们就为实例 linda 扩展出了一个实例方法 print_score
最后,要特别注意的是,为实例动态扩展出的实例属性和静态方法只属于该实例本身;如果想要为所有的实例动态扩展属性和方法,参考类的动态扩展属性和方法;
删除
此部分参考有关类相关的删除小节的内容,两者的操作是一样的;
私有属性
Python 中如何为一个类定义私有属性呢,我们修改类的定义中所使用到的类 Student,
1 | class Student(object): |
我们将成员变量的定义前面加上了一个前缀__
,__name
和__score
,这样呢,就无法通过其实例直接访问到该实例变量了;
1 | 'Linda Boston', 100) linda = Student( |
可以看到,无论是通过 linda.__name 或者是通过 linda.name 都不能直接访问其私有属性了;不过,读者不要以为 Python 在编译器中做了什么强制性的约束,对私有变量的直接访问做了万无一失的约束,对,没有,Python 只是非常简单的将 __name 做了一下重命名而已,将其重命名为了 _Student__name,下面我们来验证一下,
1 | linda._Student__name |
是吧,我们通过这个方式,就访问到了实例 linda 的私有变量;笔者写到这里,突然想到,Python 的确是一门伟大的语言,现在也被用到大量的大数据分析领域,比如 TensorFlow,但是,为什么 Python 始终不为企业级应用开发所青睐呢,比如 IBM、Oracle 和 Alibaba 等等.. 或许就是因为 Python 过于灵活,而为了灵活而丧失了相关的约束性所导致的吧,正如上面的这个私有变量的实现方式,显得是那么的随意;那么,读者会问了,这样定义私有的成员变量不是形同虚设吗?是的,Python 给出的唯一解释就是,Python 程序员应该养成良好的习惯,当遇到 __
前缀的变量的时候,你需要做的,就是“自律”,要把它当做一个私有变量来看待,不要随便的访问它;