Python-04-面向对象
类
Python 中的面对对象中的概念中,和 Java 不同的只有字段,在 Python 中直接叫做 属性
. 并且 Python 中的类其实就是一个独立的 命名空间
这个命名空间使用类名来调用,里面有变量,有方法(函数),像极了另外的一个 Python 文件。
所以,类属性其实就是另外命名空间的全局变量,方法其实就是另外命名空间的函数!!!
变量
在 Python 面对对象中使用的变量有三种类型:
- 类体中、所有函数之外:此范围定义的变量,称为类属性或类变量;(类命名空间的普通变量,Java 中的静态属性)
- 类体中,所有函数内部:以“self.变量名”的方式定义的变量,称为实例属性或实例变量;(self 保存结构体地址的变量集合,Java 中的属性)
- 类体中,所有函数内部:以“变量名=变量值”的方式定义的变量,称为局部变量。(类命名空间函数的局部变量,Java 方法的局部变量)
同时三者中的属性,也就是类属性和实例属性,默认都是 public 类型,如果想要设置 private 类型,则需要在属性名字前面加上 __
如 __name
,外界访问就会报错。当然也可以加一个 _
外界虽然也能调用,但是编译器不会给提示,约定俗成为 private 类型。
并且类属性和实例属性都支持在类外面动态添加属性。
- 类属性 ,可以通过类名进行访问和修改。
比较不同的是,Python 支持动态的为类添加类属性,直接赋值就可以了(可以理解为动态语言直接在叫做 类名 的命名空间文件中,修改了代码):
1 | class People: |
- 实例属性 ,也就是 Java 中的普通字段,通过具体的实例进行访问。比较不同的是,Python 只能在方法中进行声明,并且需要以
self.name=XXX
来声明名字为 name 的实例对象。(实例就是保存实例变量的结构体地址)
实例属性也支持动态的添加,但是是依托于某一个具体的实例进行属性添加,对于其他属性,没有影响。
1 | class People: |
- 局部变量 ,这个和函数里面的概念是一样的,直接
name=value
的形式就创建了在当前方法内部有效的局部变量。(因为本质上就是一个类命名空间里面的函数呀)
方法
Python 不支持方法重载。如果有两个重名的方法,则后面的会覆盖前面的。
方法也是分为三种:类方法,实例方法和静态方法。
但是这三种方法在本质上都是一样的,都是类命名空间下面的普通函数,只不过根据必须传递的参数不同进行了划分。必须传递实例地址 self 的为实例方法,必须传递自己类命名空间的地址的方法叫做类方法,而静态方法就是什么都不用传递的普通方法。
因为类中定义的方法默认是需要 self 的实例方法,所以要定义其他两种就需要注解来标记一下。
可以通过在方法名字前面加上 __
来将方法改成 private 类型。可以加上 _
变成假 private 类型。
Python 同样支持对三种方法进行动态添加,只要先定义一个符合规范的方法,然后按照动态添加字段的方式,就可以动态添加方法了:
1 | class CLanguage: |
尽量别改代码,不然维护起来,真的是,无语呀。
- 实例方法 ,类中定义的默认方法都是实例方法(必须有参数 self),也就是必须需要实例才可以访问的方法。
有两种调用方式,一种是实例直接调用(self 自动传入),一种是类名调用,但是需要传入 self 参数:
1 | class People: |
- 静态方法 ,静态方法就等同于 Java 的静态方法。需要在方法前面加上
@staticmethod
,通过类名访问。
1 | class People: |
- 类方法 ,类方法有点类似于 Java 的静态方法,需要在方法前面加上注解
@classmethod
,但是不一样的是,这个方法绑定了类本身,将类本身作为第一个默认参数传了进去,约定俗成这个参数名字为:cls
1 | class People: |
你可能想问,这个类方法有什么优越的地方吗,当然是有的,那就是需要用到但又无法明确一个类的时候。这是类方法的 cls 就有了作用。
比如我们要写一个父类,父类的要写一个方法,这个方法需要调用本类的构造方法,来对本类的构造方法进行固定加工。但是实际使用的时候都是子类去调用,那么这个处理方法自然也要调用的是子类的构造方法。在不明确子类是谁的时候,就需要 classmethod 的 cls 参数代替了!
1 | # 写一个父类 Date,其中的一个父类方法,会被子类继承调用 |
类封装性原理
前面说在属性或者方法前面加 _
实现约定俗成的私有,但是仍然可调用的私有方法。
而名字加上 __
则变成真正不可调用的私有方法/属性。其实这个方法/属性调用接口还是存在的,只不过被 Python 改了名字,改成了 _类名__属性名/方法名"
,只要前面加上 _类名
还是可以调用这个私有方法/属性的。
只能说,Python 没有完全私有,还可以调用。
继承
默认继承
在 Python 中,所有没有明面继承其他类的类,都会默认继承基类 object!
单继承
Python 支持继承和多继承,使用方法就是在定义类的时候,在类名后面加小括号,里面塞进去要继承的类名即可,就可以将父类的代码当作在同一命名空间使用了。
1 | class Father: |
上面的代码就等价于:
1 | class Son: |
因此在这种机制下,继承了父类的代码之后,父类定义的类属性和各种方法都得到了继承,但是一旦和子类有命名冲突,因为子类的定义在后面,所以会覆盖父类。(一定会被覆盖的就是 init 方法,所以说父类在 init 声明的实例属性无法被继承后的父类方法和子类使用)
也因此创建子对象的时候,并不会像 Java 一样自动调用无参父类的构造方法。
我们知道被覆盖的方法仅仅只是因为调用顺序的原因,Java 提供 super 实例来通过定义调用偏移量实现对覆盖的父类方法和属性的访问。
Python 则可以通过 父类名.方法(self)
名手动绑定 self 对象的方式访问父类中的方法。
1 | class Father: |
多继承
Python 的多继承就是在类名字后面的小括号里面,多写几个父类,至于命名冲突之后的访问顺序,请参考后面介绍的 MRO 机制:
1 | class Father1: |
但是上面的仅仅只是很简单的多继承关系,如果遇到了更复杂的,犹如很乱的多层树结构继承的时候,就需要根据 MRO 去分析了。
MRO 机制
MRO 全称 Method Resolution Order,方法解析顺序。
可以通过调用类名的类属性 __mro__
来获取类的优先级顺序,会返回一个元组。当然聪明的我们可以自己分析复杂的 __mro__
:
1 | class A: |
对于上面的程序,把各个类的 MRO 记为如下等式:
1 | 类 A:L[A] = merge(A , object) |
注意,以类 A 等式为例,其中 merge 包含的 A 称为 L[A] 的头,剩余元素(这里仅有一个 object)称为尾。
这里的关键在于 merge,它的运算方式如下:
- 检查第一个列表的头元素(如 L[A] 的头),记作 H。
- 若 H 未出现在 merge 中其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤 1;否则,取出下一个列表的头部记作 H,继续该步骤。
- 重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,Python 会抛出异常。
由此,可以计算出类 B 的 MRO,其计算过程为:
1 | L[B] = [B] + merge(L[A],[A]) |
同理,其他类的 MRO 也可以轻松计算得出。
父类的初始化
因为 Python 的继承不会像 Java 一样,创建子对象的时候,是不会自动调用父类的构造方法的(因为方法覆盖,只会调用最后面的那个构造方法),所以如果要调用父类的构造方法来完成初始化,就要手动调用。
有两种方法:
通过
super().__init__()
调用上一个优先级的父类的构造方法。通过
父类名.__init__(self)
手动绑定实例来调用父类的构造方法。
不过建议只在单继承中使用 super() 在多继承中只用后者。
多态实现
因为 Python 是动态类型,所以多态机制简直不要太好实现,根本不需要继承!!!
1 | def polymorphic(self): |