OOP

ZaynPei Lv6

类和实例基础

Python 允许在运行时动态地为某个实例添加、修改或删除属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dog:
def __init__(self, name):
self.name = name

dog1 = Dog("Buddy")
print(dog1.name) # 输出: Buddy

# 动态添加属性
dog1.age = 3
print(dog1.age) # 输出: 3

# 动态修改属性
dog1.name = "Max"
print(dog1.name) # 输出: Max

# 动态删除属性
del dog1.age
print(hasattr(dog1, 'age')) # 输出: False

在Python中,实例的变量名如果以**__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问

1
2
3
4
5
6
7
8
9
10
class Cat:
def __init__(self, name):
self.__name = name # 私有变量

def get_name(self):
return self.__name # 通过方法访问私有变量

cat1 = Cat("Whiskers")
print(cat1.get_name()) # 输出: Whiskers
print(cat1.__name) # 报错: AttributeError: 'Cat' object has no attribute '__name'
> 注意:Python 并没有真正的私有变量机制,这只是通过名称重整**(name mangling)来实现的。实际上,私有变量可以通过 _ClassName__varname 的方式访问,但这不推荐这样做,因为它违反了封装的原则。例如上面的例子中,其实可以通过 cat1._Cat__name 来访问私有变量 __name

实例属性和类属性

正如我们前面所说, 由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量动态绑定,或者通过self变量(在__init__函数中):

1
2
3
4
5
6
7
class Dog:
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性

dog1 = Dog("Buddy", 3)
dog1.breed = "Golden Retriever" # 动态绑定属性

在Python中,实例属性是属于某个具体实例的属性,而类属性是属于类本身的属性,所有实例共享同一个类属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog:
species = "Canis familiaris" # 类属性

def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性

dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

print(dog1.name) # 输出: Buddy
print(dog2.name) # 输出: Max
print(dog1.species) # 输出: Canis familiaris
print(dog2.species) # 输出: Canis familiaris
print(Dog.species) # 输出: Canis familiaris

如果你在实例中创建了和类属性同名的属性,那么实例属性会覆盖类属性. 因此, 在编写程序的时候,千万不要对实例属性和类属性使用相同的名字.

1
2
3
dog1.species = "Canis lupus"
print(dog1.species) # 输出: Canis lupus
print(dog2.species) # 输出: Canis familiaris

获取对象信息

Python 提供了内置函数 type() 来获取对象的类型isinstance() 来检查对象是否是某个类的实例(其实也是检查类型),dir() 来列出对象的所有属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal:
def speak(self):
return "Animal sound"

class Dog(Animal):
def speak(self):
return "Woof!"

dog1 = Dog()
print(type(dog1)) # 输出: <class '__main__.Dog'>
print(isinstance(dog1, Dog)) # 输出: True
print(isinstance(dog1, Animal)) # 输出: True
print(dir(dog1)) # 输出: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'speak']

注意这里的 dir() 函数返回的是一个列表,包含了对象的所有属性和方法,包括内置的和自定义的, 其中 __ 开头和结尾的方法是 Python 的魔法方法,用于实现特定的行为, 而没有 __ 的是普通方法, 例如speak() 方法。

比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

1
2
3
4
5
6
7
8
9
10
class MyList:
def __init__(self, items):
self.items = items

def __len__(self):
return len(self.items)

my_list = MyList([1, 2, 3, 4, 5])
print(len(my_list)) # 输出: 5
print(my_list.__len__()) # 输出: 5
仅仅把属性和方法列出来是不够的,配合getattr()setattr()以及hasattr(),我们可以直接操作一个对象的状态: - getattr(obj, 'attr'):获取对象 obj 的属性 attr 的值。 - setattr(obj, 'attr', value):设置对象 obj 的属性 attr 的值为 value。 - hasattr(obj, 'attr'):检查对象 obj 是否有属性 attr,返回布尔值。
1
2
3
4
5
6
7
8
9
10
11
12
class Person:
def __init__(self, name):
self.name = name

person1 = Person("Alice")
print(getattr(person1, 'name')) # 输出: Alice

setattr(person1, 'age', 30)
print(getattr(person1, 'age')) # 输出: 30

print(hasattr(person1, 'name')) # 输出: True
print(hasattr(person1, 'address')) # 输出: False

鸭子类型

在Python中,鸭子类型(Duck Typing)是一种动态类型系统的概念,它强调的是对象的行为(方法和属性)而不是对象的实际类型。

换句话说,如果一个对象看起来像鸭子、叫起来像鸭子,那么它就可以被当作鸭子来对待。也就是说,只要一个对象实现了某个方法或属性,就可以被视为具有该行为,而不需要显式地继承某个类或实现某个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Duck:
def quack(self):
return "Quack!"
class Dog:
def quack(self):
return "Woof!"

def make_it_quack(animal):
print(animal.quack())

duck = Duck()
dog = Dog()
make_it_quack(duck) # 输出: Quack!
make_it_quack(dog) # 输出: Woof!

