主要是关于用户自定义类的知识,比较重要。

特点

Python3 中所有的类都会默认继承一个基类 object,因此会具有一些基础的属性/方法。(在 Python2 中,自定义类是否继承 object 会有一些细微的区别)

在 Python 中,一切都是对象,有函数对象,有类对象,还有类得到的实例对象,因此和 C++不同,下文不会使用对象这个词来代表实例,我们需要区分类对象和它产生的实例对象

Python 类模型的语法太过于自由了,写起来就像搭积木一样自由,并且类和实例对象都支持支持动态修改,写法充满危险。(注:基本数据类型比如 int 不支持动态修改它的属性,只有自定义类型可以)

Python 作为典型的动态类型语言,在使用过程中并不存在对函数参数类型的强制约束,传入的对象只要满足相应的接口要求,就可以完全正常使用。这种处理方式通常被称为鸭子模型:当一个东西走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就可以被称为鸭子。这种放弃类型检查的做法使得代码的编写更加简单,但是也遗留了更多的隐患。

Python vs C++: Python 的语法非常自由,甚至没有提供常量等概念来保证安全,因此程序的健壮性完全依赖程序员自身。下文的某些用法称为习惯上的,代表 Python 开发中的推荐用法,不遵循这些习惯并不会被解释器报错,但是可能出现难以预料的麻烦。 C++的语法非常繁琐,但也非常丰富,即使 C++不断地引入各种约束和补丁来提高安全性,但是 C++可以直接接触底层的特点,以及高效率的要求仍然让代码具有危险性,程序员可能通过各种技巧破坏程序的健壮性。

简单例子

最简单的空类

1
2
3
4
5
6
7
8
9
10
class Demo:
pass

a = Demo()

print(a)
print(a.__class__)

# <__main__.Demo object at 0x0000026390D278B0>
# <class '__main__.Demo'>

带有类的数据属性和普通方法属性的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo:
value = 0 # 类的数据属性
def play(self): # 类的普通方法属性
print("play")

a = Demo()

# 普通方法通过实例调用时,实例自动作为第一个self参数
a.play()
# play

# 等价于通过类名调用,需要传入实例对象作为第一个self参数
Demo.play(a)

print(a.value) # 0

带有构造方法的类,它构造的每一个实例都自动具有一个数据属性 x

1
2
3
4
5
6
class Demo:
def __init__(self): # 类的构造方法
self.x = 1 # 实例的数据属性

a = Demo()
print(a.x) # 1

可以使用 dir(ClassName) 查看这个类的所有属性,包括用户定义方法和自带的特殊方法,例如

1
2
3
4
5
6
7
8
9
10
11
class Demo:
value = 1
def hello(self):
pass

dir(Demo)
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
# '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
# '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
# '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
# 'hello', 'value']

或者查看 __dict__ 这个特殊属性,会返回一个字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo:
value = 1
def hello(self):
pass

Demo.__dict__

'''
mappingproxy({'__module__': '__main__',
'value': 1,
'hello': <function __main__.Demo.hello(self)>,
'__dict__': <attribute '__dict__' of 'Demo' objects>,
'__weakref__': <attribute '__weakref__' of 'Demo' objects>,
'__doc__': None})
'''

类对象

在定义并执行类的定义之后,Python 就会创建一个类对象,这个对象主要有两种操作:

  • 类的属性的访问和修改(包括属性的添加,删除等)
  • 实例化得到实例对象

类的属性

定义在类当中的变量即类的属性,可以通过类名直接访问和修改,通常包括数据属性和方法属性,类的方法属性就是定义在类中的函数,这里对于方法的介绍非常简略,详细内容见下文的实例调用类的方法属性,以及后面的装饰器部分

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Demo:
value = 1 # 类的数据属性
def f(): # 类的方法属性,注意这里并没有包括self参数,并不建议这么写,见下文
print("hi")

