面向对象和类图
本文记录一些面向对象的概念以及类图的规范,代码部分主要是介绍 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
。
题外话,为什么我们不把它当作公有属性来处理呢?其实在
get
和set
中,我们完成的不仅是值传递,我们还可以加入一些判断的逻辑,而且这样添加代码也比较方便,比如我们某个操作要对一个属性进行操作,我们想统计这样的操作有多少次,那么我们就可以直接在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 绘制
泛化
泛化用实线箭头表示
实现
实现指的是实现了抽象类或接口定义的方法,用虚线箭头表示。
关联
关联关系指的是类之间存在某种联系,用不带箭头的实现表示,下面的聚合和组合都是特殊的关联关系。
聚合
聚合描述的是整体和局部的关系,用实线空心菱形表示,菱形的一侧是整体。需要注意的是聚合中的整体和局部生命周期是独立的。比如员工和部门的关系。
组合
组合也是局部和整体的关系,用实线实心菱形表示。组合和聚合的区别是,整体被销毁时,局部也全部被销毁。如浏览器窗口和标签页的关系
组合强调的更多还是生命周期的依赖关系,比如用户的订单是依赖于某个用户的,这也是组合关系。