Python 中的面对对象中的概念中,和 Java 不同的只有字段,在 Python 中直接叫做 属性 . 并且 Python 中的类其实就是一个独立的 命名空间 这个命名空间使用类名来调用,里面有变量,有方法(函数),像极了另外的一个 Python 文件。

所以,类属性其实就是另外命名空间的全局变量,方法其实就是另外命名空间的函数!!!

变量

在 Python 面对对象中使用的变量有三种类型:

  1. 类体中、所有函数之外:此范围定义的变量,称为类属性或类变量;(类命名空间的普通变量,Java 中的静态属性)
  2. 类体中,所有函数内部:以“self.变量名”的方式定义的变量,称为实例属性或实例变量;(self 保存结构体地址的变量集合,Java 中的属性)
  3. 类体中,所有函数内部:以“变量名=变量值”的方式定义的变量,称为局部变量。(类命名空间函数的局部变量,Java 方法的局部变量)

同时三者中的属性,也就是类属性和实例属性,默认都是 public 类型,如果想要设置 private 类型,则需要在属性名字前面加上 ____name,外界访问就会报错。当然也可以加一个 _ 外界虽然也能调用,但是编译器不会给提示,约定俗成为 private 类型。

并且类属性和实例属性都支持在类外面动态添加属性。


  • 类属性 ,可以通过类名进行访问和修改。

比较不同的是,Python 支持动态的为类添加类属性,直接赋值就可以了(可以理解为动态语言直接在叫做 类名 的命名空间文件中,修改了代码):

1
2
3
4
5
6
class People:  
name = "People" # 通过类名访问的静态字段 name

People.age = 18 # 动态添加类属性

print(People.age) # 访问类属性

  • 实例属性 ,也就是 Java 中的普通字段,通过具体的实例进行访问。比较不同的是,Python 只能在方法中进行声明,并且需要以 self.name=XXX 来声明名字为 name 的实例对象。(实例就是保存实例变量的结构体地址)

实例属性也支持动态的添加,但是是依托于某一个具体的实例进行属性添加,对于其他属性,没有影响。

1
2
3
4
5
class People:  
def __init__(self): # 建议实例属性都放在构造器 __init__ 里面声明
self.age = 18 # 以 self 开头,为实例属性

print(People().age) # 通过新建实例来访问

  • 局部变量 ,这个和函数里面的概念是一样的,直接 name=value 的形式就创建了在当前方法内部有效的局部变量。(因为本质上就是一个类命名空间里面的函数呀)

方法

Python 不支持方法重载。如果有两个重名的方法,则后面的会覆盖前面的。

方法也是分为三种:类方法,实例方法和静态方法。

但是这三种方法在本质上都是一样的,都是类命名空间下面的普通函数,只不过根据必须传递的参数不同进行了划分。必须传递实例地址 self 的为实例方法,必须传递自己类命名空间的地址的方法叫做类方法,而静态方法就是什么都不用传递的普通方法。

因为类中定义的方法默认是需要 self 的实例方法,所以要定义其他两种就需要注解来标记一下。


可以通过在方法名字前面加上 __ 来将方法改成 private 类型。可以加上 _ 变成假 private 类型。


Python 同样支持对三种方法进行动态添加,只要先定义一个符合规范的方法,然后按照动态添加字段的方式,就可以动态添加方法了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CLanguage:  
pass
#下面定义了一个实例方法
def info(self):
print("正在调用实例方法")
#下面定义了一个类方法
@classmethod
def info2(cls):
print("正在调用类方法")
#下面定义个静态方法
@staticmethod
def info3():
print("正在调用静态方法")
#类可以动态添加以上 3 种方法,会影响所有实例对象
CLanguage.info = info
CLanguage.info2 = info2
CLanguage.info3 = info3
clang = CLanguage()

#类实例对象只能动态添加实例方法,不会影响其它实例对象
clang1 = CLanguage()
clanginfo = info
#必须手动为 self 传值
clanginfo(clang1)

