本文记录一些面向对象的概念以及类图的规范,代码部分主要是介绍 python 中的写法。

python 中的类

类属性

python 中使用关键字 class 定义一个类。类中的属性包含两种,类属性和对象属性。类中私有属性可以通过添加两个下划线实现,即 __

class Student:
    # stu 是类属性
    stu = 'university'
    __pri = True
    def __init__(self, name):
        # name 是对象属性
        self.name = name

student1 = Student('a')
# student1.stu 和 student1.name 是可以的
# Student.stu 可以,但是 Student.name 不行
# 无法访问 student1.__pri,它是私有的

但是私有属性只是 python 解释器把 __pri 变成了 _Student__pri,按后面这个是可以访问的。

类中的方法

类中定义的方法,可以分为实例方法,类方法以及静态方法。

实例方法

实例方法第一个参数是实例的引用,我们用 实例.方法() 时,相当于将自身的引用传入,实例方法定义时第一个参数一般是 self 关键字(不是 self 编译也能过),此外还可以使用 类.方法(实例) 的方式使用。 构造函数也是实例方法。

class Student:
    ...
    def getname(self)
        return self.name

student2 = Student('b')
student2.getname()
Student.getname(student2)

上述两种调用 getname 的方法都是可以的。

类方法

类方法需要装饰器 @classmethod,类方法第一个参数是类对象(python 中一切皆对象,我们定义的类本身也是一个对象),因此很容易解释它只能访问到类属性。第一个参数一般命名为 cls(当然也可以换成其他的),使用类方法的方式也有两种,即 类.类方法实例.类方法,但是推荐用法还是第一种

class Student:
    year = 1
    @classmethod
    def setyear(cls, year)
        cls.year = year

Student.setyear(2)
print(student2.year)

静态方法

静态方法需要装饰器 @staticmethod,静态方法和类外部的函数类似,不和类或者对象进行绑定,因此他无法访问类属性和对象属性,他只是类命名空间中的一个方法而已。

class Student:
    ...
    @staticmethod
    def incyear(year):
        return year + 1

student3 = Student('c')
student3.incyear(Student.year)
Student.incyear(Student.year)

静态方法通过类和实例都可以访问。最后两种调用 incyear 的方法都是可以的。

同样的,类方法可以通过添加 __ 的方式将方法设置为私有的方法。和前面一样,这并不是真正的实现了外部无法访问,python 解释器会把双下划线开头的变量 __变量名 改为 _类名__变量名

class Student:
    ...
    def __plus(n):
        return n + 1
    def getnextyear(self):
        return self.__plus(self.year)

student4 = Student('d')
# student4.__plus(1) 无法访问
# student4.getnextyear() 可以

继承

继承能实现代码的重用,子类可以获取父类的属性和方法。继承的定义方式是 class 子类(父类),先从一个简单的例子入手

class Animal:
    def __init__(self, name):
        self.name = name
    def say(self):
        print(self.name, 'is calling')

class Dog(Animal):
    pass

dog = Dog('yjj')
dog.say()
# 输出 yjj is calling

子类能够继承了父类的方法和属性。python 是一个允许多继承的语言,子类也能继承了多个父类,写法是 子类(父类1, 父类2, ...)

class Animal:
    def __init__(self, name):
        self.name = name
    def say(self):
        print(self.name, 'is calling')

class Pet:
    def __init__(self, name):
        self.name = f'pet {name}'

class Dog(Pet, Animal):
    pass

dog = Dog('yjj')
dog.calling()
# 输出 pet yjj is calling

当继承的多个父类有同名属性或方法时,会优先调用继承的第一个类的属性和方法

方法解析顺序 MRO

一个类的方法和属性可能定义在基类,也可能定义在它继承的某一个父类中。因此调用类方法和属性时,需要对类及其父类进行搜索,这个搜索顺序就是 MRO。单继承的语言比如 java 的 MRO 非常简单,逐步往父类搜索即可,但是对于多继承的语言来说,比如 C++ 和 python,多继承会带来一些困难。

在 c++ 中,多继承会带来菱形继承的问题。

class A {
public:
    int n;
    A() {
        n = 10;
    }
};
class B: public A{};
class C: public A{};
class D: public B, public C {};