# 通过类名,访问与修改类的数据属性
print(Demo.value) # 1
Demo.value = 2
print(Demo.value) # 2

# 通过类名,访问与修改类的方法属性
Demo.f() # hi
Demo.f = lambda : print("hello")
Demo.hello() # hello

注意:在类的其它方法内部访问类属性时,要加上类名前缀,否则显示变量未定义,类属性相互之间仍然是不可见的。

除了自定义的类属性,还有形如 __XXX__ 的特殊属性,有相应的特殊含义,例如 __init__ 即构造方法。

类的方法并不要求定义在类的内部,也可以指向一个已经定义的函数

1
2
3
4
5
6
7
def f():
print("hi")

class Demo:
hi = f # 方法属性

Demo.f() # hi

我们甚至可以在类的定义完成之后,动态地添加或修改类的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Demo:
... # 空类,什么也没有

# 添加属性
Demo.x = 1
Demo.f = lambda a,b:a+b

print(Demo.x) # 1
print(Demo.f(1,2)) # 3

# 修改属性
Demo.x = 2
print(Demo.x) # 2

# 修改属性,可以把方法属性改成数据属性,没什么本质不同
Demo.f = 100
print(Demo.f) # 100

# 删除属性
del Demo.x
print(Demo.x) # 报错,Demo.x已经被删除

这是一个很灵活的机制,就像 Python 不需要提前声明变量一样,在 Python 中一个自定义的对象(类对象和下面的实例对象)的属性或者说成员并不是在定义时就固定的,我们可以实时动态修改,给它添加任意的数据或功能。

实例化

在默认情形下,类对象可以用如下语句得到实例对象

1
2
3
4
class Demo:
pass

a = Demo() # 实例化

这是最简单的类定义,pass也可以换成...占位,因为不允许类的定义部分为空(就像空函数体一样)

如果我们在类的定义中,包含了 __init__ 构造方法,那么实例化在最后会调用这个构造方法,我们可以给构造方法提供更多的参数,甚至是不定参数,例如

1
2
3
4
5
6
7
8
class Demo:
def __init__(self, v):
print("v =", v)

a = Demo(1) # 实例化
# v = 1

b = Demo() # 语法错误,__init__缺少参数

如果不显式提供 __init__ 方法,相当于采用了默认的只有 self 参数的构造方法。通常不支持多个 __init__ 方法,可以使用不定参数 *args,**kwargs 来间接实现。

实例对象

实例的属性

通过类对象的实例化,可以得到实例对象,实例对象和类对象有一个共同点——都是自定义对象,因此都可以动态地修改自己的属性。

例如:

1
2
3
4
5
6
7
8
9
10
11
class Demo:
... # 空类

a = Demo() # 实例化得到实例对象

# 添加实例的属性
a.x = 1
a.f = lambda : print("hi")

print(a.x) # 1
a.f() # hi

通常习惯上在 __init__ 构造方法中,基于构造参数,添加所有的实例属性,这样可以确保所有的实例在构造之后,都自动具有相应的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Demo:
def __init__(self, xx, yy):
self.x = xx # 添加self.x属性
self.y = yy # 添加self.y属性

def enhance(self,zz):
self.z = zz # 添加self.z属性

a = Demo(1,2) # a自动具有两个属性值1,2
b = Demo(0,1) # b自动具有两个属性值0,1

# 只有调用了enhance之后,才会具有self.z属性
a.enhance(3) # a具有三个属性

如果上文不使用 self.x,而是仅仅写作 x,则会视作方法内部的局部变量,不会视作实例的属性,如果与类属性同名,也不会视作类属性。(C++隐含调用 this 指针,但是 Python 不会)

实例调用类的属性

实例对象和产生它的类对象是具有紧密关系的,这个联系体现在:在读取实例对象的属性时,如果没有在自身找到,那么会向上寻找类对象的同名属性,如果这个属性还是可调用的方法,那么会将自身作为第一个参数传递给方法属性。因此在类的方法属性定义中,习惯上总是在第一个参数使用 self,无论是否需要。