尽量别改代码,不然维护起来,真的是,无语呀。


  • 实例方法 ,类中定义的默认方法都是实例方法(必须有参数 self),也就是必须需要实例才可以访问的方法。

有两种调用方式,一种是实例直接调用(self 自动传入),一种是类名调用,但是需要传入 self 参数:

1
2
3
4
5
6
7
8
class People:  
def say_hello(self):
print("Hello!")

xorex = People()
xorex.say_hello() # 通过实例调用实例方法

People.say_hello(xorex) # 通过类名调用实例方法

  • 静态方法 ,静态方法就等同于 Java 的静态方法。需要在方法前面加上 @staticmethod,通过类名访问。
1
2
3
4
5
6
class People:  
@staticmethod
def Hello():
print("Hello")

People.Hello()

  • 类方法 ,类方法有点类似于 Java 的静态方法,需要在方法前面加上注解 @classmethod,但是不一样的是,这个方法绑定了类本身,将类本身作为第一个默认参数传了进去,约定俗成这个参数名字为:cls
1
2
3
4
5
6
class People:  
@classmethod
def what_is_classmethod(cls):
print(cls is People) # 输出内容为 True,可以看到 cls 就是类 People 的地址。

People.what_is_classmethod()

你可能想问,这个类方法有什么优越的地方吗,当然是有的,那就是需要用到但又无法明确一个类的时候。这是类方法的 cls 就有了作用。

比如我们要写一个父类,父类的要写一个方法,这个方法需要调用本类的构造方法,来对本类的构造方法进行固定加工。但是实际使用的时候都是子类去调用,那么这个处理方法自然也要调用的是子类的构造方法。在不明确子类是谁的时候,就需要 classmethod 的 cls 参数代替了!

1
2
3
4
5
6
# 写一个父类 Date,其中的一个父类方法,会被子类继承调用
@classmethod
def from_string(cls, date_as_string):
  day, month, year = map(int, date_as_string.split('-'))
  date1 = cls(day, month, year) # 想想这里 cls 为什么不能用具体的类名代替,不同子类的构造方法可能不一样
  return date1

类封装性原理

前面说在属性或者方法前面加 _ 实现约定俗成的私有,但是仍然可调用的私有方法。

而名字加上 __ 则变成真正不可调用的私有方法/属性。其实这个方法/属性调用接口还是存在的,只不过被 Python 改了名字,改成了 _类名__属性名/方法名",只要前面加上 _类名 还是可以调用这个私有方法/属性的。

只能说,Python 没有完全私有,还可以调用。

继承

默认继承

在 Python 中,所有没有明面继承其他类的类,都会默认继承基类 object!

单继承

Python 支持继承和多继承,使用方法就是在定义类的时候,在类名后面加小括号,里面塞进去要继承的类名即可,就可以将父类的代码当作在同一命名空间使用了。

1
2
3
4
5
6
7
8
9
10
11
class Father:  
fatherStatic = "FatherStatic"

def __init__(self):
self.fatherField = "FatherField"

class Son(Father): # 继承类 Father
sonStatic = "SonStatic"

def __init__(self):
self.sonField = "SonField"

上面的代码就等价于:

1
2
3
4
5
6
7
8
9
10
class Son:  
fatherStatic = "FatherStatic"

def __init__(self):
self.fatherField = "FatherField"

sonStatic = "SonStatic"

def __init__(self):
self.sonField = "SonField"

因此在这种机制下,继承了父类的代码之后,父类定义的类属性和各种方法都得到了继承,但是一旦和子类有命名冲突,因为子类的定义在后面,所以会覆盖父类。(一定会被覆盖的就是 init 方法,所以说父类在 init 声明的实例属性无法被继承后的父类方法和子类使用)

也因此创建子对象的时候,并不会像 Java 一样自动调用无参父类的构造方法。