int main() {
    D d;
    // d.n = 20;
    d.C::n = 20;
}

注释处会出现报错,因为 B,C 都继承自 A,都有属性 n,当实例 d 修改属性 n 时,编译器不知道 d.n 指的是哪个作用域下的,会出现二义性,c++ 的解决方法是虚继承。python3 中,MRO 列表的构造是通过一个C3线性化算法来实现的。它实际上就是合并所有父类的 MRO 列表并遵循如下三条准则:

  • 子类会先于父类被检查
  • 多个父类会根据它们在列表中的顺序被检查
  • 如果对下一个类存在两个合法的选择,选择第一个父类

一个类的 mro 可以通过 __mro__ 属性访问

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        super().__init__()
        print('A.__init__')

class B(Base):
    def __init__(self):
        super().__init__()
        print('B.__init__')

class C(A,B):
    def __init__(self):
        super().__init__()
        print('C.__init__')

class D:
    def __init__(self):
        print('D')
    def getsuper(self):
        return super()

print(D.__mro__)
print(C.__mro__)
print(B.__mro__)

C 的 MRO 列表为 [C, A, B, Base]

super 类

super(cls, inst) 返回一个代理对象,将方法的调用委托给实例 inst 的 mro 列表中 cls 的下一个类。

c = C()
print(super(A, c))
# print 内容:<super: <class 'A'>, <C object>>

上述代码中使用 super(A, c).__init__() 相当于执行的是 B 的构造函数。在某个类 C 中使用 super() 等同于使用 super(C, self)

抽象类和接口

java 是单继承的语言,只能继承一个父类(包括抽象类),但是可以继承多个父接口。抽象类和接口都无法实例化,都可以包括一些未实现的方法。但是抽象类内是可以写非抽象方法,接口只能写抽象的方法。一个类只继承于一个

python 也可以实现抽象类和接口。首先是抽象类,类是一些具有相似数据特征的结构,抽象类则是对具有相似结构类的抽象。

抽象类的特点是只能被继承,不能被实例化。python 中可以使用 abc 模块实现,通过继承 abc.ABC 来定义抽象类,通过 @abstractmethod 装饰器定义抽象方法

from abc import ABC, abstractmethod

class Payment(ABC):
    @abstractmethod
    def pay(self):
        """
        付款功能
        """

    @abstractmethod
    def collect(self):
        """
        收款功能
        """


class Wechat(Payment):
    def pay(self):
        print('wechat is paying')
    
    def collect(self):
        print('wechat is collecting')

在子类中,必须实现 Payment 中的两个抽象方法,否则会出现 Error

不过需要注意的是,使用抽象方法的前提必须是抽象类,否则会无效

property

我们使用私有属性的时候,有时候需要从外部对其读取值和修改值,像其他语言中,对于一个私有属性 val,我们可以设置一个 getval 来访问,setval 来修改,python 中我们当然也可以这么写。比如对于私有属性 __val,我们设置一个 get_val 来获取值,对于修改值,我们设置一个 set_val

题外话,为什么我们不把它当作公有属性来处理呢?其实在 getset 中,我们完成的不仅是值传递,我们还可以加入一些判断的逻辑,而且这样添加代码也比较方便,比如我们某个操作要对一个属性进行操作,我们想统计这样的操作有多少次,那么我们就可以直接在 set 方法中添加计数功能,而不用在所有引用了属性值的地方添加计数。当然我们也可以只设置 get 方法,让该属性只读。

回到正题

class Myclass:
    def __init__(self):
        self.__val = 1

    def get_val(self):
        return self.__val

    def set_val(self, val):
        self.__val = val

inst = Myclass()
print(inst.get_val())

具体的,我们使用不同类型的 property 去装饰我们需要的 set_val 函数和 get_val 函数。property 用于装饰我们获取值的函数func,而func.setter 用于装饰我们修改值的函数

    @property
    def get_val(self):
        return self.__val

    @get_val.setter
    def set_val(self, val):
        self.__val = val

inst = Myclass()
print(inst.get_val)
# output: 1
inst.set_val = 2
print(inst.get_val)
# output: 2

所以如果我们把获取值和修改值的函数名字改为属性值的名字的话(不带下划线),那么上述操作会更优雅