注意:

  • 只有找到的方法属性是类的,实例才会把自己作为第一个参数传递,但是如果这个方法属性是实例自身的,并不会把自己作为第一个参数传递。
  • 在修改实例对象的属性时,如果没有在自身找到,就会立刻创建该属性,视作属性的添加,即使有类对象的同名属性,也与之没有关联。见下文
  • 在类的方法内部调用其他方法时,仍然要通过self.xxx()形式调用

验证上面的一段话,首先我们不给实例对象添加任何属性,此时对于数据属性,作如下实验:

1
2
3
4
5
6
7
8
9
10
11
class Demo:
... # 空类,什么也没有

# 添加属性
Demo.s = 10

a = Demo()
print(a.s) # 10

Demo.s = 30
print(a.s) # 30

可以发现,通过实例访问类的数据属性,每次都会动态查找并返回当前类属性的值,但是并不会将它保存下来作为实例的属性。或者查看 __dict__ 属性也可以知道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Demo:
... # 空类,什么也没有

# 添加属性
Demo.s = 10

a = Demo()
print(a.s) # 10

# a 并没有把s作为自己的属性保存下来
print(a.__dict__) # {}

# 这里并不会影响类的属性,而是给a添加了一个属性a.s,这个属性和Demo.s相互独立
a.s = 1

print(a.s) # 1
print(Demo.s) # 10
print(a.__dict__) # {'s': 1}

对于方法属性,同样的实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Demo:
... # 空类,什么也没有

# 添加属性
Demo.f = lambda self:print("hi-1",self.name)

a = Demo()

a.name = 'a'

a.f() # hi-1 a

Demo.f = lambda self:print("hi-2",self.name)
a.f() # hi-2 a

a.name = 'A'
Demo.f = lambda self:print("hi-3",self.name)
a.f() # hi-3 A

Demo.f(a) # hi-3 A

可以发现,通过实例访问类的方法属性,每次都会动态查找并把自身作为第一个参数传入,并不会将它保存下来作为实例属性。

但是我们继续实验,发现实例访问自己的可调用属性,并不会自动把自己作为第一个参数。

1
2
a.f = lambda self:print("hi-2",self.name)
a.f() # 报错,缺少self参数

对于类的方法属性,通过类名和实例名都可以调用,但是尤其注意的时:通过实例调用时会自动将实例自身作为第一个参数(通常为self)传入方法中。可以通过装饰器来改变这种默认行为,此时就可以把类的方法属性分成三类:

  • 普通方法(实例方法)(默认的方法,不使用任何装饰器),通过实例名调用时,会将实例对象作为第一个参数(通过为self)传入方法中
  • 类方法(基于 @classmethod 装饰器),无论通过类名还是实例名调用,都会将类对象作为第一个参数(通常为cls)传入方法中
  • 静态方法(基于 @staticmethod 装饰器),无论通过类名还是实例名调用,不会进行任何的自动传参

例如

1
2
3
4
5
6
7
8
9
10
11
class Demo:
def normal_mathod(self,a):
...

@classmethod
def class_method(cls,b):
...

@staticmethod
def static_method(c):
...

实例调用自身属性

如果实例自己显式添加或修改了某些属性,并且与类的属性重名,那么访问这些属性就会直接访问实例属性,而不是类的属性,此时并不会影响类的属性,但是不再可以通过实例名调用了,相当于全局变量被局部变量遮蔽了。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Demo:
value = 1
def f(self):
print("hi")

a = Demo()

a.value = 2
print(a.value) # 2
print(Demo.value) # 1

# 注意这里的f没有任何参数
a.f = lambda : print("hello")
a.f() # hello

# 注意这里的f有一个self参数
Demo().f() # hi

