Python学习笔记——5.类
主要是关于用户自定义类的知识,比较重要。
特点
Python3 中所有的类都会默认继承一个基类 object,因此会具有一些基础的属性/方法。(在 Python2 中,自定义类是否继承 object 会有一些细微的区别)
在 Python 中,一切都是对象,有函数对象,有类对象,还有类得到的实例对象,因此和 C++不同,下文不会使用对象这个词来代表实例,我们需要区分类对象和它产生的实例对象。
Python 类模型的语法太过于自由了,写起来就像搭积木一样自由,并且类和实例对象都支持支持动态修改,写法充满危险。(注:基本数据类型比如 int 不支持动态修改它的属性,只有自定义类型可以)
Python 作为典型的动态类型语言,在使用过程中并不存在对函数参数类型的强制约束,传入的对象只要满足相应的接口要求,就可以完全正常使用。这种处理方式通常被称为鸭子模型:当一个东西走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就可以被称为鸭子。这种放弃类型检查的做法使得代码的编写更加简单,但是也遗留了更多的隐患。
Python vs C++: Python 的语法非常自由,甚至没有提供常量等概念来保证安全,因此程序的健壮性完全依赖程序员自身。下文的某些用法称为习惯上的,代表 Python 开发中的推荐用法,不遵循这些习惯并不会被解释器报错,但是可能出现难以预料的麻烦。 C++的语法非常繁琐,但也非常丰富,即使 C++不断地引入各种约束和补丁来提高安全性,但是 C++可以直接接触底层的特点,以及高效率的要求仍然让代码具有危险性,程序员可能通过各种技巧破坏程序的健壮性。
简单例子
最简单的空类
1 | class Demo: |
带有类的数据属性和普通方法属性的类
1 | class Demo: |
带有构造方法的类,它构造的每一个实例都自动具有一个数据属性
x
1 | class Demo: |
可以使用 dir(ClassName)
查看这个类的所有属性,包括用户定义方法和自带的特殊方法,例如
1 | class Demo: |
或者查看 __dict__
这个特殊属性,会返回一个字典
1 | class Demo: |
类对象
在定义并执行类的定义之后,Python 就会创建一个类对象,这个对象主要有两种操作:
- 类的属性的访问和修改(包括属性的添加,删除等)
- 实例化得到实例对象
类的属性
定义在类当中的变量即类的属性,可以通过类名直接访问和修改,通常包括数据属性和方法属性,类的方法属性就是定义在类中的函数,这里对于方法的介绍非常简略,详细内容见下文的实例调用类的方法属性,以及后面的装饰器部分
例如
1 | class Demo: |
注意:在类的其它方法内部访问类属性时,要加上类名前缀,否则显示变量未定义,类属性相互之间仍然是不可见的。
除了自定义的类属性,还有形如 __XXX__
的特殊属性,有相应的特殊含义,例如 __init__
即构造方法。
类的方法并不要求定义在类的内部,也可以指向一个已经定义的函数
1 | def f(): |
我们甚至可以在类的定义完成之后,动态地添加或修改类的属性
1 | class Demo: |
这是一个很灵活的机制,就像 Python 不需要提前声明变量一样,在 Python 中一个自定义的对象(类对象和下面的实例对象)的属性或者说成员并不是在定义时就固定的,我们可以实时动态修改,给它添加任意的数据或功能。
实例化
在默认情形下,类对象可以用如下语句得到实例对象
1 | class Demo: |
这是最简单的类定义,pass
也可以换成...
占位,因为不允许类的定义部分为空(就像空函数体一样)
如果我们在类的定义中,包含了 __init__
构造方法,那么实例化在最后会调用这个构造方法,我们可以给构造方法提供更多的参数,甚至是不定参数,例如
1 | class Demo: |
如果不显式提供 __init__
方法,相当于采用了默认的只有
self
参数的构造方法。通常不支持多个 __init__
方法,可以使用不定参数 *args,**kwargs
来间接实现。
实例对象
实例的属性
通过类对象的实例化,可以得到实例对象,实例对象和类对象有一个共同点——都是自定义对象,因此都可以动态地修改自己的属性。
例如:
1 | class Demo: |
通常习惯上在 __init__
构造方法中,基于构造参数,添加所有的实例属性,这样可以确保所有的实例在构造之后,都自动具有相应的属性。
1 | class Demo: |
如果上文不使用 self.x
,而是仅仅写作
x
,则会视作方法内部的局部变量,不会视作实例的属性,如果与类属性同名,也不会视作类属性。(C++隐含调用
this
指针,但是 Python 不会)
实例调用类的属性
实例对象和产生它的类对象是具有紧密关系的,这个联系体现在:在读取实例对象的属性时,如果没有在自身找到,那么会向上寻找类对象的同名属性,如果这个属性还是可调用的方法,那么会将自身作为第一个参数传递给方法属性。因此在类的方法属性定义中,习惯上总是在第一个参数使用
self
,无论是否需要。
注意:
- 只有找到的方法属性是类的,实例才会把自己作为第一个参数传递,但是如果这个方法属性是实例自身的,并不会把自己作为第一个参数传递。
- 在修改实例对象的属性时,如果没有在自身找到,就会立刻创建该属性,视作属性的添加,即使有类对象的同名属性,也与之没有关联。见下文
- 在类的方法内部调用其他方法时,仍然要通过
self.xxx()
形式调用
验证上面的一段话,首先我们不给实例对象添加任何属性,此时对于数据属性,作如下实验:
1 | class Demo: |
可以发现,通过实例访问类的数据属性,每次都会动态查找并返回当前类属性的值,但是并不会将它保存下来作为实例的属性。或者查看
__dict__
属性也可以知道:
1 | class Demo: |
对于方法属性,同样的实验
1 | class Demo: |
可以发现,通过实例访问类的方法属性,每次都会动态查找并把自身作为第一个参数传入,并不会将它保存下来作为实例属性。
但是我们继续实验,发现实例访问自己的可调用属性,并不会自动把自己作为第一个参数。
1 | a.f = lambda self:print("hi-2",self.name) |
对于类的方法属性,通过类名和实例名都可以调用,但是尤其注意的时:通过实例调用时会自动将实例自身作为第一个参数(通常为self
)传入方法中。可以通过装饰器来改变这种默认行为,此时就可以把类的方法属性分成三类:
- 普通方法(实例方法)(默认的方法,不使用任何装饰器),通过实例名调用时,会将实例对象作为第一个参数(通过为
self
)传入方法中 - 类方法(基于
@classmethod
装饰器),无论通过类名还是实例名调用,都会将类对象作为第一个参数(通常为cls
)传入方法中 - 静态方法(基于
@staticmethod
装饰器),无论通过类名还是实例名调用,不会进行任何的自动传参
例如 1
2
3
4
5
6
7
8
9
10
11class Demo:
def normal_mathod(self,a):
...
def class_method(cls,b):
...
def static_method(c):
...
实例调用自身属性
如果实例自己显式添加或修改了某些属性,并且与类的属性重名,那么访问这些属性就会直接访问实例属性,而不是类的属性,此时并不会影响类的属性,但是不再可以通过实例名调用了,相当于全局变量被局部变量遮蔽了。
例如
1 | class Demo: |
上面对实例属性的修改并没有影响到类属性。当然由于 Python 变量的底层原理,如果实例属性作为类属性的赋值,还是可能将修改影响到类属性的,例如
1 | class Demo: |
方法的延迟调用
如果把实例调用的方法记录下来,进行延迟调用,在实际调用之前,实例对象和类对象都发生了修改,那么会发生什么?
1 | class Demo: |
上文中的 f
保留了实例的引用,在最后调用时,实例对象的修改被体现出来了,但是类对象的修改并没有体现出来,说明
f
已经锁定了 Demo.do
当时指向的函数对象,并不会随着 Demo.do=Demo.do2
而改变。
私有属性
Python
其实是没有访问权限的概念,就像没有常量概念一样,但是我们在类模型中确实需要私有的属性,习惯上的做法是使用两个下划线开头的标识符,例如
__AAA
或者 __AAA_
。注意不要使用首尾双下划线的
__AAA__
,这代表特殊方法。
Python 解释器会自动修饰这些标识符,在前面加上 _ClassName
前缀,避免误用。
私有数据属性例如
1 | class Demo: |
私有方法属性也是同样的处理,例如
1 | class Demo: |
对于两个下划线开头的 __XXX
,Python
会强制用类名进行名称修饰,主要的考量还是在类的继承中可以遇到的麻烦。
还有一些比较弱的私有概念,仅仅使用一个下划线开头的
_XXX
,这时也是不希望从类的外部(或者模块外部)访问的,但这只是一种约定。
类的继承
Python 中所有的类都继承自 object 这个基类,它提供了通用的一些基础属性。
对于自定义类,继承的语法如下
1 | class Base: |
继承类会记住它的基类,因为在查找继承类的某个属性时,如果找不到就会递归地向上查找。Python 也支持多继承,但是同样会出现菱形继承等问题,不推荐使用。
1 | class Derived(Base1,Base2): |
可以使用两个函数来动态检查继承关系:
isinstance(Instace,Class)
返回 True,当 Instance 是 Class 或者它的子类产生的实例时;issubclass(Class1,Class2)
返回 True,当 Class1 是 Class2 的子类时。
调用基类方法
如果继承类没有重写基类的方法,那么可以直接获取基类的方法属性并调用,如果重写了基类的方法,则存在调用的歧义。
继承类是有必要调用基类的方法的,尤其是在经常重写的构造方法中,有必要先调用基类的构造方法。解决办法有两种,第一种是使用基类的类名调用,例如
1 | class Base: |
第二种选择是直接使用内置函数
super()
,它可以自动获取当前的基类,然后调用基类的方法,例如
1 | class Derived(Base): |
方法重写
继承类可以重写基类的方法,Python 的实例方法调用相当于全都是 C++的
虚函数+引用->动态绑定
,执行哪个方法只取决于当前对象
self
到底是基类还是继承类,例如
1 | class Base: |
这里虽然传递给基类的
__init__
,但是在基类构造方法中,self.play()
调用的还是继承类的对应方法。
如果使用的是类方法调用(在第一个参数处传入实例对象)而不是实例方法调用,那么就不存在这种问题,会严格按照类名去找对应的方法。
1 | class Base: |
结构体
在 Python 中我们有时希望使用类似 C 语言的结构体功能,将一些量简单打包,此时可以借助空类和实例的动态添加属性来实现,例如
1 | class Empty: |
这里的动态修改属性,对于自定义类总是可以的,但是对于基础的类如
int
是不可以的。
运算符重载
在 Python 中对于自定义类的运算符操作,以及 print 输出,本质上会调用相应的特殊方法,因此我们可以通过实现对应特殊方法,来完成运算符重载。
自定义类支持 print 输出,可以通过重写 __str__
方法,生成输出的字符串来完成。
1 | class Point: |
自定义类型支持加法,可以通过重写 __add__
方法来完成。
1 | class Point: |
自定义类型支持等于号判断,可以通过重写 __eq__
方法来完成。
1 | class Point: |
一个最常见的自定义重载是模仿函数调用,通过重写 __call__
方法实现。
1 | class Add: |
其它运算符同理,略。但是注意 Python
并不能对类型进行严格限制,例如下面的操作对于任何具有 x,y
属性的对象,都是可以的。
1 | class Point: |