在Python编程中,类(Class)的概念无疑是提升代码组织能力和复用性的关键工具。无论是初学者编写简单脚本,还是资深开发者设计复杂的系统,类都为我们提供了强大的支持。它不仅是面向对象编程(OOP)的核心,也是理解Python语言特性的重要一步。
1. 继承与组合
有时候我们应该使用继承,有时候不应该。让我们来讨论一下。
继承应该用于 “属于” 关系。例如,狗属于一种动物,矩形属于一种形状,汽车属于一种交通工具。因此:
- Dog 类继承自 Animal 类
- Rectangle 类继承自 Shape 类
- Car 类继承自 Vehicle 类
组合应该用于 “拥有 关系。例如,狗有主人,汽车有发动机,项目组有一名或多成员。
反过来,狗并不是主人,汽车并不是发动机,项目组并不是成员。我们不能用继承来表示这些关系,而应该使用组合。
注意这里没有继承。相反,Dog 对象 包含 一个 Owner 对象,Car 对象 包含 一个 Engine 对象,ProjectGroup 对象 包含 多个 Member 对象。
在决定使用继承还是组合时要非常小心,因为错误的选择可能会给项目带来麻烦。
2. super() 及其用途
在类中使用 super() 可以让我们访问父类的方法。
假设我们有两个类——Rectangle(矩形)和 Square(正方形)。
正方形是矩形的一种特殊情况,即长等于宽,因此我们可以让 Square 继承自 Rectangle。那么,Rectangle 是父类,Square 是子类。我们先定义 Rectangle 类。
接下来,我们定义继承自 Rectangle 的 Square 类。注意,我们希望尽可能复用现有的方法。
在这里,super() 指的是父类 Rectangle。因此,super().__init__ 实际上是父类 Rectangle 的 __init__ 方法。
在 Square 的 __init__ 方法中,我们调用了 Rectangle 的 __init__ 方法,它接收 (length, width)。因此,我们需要向 super().__init__ 传递两个值。
由于正方形的长等于宽,我们向 super().__init__ 传递 (length, length)。因此,self.length = length 和 self.width = length,我们仍然可以使用 area 和 perimeter 方法。
3. 实例方法、类方法与静态方法
实例方法属于类的 实例(对象)。实例方法可以访问实例的属性。
例如,intro 是 Dog 类的一个实例方法。
类方法属于类(而不是实例)。类方法只能访问类属性(不能访问实例属性)。
例如,get_num_dogs 是 Dog 类的一个类方法。
- 我们使用
@classmethod装饰器来标记这是一个类方法。 - 类方法接收
cls而不是self。cls代表 Dog 类本身。 get_num_dogs只能访问类属性(如num_dogs),而不能访问实例属性(如name或age)。
静态方法属于类。静态方法不能访问任何属性。
例如,description 是 Dog 类的一个静态方法。
- 我们使用
@staticmethod装饰器来标记这是一个静态方法。 - 注意静态方法不接收
self或cls。 - 静态方法既不能访问实例属性,也不能访问类属性。
4. 数据类(Dataclasses)
我们如何创建一个简单的 Dog 类,包含属性 (name, age)。
使用数据类可以实现相同的功能:
注意,数据类中我们不需要手动编写 __init__ 方法,因为它已经为我们自动化了。这在需要创建具有许多简单属性的类时特别有用。
注意:如果你需要在 __init__ 方法中做复杂的事情,那么使用数据类可能不是一个好主意。
5. __dict__ 属性
我们可以使用对象的 __dict__ 属性来检查其属性。
注意,动态设置的属性也会出现在 __dict__ 中:
这在调试和检查具有许多属性的复杂对象和类时非常有用。
6. 名称修饰(Name Mangling)
如果我们将属性定义为以两个下划线开头,就会发生名称修饰。
在 dog.__dict__ 中,_Dog__name 替代了 __name。
在 Dog 类内部,我们可以直接使用 __name,没有任何问题。
在 Dog 类外部,__name 不存在。相反,我们需要使用名称修饰后的 _Dog__name 来访问 __name 属性。
这是有意为之的。以两个下划线开头的属性 不打算在类外部被访问,因此会发生名称修饰。
然而,需要注意的是,Python 认为我们是成年人,因此我们仍然可以通过 _Dog__name 来访问 __name(尽管这不是一个好的实践)。
7. @property 和托管属性
@property 装饰器允许我们创建对象的属性(也称为托管属性)。这反过来又允许我们控制属性是可读的、可写的,还是两者皆可。
让我们创建一个可读写属性 name,使用 @property。
- 对于我们的 getter 方法,我们使用
@property装饰器,并返回self.__name。 - 在类外部,我们可以通过
dog.name来获取这个属性。 - 我们需要在 getter 方法之后定义 setter 方法。
- 我们使用
@name.setter来定义 setter 方法。 - 在类外部,我们可以直接使用
dog.name = new_name。
现在让我们将 name 属性设置为只读。
为此,我们只需要移除 setter 方法。如果只定义了使用 @property 的 getter 方法,那么这个属性默认为只读。
注意:技术上,我们仍然可以通过直接修改名称修饰后的属性来改变只读属性,但这是一种糟糕的实践。Python 允许这样做是因为它把我们当作成年人,但在生产代码中尽量不要这样做。
8. 使用 type() 定义类
与其使用 class 关键字正常创建类,我们还可以使用type() 动态创建类。
为此,我们需要传递 type(name, bases, dict):
name是一个字符串,表示类的名称。bases是一个父类元组,表示这个类继承的父类。dict是一个字典,包含类的所有方法。
以下是使用 class 关键字正常定义类的方式:
而通过 type() 动态创建相同的类的方式如下:
这种方式在我们希望动态创建类而无需使用 class 关键字时非常有用。
9. __slots__
类的 __slots__ 属性允许我们定义对象上可以设置的属性。
在以下示例中,只有 name 和 age 属性可以设置在 Dog 对象上。如果我们尝试设置任何其他属性(例如 gender),将会报错。
这在我们希望严格限制类的属性以确保开发过程中的结构化时非常有用。