上面对实例属性的修改并没有影响到类属性。当然由于 Python 变量的底层原理,如果实例属性作为类属性的赋值,还是可能将修改影响到类属性的,例如

1
2
3
4
5
6
7
8
9
class Demo:
lis = []

a = Demo()
a.lis = Demo.lis
a.lis.append('a')

print(Demo.lis)
# ['a']

方法的延迟调用

如果把实例调用的方法记录下来,进行延迟调用,在实际调用之前,实例对象和类对象都发生了修改,那么会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Demo:
def __init__(self,name):
self.name = name
def do(self,v):
print(self.name," do: ",v)
def do2(self,v):
print(self.name," do2: ",v)

a = Demo("dog")
a.do("run")
# dog do: run

f = a.do # 记录并延迟调用
f("swim")
# dog do: swim

# 修改类对象
Demo.do = Demo.do2
# 修改实例对象
a.name = "cat"

f("sleep") # 延迟调用
# cat do: sleep

上文中的 f 保留了实例的引用,在最后调用时,实例对象的修改被体现出来了,但是类对象的修改并没有体现出来,说明 f 已经锁定了 Demo.do 当时指向的函数对象,并不会随着 Demo.do=Demo.do2 而改变。

私有属性

Python 其实是没有访问权限的概念,就像没有常量概念一样,但是我们在类模型中确实需要私有的属性,习惯上的做法是使用两个下划线开头的标识符,例如 __AAA 或者 __AAA_。注意不要使用首尾双下划线的 __AAA__,这代表特殊方法。

Python 解释器会自动修饰这些标识符,在前面加上 _ClassName 前缀,避免误用。

私有数据属性例如

1
2
3
4
5
6
7
8
9
class Demo:
def __init__(self):
self.__value = 1 # 私有数据属性,在其它方法中也可以使用self.__value

a = Demo()
print(a.__value) # 报错,找不到实例的__value属性

# 因为名称被解释器自动修饰过了,真正的名称如下,还是可以访问到的
print(a._Demo__value) # 1

私有方法属性也是同样的处理,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Demo:
def __do_something(self): # 私有方法属性
print("do something")

def do(self):
self.__do_something()

a = Demo()

a.__do_something() # 报错,在外面找不到这个方法属性

a.do() # 可以使用公开的方法
# do something

a._Demo__do_something() # 或者使用修饰后的名称也是可以的
# do something

对于两个下划线开头的 __XXX,Python 会强制用类名进行名称修饰,主要的考量还是在类的继承中可以遇到的麻烦。 还有一些比较弱的私有概念,仅仅使用一个下划线开头的 _XXX,这时也是不希望从类的外部(或者模块外部)访问的,但这只是一种约定。

类的继承

Python 中所有的类都继承自 object 这个基类,它提供了通用的一些基础属性。

对于自定义类,继承的语法如下

1
2
3
4
5
class Base:
...

class Derived(Base):
...

继承类会记住它的基类,因为在查找继承类的某个属性时,如果找不到就会递归地向上查找。Python 也支持多继承,但是同样会出现菱形继承等问题,不推荐使用。

1
2
class Derived(Base1,Base2):
...

可以使用两个函数来动态检查继承关系:

  • isinstance(Instace,Class) 返回 True,当 Instance 是 Class 或者它的子类产生的实例时;
  • issubclass(Class1,Class2) 返回 True,当 Class1 是 Class2 的子类时。

调用基类方法

如果继承类没有重写基类的方法,那么可以直接获取基类的方法属性并调用,如果重写了基类的方法,则存在调用的歧义。

继承类是有必要调用基类的方法的,尤其是在经常重写的构造方法中,有必要先调用基类的构造方法。解决办法有两种,第一种是使用基类的类名调用,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base:
def __init__(self,v):
self.v = v

class Derived(Base):
def __init__(self,u,v):
Base.__init__(self,v)
self.u = u