这里的 make_it_quack 函数接受任何对象,只要该对象有 quack 方法,就可以调用它,而不关心该对象的实际类型, 这就是鸭子类型的体现。

多继承和MixIn

与C++一样,Python也支持多继承,但它通过一种非常明确且可预测的方式——MRO(方法解析顺序)——解决了多继承中的二义性问题,尤其是菱形继承问题。

Python的多继承

在Python中,一个类可以同时从多个父类继承,从而获得所有父类的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Bird:
def fly(self):
print("I am flying!")

class Mammal:
def walk(self):
print("I am walking!")

# Bat 同时继承 Bird 和 Mammal
class Bat(Bird, Mammal):
pass

# 创建实例
b = Bat()
b.fly() # 输出: I am flying! (继承自 Bird)
b.walk() # 输出: I am walking! (继承自 Mammal)

而当遇到菱形继承时,Python并不会像C++那样需要开发者使用virtual关键字来解决。相反,Python使用 C3线性化算法 来计算出一个确定的方法解析顺序(Method Resolution Order, MRO)。

MRO是一个列表,它定义了当调用一个方法时,Python解释器查找该方法的顺序。你可以通过类的 mro 属性或 mro() 方法来查看它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
def who_am_i(self):
print("I am an A")

class B(A):
def who_am_i(self):
print("I am a B")

class C(A):
def who_am_i(self):
print("I am a C")

class D(B, C):
pass

d = D()
d.who_am_i() # 输出会是什么?
在这个例子中,D的对象调用 who_am_i() 时,Python会遵循MRO来查找。我们可以打印出D类的MRO来理解其查找顺序。
1
2
3
4
print(D.mro())
# 或者 print(D.__mro__)

# 输出: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
这里MRO列表的顺序是 [D, B, C, A, object]。当调用 d.who_am_i() 时,Python首先在 D 类中查找。D 中没有定义,于是继续。接着在 B 类中查找。B 中定义了该方法,于是调用 B.who_am_i()。因此,最终输出是 I am a B。查找在找到第一个匹配项后就停止了,不会再继续查找 C 或 A。

MRO保证了无论继承结构多复杂,方法的查找路径都是唯一且确定的,从而优雅地解决了二义性问题。

MixIn 设计模式

虽然多继承很强大,但如果滥用,会使类的层次结构变得混乱且难以维护。为了以一种更可控、更清晰的方式利用多继承,Python社区广泛采用 MixIn 设计模式。

MixIn (或 Mix-in) 是一个类,它包含了一组特定的、可重用的功能,旨在通过多继承的方式“混入”到其他类中,为这些类添加某些功能,而不是为了建立 “is-a” (是一个) 的父子关系。我们可以把MixIn看作是一个功能插件。

MixIn类通常有以下特点:

  • 功能单一、职责明确:一个MixIn类通常只提供一种特定的功能,例如日志记录、序列化、对象表示等。
  • 不用于实例化:MixIn类通常不应该被直接实例化。它存在的意义就是被其他类继承
  • 通常无 init 构造函数:为了避免与主类的构造函数发生冲突,MixIn通常**不定义自己的__init__方法**。如果需要初始化,也应确保它能与super()链良好协作。
  • 命名约定:为了清晰起见,MixIn类的命名通常以后缀 Mixin 结尾,例如 LoggingMixin、JSONMixin。

假设我们有几个不同的类,我们都希望它们能方便地将自身属性转换为字典或JSON格式。我们可以为此创建一个 SerializationMixin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import json

# 1. 定义一个 MixIn 类
class SerializationMixin:
"""一个将对象属性序列化为字典和JSON的MixIn。"""
def to_dict(self):
# vars(self) 返回对象 __dict__ 属性,即其所有实例变量
return vars(self)

def to_json(self):
return json.dumps(self.to_dict(), indent=2)

# 2. 定义一些业务类
class Book:
def __init__(self, title, author):
self.title = title
self.author = author

class Course:
def __init__(self, name, teacher, duration):
self.name = name
self.teacher = teacher
self.duration = duration

# 3. 将 MixIn “混入”到业务类中
class SerializableBook(SerializationMixin, Book):
pass

class SerializableCourse(SerializationMixin, Course):
pass

# 4. 使用“混入”的功能
book = SerializableBook("The Lord of the Rings", "J.R.R. Tolkien")
course = SerializableCourse("Computer Networking", "Dr. Smith", "16 weeks")

print("--- Book Object ---")
print(book.to_dict())
print(book.to_json())

print("\n--- Course Object ---")
print(course.to_dict())
print(course.to_json())

这里 SerializationMixin 提供了 to_dict() 和 to_json() 两个方法。它本身不关心操作的是什么对象,只负责通用的序列化逻辑

SerializableBook 通过继承 SerializationMixin 和 Book,既拥有了Book的属性和逻辑,又“免费获得”了序列化的能力。

注意继承顺序很重要:我们将 SerializationMixin 放在前面。根据MRO,如果Book中也有一个to_dict方法,那么SerializationMixin中的版本会优先被调用