Python从进阶到高级—通俗易懂版(二)

# ==================================
# Author : Mikigo
# Env    :deepin 20.4 os
# ==================================

1、类变量和实例变量

(1)类变量是在类里面直接定义的变量,它可以被类对象访问和赋值,也可以被实例对象访问和赋值。

class Test:
    b = 1

    def __init__(self):  # 构造函数
        self.a = 1

T = Test()
print(T.b)
print(Test.b)
T.b = 2  # 通过实例对象赋值
print(T.b)
Test.b = 2 # 通过类对象赋值
print(Test.b)
1
1
2
2

b 是类变量,都能被访问和赋值,没问题哈。

(2)实例变量是在构造函数里面定义的变量,它可以被实例对象访问和赋值,不能被类对象访问和赋值。

class Test:
    b = 1

    def __init__(self):
        self.a = 1

T = Test()
print(T.a)
T.a = 2
print(T.a)
print(Test.a)
Test.a = 2
print(Test.a)
1
2
Traceback (most recent call last):
  File "/tmp/pycharm_project_16/123.py", line 12, in <module>
    print(Test.a)
AttributeError: type object 'Test' has no attribute 'a'

a 是实例变量,你看实例对象访问和赋值正常的,类对象访问就报错了。

2、类方法、静态方法和实例方法

(1)实例方法又称对象方法,是类中最常见的一种方法。

class Test:
    
    def obj_method(self):
        print("this is obj method")

实例方法参数必须传入 selfself 表示实例对象本身,实例方法的调用也必须通过实例对象来调用:

Test().obj_method()

(2)类方法

class Test:
    
    @classmethod
    def cls_method(cls):
        print("this is class method")

可以通过类对象调用,也可以通过实例对象调用。

Test.cls_method()
Test().cls_method()

注意两点:

  • 方法前面必须加装饰器 classmethod ,装饰器是 Python 中的一种语法糖,后面会讲到,记住这种固定用法,这种写法也是初代装饰器的用法。
  • 参数传入 clscls 表示类对象,但是注意不是必须的写法,写 cls 是一种约定俗成的写法,方便我们理解,也就是说这里你写 self 从语法上也是不会有问题的。这就是为什么有时候我们将一个实例方法改成类方法,直接在方法前面添加了装饰器,而没有改 self,仍然能正常执行的原因。

(3)静态方法,实际上就是普通的函数,和这个类没有任何关系,它只是进入类的名称空间。

class Test:
    
    @staticmethod
    def static_method():
        print("this is static method")

不需要传入任何参数。同样,可以通过类对象调用,也可以通过实例对象调用。

Test.static_method()
Test().static_method()

我看到一些社区大佬都表现出对静态方法的嫌弃,他们觉得既然静态方法和函数没有关系,何不如在类外面写,直接写在模块里面岂不快哉。咱们不予评价,存在即合理。

3、类和实例属性的查找顺序

这里需要引入一个概念:MRO(Method Resolution Order),直译过来就是“方法查找顺序”。

大家知道类是可以继承的,子类继承了父类,子类就可以调用父类的属性和方法,那么在多继承的情况下,子类在调用父类方法时的逻辑时怎样的呢,如果多个父类中存在相同的方法,调用逻辑又是怎样的呢,这就是 MRO

Python2.3 之前的一些查找算法,比如:深度优先(deep first search)、广度优化等,对于一些菱形继承的问题都不能很好的处理。这部分内容比较多且杂,可以自己查阅资料。

Python2.3 之后,方法的查找算法都统一为叫 C3 的查找算法,升级之后的算法更加复杂,采用的特技版拓扑排序,这里也不细讲,可以自己查阅资料,我们只需要关心现在方法查找顺序是怎样的就行了。

来,这里举例说明:

class A:
    pass

class B:
    pass

class C(A, B):
    pass

print(C.__mro__)

__mro__ 可以查看方法的查找顺序。

(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

可以看到,对于 C 来讲,它里面的方法查找顺序是 C — A — B,没毛病哈,很清楚。

现在升级一下继承关系,试试菱形继承:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.__mro__)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

D 的查找顺序是 D — B — C — A

说明什么问题?我在这噼里啪啦说了这么多,到底想说啥?

想象一下,如果你在 B 和 C 里面都重载了 A 里面的一个方法,此时如果你想调用的是 C 里面的方法,实际上是无法调用的,因为根据方法的查找顺序,会先找到 B 里面的方法。

因此,重点来了:在 Python 中虽然是支持多继承的,但是在实际项目中不建议使用多继承,因为如果继承关系设计得不好,很容易造成逻辑关系的混乱,原因就是 MRO

Ruby 之父在《松本行弘的程序世界》书中,讲到三点多继承的问题:

  • 结构复杂化:如果是单一继承,一个类的父类是什么,父类的父类是什么,都很明确,因为只有单一的继承关系,然而如果是多重继承的话,一个类有多个父类,这些父类又有自己的父类,那么类之间的关系就很复杂了。
  • 优先顺序模糊:假如我有A,C类同时继承了基类,B类继承了A类,然后D类又同时继承了B和C类,所以D类继承父类的方法的顺序应该是D、B、A、C还是D、B、C、A,或者是其他的顺序,很不明确。
  • 功能冲突:因为多重继承有多个父类,所以当不同的父类中有相同的方法是就会产生冲突。如果B类和C类同时又有相同的方法时,D继承的是哪个方法就不明确了,因为存在两种可能性。

看看这是大佬说的,不是我说的。

那有同学要问了,我写的功能很复杂啊,必须要继承多个类,怎么办,难受!

实际上有一种比较流行且先进的设计模式:Mixin 混合模式,完美解决这个问题。

举个简单的例子:

class Animal:
    pass

# 大类
class Mammal(Animal):
    pass

# 各种动物
class Dog(Mammal):
    pass

class Bat(Mammal):
    pass

现在动物们没有任何技能,咱们需要给动物们增加一下技能:

class RunnableMixIn:
    def run(self):
        print('Running...')

class FlyableMixIn:
    def fly(self):
        print('Flying...')

注意 Mixin 的类功能是独立的,命名上也应该使用 MixIn 结尾,这是一种规范。

需要 Run 技能的动物:

class Dog(Mammal, RunnableMixIn):
    pass

需要 Fly 技能的动物:

class Bat(Mammal, FlyableMixIn):
    pass

有点感觉了没,Mixin 类的特点:

  • 功能独立、单一;
  • 只用于拓展子类的功能,不能影响子类的主要功能,子类也不能依赖 Mixin
  • 自身不应该进行实例化,仅用于被子类继承。

Mixin 设计思想简单讲就是:不与任何类关联,可与任何类组合。

posted @ 2022-10-21 11:36  mikigo  阅读(72)  评论(0编辑  收藏  举报