a = Derived(1,2)
print(a.u) # 1
print(a.v) # 2

第二种选择是直接使用内置函数 super(),它可以自动获取当前的基类,然后调用基类的方法,例如

1
2
3
4
class Derived(Base):
def __init__(self,u,v):
super().__init__(self,v)
self.u = u

方法重写

继承类可以重写基类的方法,Python 的实例方法调用相当于全都是 C++的 虚函数+引用->动态绑定,执行哪个方法只取决于当前对象 self 到底是基类还是继承类,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base:
def __init__(self,v):
self.v = v
self.play()
def play(self):
print("Base play")

class Derived(Base):
def __init__(self,u,v):
Base.__init__(self,v)
self.u = u
self.play()

def play(self): # 重写
print("Derived play")

a = Derived(1,2)

# Derived play
# Derived play

这里虽然传递给基类的 __init__,但是在基类构造方法中,self.play() 调用的还是继承类的对应方法。

如果使用的是类方法调用(在第一个参数处传入实例对象)而不是实例方法调用,那么就不存在这种问题,会严格按照类名去找对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base:
def __init__(self,v):
self.v = v
Base.play(self)
def play(self):
print("Base play")

class Derived(Base):
def __init__(self,u,v):
Base.__init__(self,v)
self.u = u
self.play() # 或者 Derived.play(self)

def play(self): # 重写
print("Derived play")

a = Derived(1,2)

# Base play
# Derived play

结构体

在 Python 中我们有时希望使用类似 C 语言的结构体功能,将一些量简单打包,此时可以借助空类和实例的动态添加属性来实现,例如

1
2
3
4
5
6
7
class Empty:
...

a = Empty()
a.time = 100
a.cost = 19
a.str = 'abcd'

这里的动态修改属性,对于自定义类总是可以的,但是对于基础的类如 int 是不可以的。

运算符重载

在 Python 中对于自定义类的运算符操作,以及 print 输出,本质上会调用相应的特殊方法,因此我们可以通过实现对应特殊方法,来完成运算符重载。

自定义类支持 print 输出,可以通过重写 __str__ 方法,生成输出的字符串来完成。

1
2
3
4
5
6
7
8
9
10
11
12
class Point:
def __init__(self, x , y):
self.x = x
self.y = y

def __str__(self):
return "({0},{1})".format(self.x,self.y)

p = Point(1,2)

print(p)
# (1,2)

自定义类型支持加法,可以通过重写 __add__ 方法来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point:
def __init__(self, x , y):
self.x = x
self.y = y

def __str__(self):
return "({0},{1})".format(self.x,self.y)

def __add__(self,other):
x = self.x + other.x
y = self.y + other.y
return Point(x,y)

p1 = Point(1,2)
p2 = Point(3,4)
print(p1+p2)
# (4,6)

自定义类型支持等于号判断,可以通过重写 __eq__ 方法来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point:
def __init__(self, x , y):
self.x = x
self.y = y

def __eq__(self,other):
return (self.x == other.x) and (self.y == other.y)

p1 = Point(1,2)
p2 = Point(3,4)
print(p1==p2)
# False

p3 = Point(1,2)
print(p1==p3)
# True

一个最常见的自定义重载是模仿函数调用,通过重写 __call__ 方法实现。

1
2
3
4
5
6
7
8
class Add:
def __init__(self,x):
self.x = x
def __call__(self,y):
return self.x + y

a = Add(3)
print(a(5)) # 8

其它运算符同理,略。但是注意 Python 并不能对类型进行严格限制,例如下面的操作对于任何具有 x,y 属性的对象,都是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point:
def __init__(self, x , y):
self.x = x
self.y = y

def __eq__(self,other):
return (self.x == other.x) and (self.y == other.y)

class Empty:
...


p = Point(1,2)

q = Empty()
q.x = 1; q.y = 2

print(p==q)
# True