class Myclass:
    def __init__(self):
        self.__val = 1

    @property
    def val(self):
        return self.__val

    @val.setter
    def val(self, val):
        self.__val = val

inst = Myclass()
print(inst.val)
# output: 1
inst.val = 2
print(inst.val)
# output: 2

另外,property 还可以使用 deleter

具体就参见官方文档吧,感觉和垃圾回收也有关系,留给以后补充

再总结下,其实如果 @property,@<methodname>.setter, @<methodname>.deleter 这三者修饰函数 fget, fset, fdel后,当我们使用 实例.fget, 实例.fset=val, del 实例.fdel 时,相当于使用了 实例.fget(), 实例.fset(val), 实例.fdel()

另外,property 可以用来实现属性之间的动态依赖关系,比如

class Date:
    def __init__(self, month, day):
        self.__month = month
        self.__day = day
        self.__date = f"{month}/{day}"

    @property
    def month(self):
        return self.__month

    @property
    def day(self):
        return self.__day

    @property
    def date(self):
        return self.__date

    @month.setter
    def month(self, m):
        self.__month = m
        self.__date = f"{m}/{self.__day}"

    @day.setter
    def day(self, d):
        self.__day = d
        self.__date = f"{self.__month}/{d}"

    @date.setter
    def date(self, d):
        self.__date = d
        self.__month, self.__day = d.split("/")

    def __str__(self) -> str:
        return self.__date


date = Date(10, 13)
print(date)
# output: 10/13
date.month = 11
print(date)
# output: 11/13
date.date = "10/12"
print(date.day)
# output: 12

魔术方法

Python 类中还有一些执行特殊功能的方法,方法名通过双下划线围起来

__str__:在使用 str()print() 时触发,该方法必须返回一个 str,用一个 self 接受对象

__len__:在使用 len() 时触发,该方法接受一个 self,然后返回一个 int

__call__:一个对象的类是否是 callable 就等价于该类是否定义了 __call__ 这个方法。定义了这个方法的类的对象可以通过 对象() 的方式来触发这个函数。可以通过 callable() 判断是否定义了该方法。该方法至少接受一个 self 参数,返回值根据情况而定。

__getattr__:当获取不存在的对象成员时触发,接受 self 参数和成员名称的字符串,返回一个值。可以用来做代理访问

__setattr__:设置对象成员时触发,接受 self 参数和成员名称的字符串以及要设置的值

__delattr__:删除成员时触发,接受 self 参数,没有返回值

__getattribute__:使用对象成员时触发,接受 self 参数和成员名称的字符串,返回一个值

__iter__, __next__ 以及 __getitem__: 见 python-迭代

类图

类图有三个部分,从上到小是类名、属性和方法。属性和方法前的 - 代表 private,+ 代表 public。如果一个类是抽象类,则类名可以用斜体或者放入 <<>> 中来进行区分

类图

了解了类图的组成之后,需要说明类图中怎么表现类之间的关系。

本节的所有类图使用 ProcessOn 绘制

泛化

泛化用实线箭头表示

泛化

实现

实现指的是实现了抽象类或接口定义的方法,用虚线箭头表示。

实现

关联

关联关系指的是类之间存在某种联系,用不带箭头的实现表示,下面的聚合和组合都是特殊的关联关系。

关联

聚合

聚合描述的是整体和局部的关系,用实线空心菱形表示,菱形的一侧是整体。需要注意的是聚合中的整体和局部生命周期是独立的。比如员工和部门的关系。

聚合

组合

组合也是局部和整体的关系,用实线实心菱形表示。组合和聚合的区别是,整体被销毁时,局部也全部被销毁。如浏览器窗口和标签页的关系

组合

组合强调的更多还是生命周期的依赖关系,比如用户的订单是依赖于某个用户的,这也是组合关系。

References

  1. Python 面向对象学习整理 (看这一篇就足够了
  2. 详解Python类中的三种方法
  3. Python MRO方法解析顺序详解
  4. 调用父类方法
  5. 内置函数 - Python 3.11.4 文档
  6. 看懂UML类图和时序图
  7. Python @property属性详解 - 知乎
  8. Python 常用魔术方法