我们知道被覆盖的方法仅仅只是因为调用顺序的原因,Java 提供 super 实例来通过定义调用偏移量实现对覆盖的父类方法和属性的访问。

Python 则可以通过 父类名.方法(self) 名手动绑定 self 对象的方式访问父类中的方法。

1
2
3
4
5
6
7
8
9
10
11
class Father:  
def Hello(self):
print("Father Hello")

class Son(Father):
def Hello(self):
print("Son Hello")

son = Son()
son.Hello()
Father.Hello(son) # 借助类访问方法,需要自己填充实例对象 self

多继承

Python 的多继承就是在类名字后面的小括号里面,多写几个父类,至于命名冲突之后的访问顺序,请参考后面介绍的 MRO 机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Father1:  
father1Static = "Father1Static"

def Hello(self):
print(Fatherfather1Static)

class Father2:
father2Static = "Father2Static"

def Hello(self):
print(Fatherfather2Static)

class Son(Father1,Father2): # 多继承 Father1 和 Father2,优先级从左到右
sonStatic = "SonStatic"

def __init__(self):
self.sonField = "SonField"

son = Son()
son.Hello() # 说明 Father1 优先级高于 Father2
print(Son.father1Static) # 继承了 Father1
print(Son.father2Static) # 继承了 Father2

但是上面的仅仅只是很简单的多继承关系,如果遇到了更复杂的,犹如很乱的多层树结构继承的时候,就需要根据 MRO 去分析了。

MRO 机制

MRO 全称 Method Resolution Order,方法解析顺序。

可以通过调用类名的类属性 __mro__ 来获取类的优先级顺序,会返回一个元组。当然聪明的我们可以自己分析复杂的 __mro__ :


1
2
3
4
5
6
7
8
9
10
11
class A:  
def method(self):
print("CommonA")
class B(A):
pass
class C(A):
def method(self):
print("CommonC")
class D(B, C):
pass
print(D().method())

对于上面的程序,把各个类的 MRO 记为如下等式:

1
2
3
4
类 A:L[A] = merge(A , object)  
类 B:L[B] = [B] + merge(L[A] , [A])
类 C:L[C] = [C] + merge(L[A] , [A])
类 D:L[D] = [D] + merge(L[B] , L[C] , [B] , [C])

注意,以类 A 等式为例,其中 merge 包含的 A 称为 L[A] 的头,剩余元素(这里仅有一个 object)称为尾。

这里的关键在于 merge,它的运算方式如下:

  1. 检查第一个列表的头元素(如 L[A] 的头),记作 H。
  2. 若 H 未出现在 merge 中其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤 1;否则,取出下一个列表的头部记作 H,继续该步骤。
  3. 重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,Python 会抛出异常。

由此,可以计算出类 B 的 MRO,其计算过程为:

1
2
3
4
L[B] = [B] + merge(L[A],[A])  
= [B] + merge([A,object],[A])
= [B,A] + merge([object])
= [B,A,object]

同理,其他类的 MRO 也可以轻松计算得出。

父类的初始化

因为 Python 的继承不会像 Java 一样,创建子对象的时候,是不会自动调用父类的构造方法的(因为方法覆盖,只会调用最后面的那个构造方法),所以如果要调用父类的构造方法来完成初始化,就要手动调用。

有两种方法:

  • 通过 super().__init__() 调用上一个优先级的父类的构造方法。

  • 通过 父类名.__init__(self) 手动绑定实例来调用父类的构造方法。

不过建议只在单继承中使用 super() 在多继承中只用后者。

多态实现

因为 Python 是动态类型,所以多态机制简直不要太好实现,根本不需要继承!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def polymorphic(self):  
self.Hello()

class One:
def Hello(self):
print("One Hello")

class Two:
def Hello(self):
print("Two Hello")

all = (One(),Two())

for i in all: # 多态实现
polymorphic(i)