Skip to content
This page has been auto-translated and may contain errors.View in English

类与对象

到目前为止你使用的每种类型(字符串、列表、字典)实际上都是一个类。当你调用 "hello".upper() 时,就是在调用字符串对象上的一个方法。类让你能够定义自己的类型,拥有自己的数据和行为。一个 Player 类可以保存名字、分数和等级,并知道如何显示自己。

类是用户自定义类型的机制。类定义了一个模板:每个实例所保存的数据(属性)以及它所支持的操作(方法)。你不再需要在并行变量中跟踪值并到处传递它们,而是将它们打包成一个具有清晰接口的内聚对象。

class 语句在当前命名空间中创建一个新的类型对象,使用元类(默认为 type)来构造它。实例通过调用类来创建,这会调用 __new__ 来分配对象,并调用 __init__ 来初始化它。Python 的对象模型是统一的:一切皆对象,包括类、函数和类型本身。

蓝图和实例

类是一个蓝图。实例是根据该蓝图创建的具体事物。你可以根据需要创建任意多个实例,每个实例都有自己的数据,但共享类中定义的相同方法。

类定义结构和行为。实例是从该类创建的对象;每个实例都有自己的属性命名空间,但共享类的方法对象。创建实例就像调用函数一样调用类:Dog() 创建一个新的 Dog 实例。

调用类会触发 type.__call__,它调用 cls.__new__(cls) 来分配实例,并调用 cls.__init__(instance, ...) 来初始化它。生成对象的 __class__ 指回该类。方法查找遵循 MRO:实例 __dict__、类 __dict__,然后是 MRO 链。

python
class Dog:
    def bark(self):
        print("Woof!")

rex  = Dog()
luna = Dog()

rex.bark()    # "Woof!"
luna.bark()   # "Woof!"

Dog 是类。rexluna 是实例:两只不同的狗,每只都共享类中定义的相同行为。

__init__self

__init__ 是创建新实例时 Python 自动调用的方法。它是你为对象设置初始数据的地方。self 是方法引用其正在操作的具体实例的方式,它始终是第一个参数。

__init__ 初始化一个刚刚分配好的实例。self 是每个实例方法第一个参数的常规命名;当你调用 alice.display() 时,Python 会自动传入该实例。在 __init__ 中设置在 self 上的属性是实例属性:每个实例都有自己的副本。

__init____new__ 接收已分配的实例并对其进行设置。self 不是关键字;它是第一个位置参数,在你调用 instance.method() 时由描述符协议隐式传递。设置 self.attr = value 会调用 object.__setattr__,它会写入实例的 __dict__。未在 __init__ 中设置的属性仍然可以稍后在任何实例上设置,因为 Python 的字典是动态的。

python
class Player:
    def __init__(self, name, score=0):
        self.name  = name
        self.score = score

    def add_points(self, points):
        self.score += points

    def display(self):
        print(f"{self.name}: {self.score} points")

alice = Player("小明")
bob   = Player("小红", score=50)

alice.add_points(30)
alice.display()   # "小明: 30 points"
bob.display()     # "小红: 50 points"

self.nameself.score实例属性:它们属于具体的对象,而不是类本身。每个 Player 实例都有自己的 namescore

方法

类内部定义的任何函数都是方法。实例方法的第一个参数始终是 self;Python 会自动传入它。方法可以通过 self 读取和修改实例的数据。

实例方法是存储在类命名空间中的普通函数。描述符协议将 instance.method 转换为绑定方法,自动绑定 self。从方法返回 self 可以实现方法链式调用:obj.scale(2).rotate(90)

instance.method 触发描述符查找:type.__getattribute__ 在类的 __dict__ 中找到 method,将其识别为函数(它实现了描述符协议),并返回一个绑定方法对象,该对象将函数与已预先应用的实例打包。这就是 self 被自动传递的方式。返回 self 可以实现流式接口。

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def scale(self, factor):
        self.radius *= factor
        return self    # 返回 self 允许链式调用:c.scale(2).scale(0.5)

c = Circle(5)
print(c.area())    # 78.53975
c.scale(2)
print(c.area())    # 314.159

类变量与实例变量

直接定义在类上(不在 __init__ 内部)的变量是类变量。所有实例共享同一个类变量。在 __init__ 中设置在 self 上的变量是实例变量,对每个对象都是唯一的。

类变量存储在类的 __dict__ 中,由所有实例共享。实例变量存储在每个实例的 __dict__ 中。当你读取 self.attr 时,Python 先检查实例,然后检查类。当你写入 self.attr = value 时,它总是创建或更新实例属性,如果存在同名类变量则会被遮蔽。

实例上的属性查找按以下顺序进行:实例 __dict__、类 __dict__,然后是 MRO。写入 self.attr 始终针对实例 __dict__(除非重写了 __setattr__)。类变量是共享的,直到写入 self.attr 创建出一个实例遮蔽副本。可变的类变量(如列表)尤其棘手:所有实例共享同一个对象,因此通过任何一个实例的变更对所有实例都可见。

python
class Player:
    max_lives = 3    # 类变量,对每个 Player 都相同

    def __init__(self, name):
        self.name  = name   # 实例变量,每个 Player 各不相同
        self.lives = Player.max_lives

    def die(self):
        self.lives -= 1

alice = Player("小明")
bob   = Player("小红")

Player.max_lives = 5    # 改变所有当前和未来的实例

类变量用于在所有实例之间共享的值:常量、计数器、默认值。实例变量用于每个对象不同的数据。

__str____repr__

__str__ 控制 print() 和 f-字符串显示对象的内容。__repr__ 控制控制台和调试时显示的开发者视图。始终定义 __repr__。当你希望有一个独立于调试视图的、面向用户的清晰显示时,再定义 __str__

__str__str()print() 和 f-字符串调用。__repr__repr() 调用并在 REPL 中显示。如果只定义了 __repr__,Python 会将其同时用于两者。约定:__repr__ 应返回一个能够重建对象的字符串;__str__ 应返回一个人类可读的摘要。

str(obj) 调用 type(obj).__str__(obj),如果未定义 __str__,则回退到 __repr__repr(obj) 调用 type(obj).__repr__(obj)。在 f-字符串中,{obj} 调用 __format__,默认情况下它会调用 __str__{obj!r} 在格式化之前应用 repr()__repr__ 约定:返回形如 ClassName(arg1=..., arg2=...) 的字符串,求值时能重现该对象。

python
class Player:
    def __init__(self, name, score):
        self.name  = name
        self.score = score

    def __str__(self):
        return f"{self.name} ({self.score} pts)"

    def __repr__(self):
        return f"Player(name={self.name!r}, score={self.score})"

alice = Player("小明", 87)
print(alice)        # "小明 (87 pts)"   (uses __str__)
repr(alice)         # "Player(name='小明', score=87)"  (uses __repr__)

始终定义 __repr__。当你希望有一个独立于调试视图的、面向用户的清晰表示时,再定义 __str__。如果只定义了 __repr__,Python 会将其同时用于两者。

私有约定

Python 没有真正的私有变量,但名称开头的单个下划线(_balance)是一种约定,表示"这是内部使用的,不要从类外部直接使用"。语言本身不强制执行;它只是与其他开发者的沟通方式。

单个下划线(_attr)是表示内部使用的约定。Python 不强制执行,但所有 linter、IDE 和开发者都会遵守。双下划线(__attr)会触发名称改写:Python 会将其重写为 _ClassName__attr,以防止在子类中意外冲突。这不是真正的私有性;它是一种冲突避免机制。

单下划线:仅为约定,无强制执行。双下划线:名称改写在编译时(而非运行时)将 __attr 转换为 _ClassName__attr。这是一种子类冲突保护机制,而非隐私机制;改写后的名称仍可从外部访问。__slots__ 是更根本的控制:它用一组固定的槽描述符替换实例的 __dict__,防止任意属性创建并减少内存开销。

python
class BankAccount:
    def __init__(self, balance):
        self._balance = balance    # _ 表示"勿动"

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def balance(self):
        return self._balance

双下划线(__name)触发名称改写;Python 将属性重命名为 _ClassName__name 以避免子类中的冲突。这种用法很少需要。单下划线在大多数代码中都是约定。

继承

一个类可以从另一个类继承,自动获得其所有属性和方法。然后你可以在子类中重写特定方法以改变其行为。这让你能够重用一个公共基类,并在需要的地方进行特化。

继承创建一种"是一个"的关系。子类继承父类的所有方法和属性,并可以重写其中任何一个。方法解析顺序(MRO)定义了属性的查找顺序。Python 支持多重继承;MRO 使用 C3 线性化算法计算。

MRO 由 type.__mro_entries__ 使用 C3 线性化算法计算,可通过 ClassName.__mro__ 访问。方法解析按从左到右的 MRO 顺序进行。isinstance(obj, cls) 遍历 MRO 进行检查。多重继承很强大但可能导致菱形继承;MRO 以确定的方式解决它。super() 遵循 MRO,而不仅仅是直接父类。

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

pets = [Dog("旺财"), Cat("小花"), Dog("大黄")]
for pet in pets:
    print(pet.speak())

DogCat 继承了 Animal__init__,因此它们不需要自己的 __init__。它们用特定的行为重写了 speak()

super()

super() 调用父类的方法。当你想要扩展父类的行为而非完全替换它时使用它:调用父类的 __init__ 来运行其设置代码,然后再添加子类需要的内容。

super() 返回一个代理对象,它将方法调用委托给 MRO 中的下一个类。当父类有 __init__ 时,始终在子类的 __init__ 中调用 super().__init__()。跳过它意味着父类的设置代码不会运行,这可能导致对象处于损坏状态。

不带参数的 super()(Python 3)使用 __class__(编译器注入的单元变量)和闭包方法的第一个参数来确定 MRO 位置。super(cls, self) 是显式形式。代理从 type(self).__mro__cls 之后的下一个类开始解析方法查找。这对正确的多重继承至关重要:super() 通过完整的 MRO 链接每个类的 __init__

python
class Animal:
    def __init__(self, name, sound):
        self.name  = name
        self.sound = sound

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")   # 调用 Animal.__init__
        self.tricks = []                  # 添加一些额外的东西

    def learn(self, trick):
        self.tricks.append(trick)

rex = Dog("旺财")
rex.learn("sit")
print(rex.tricks)   # ["sit"]

当你的子类有自己的 __init__ 且父类也有时,始终调用 super().__init__()

类方法和静态方法

@classmethod 创建一个接收类本身而非实例的方法。它适用于备用构造器:从字符串、文件或其他格式创建实例。@staticmethod 是出于组织原因放在类内部的普通函数;它既不接收实例也不接收类。

@classmethod 接收 cls(类)作为它的第一个参数,而非实例。主要用于从不同输入格式创建实例的备用构造器。@staticmethod 是命名空间归属于类的普通函数;它无法访问类或实例。用 @classmethod 来定义构造器,用 @staticmethod 来定义在逻辑上与类相关的工具函数。

@classmethod 使用描述符协议:classmethod 描述符返回一个绑定方法,其第一个参数是类,而非实例。cls 是调用时的实际类,因此子类方法会得到子类。@staticmethod 是一个更简单的描述符,它原样返回底层函数,没有隐式的第一个参数。这两个装饰器都修改描述符行为;都不影响函数的代码对象。

python
class Player:
    def __init__(self, name, score):
        self.name  = name
        self.score = score

    @classmethod
    def from_string(cls, data):
        name, score = data.split(",")
        return cls(name, int(score))

alice = Player.from_string("小明,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("小明")   # True
Player.is_valid_name("A1")      # False

使用 @classmethod 实现备用构造器。使用 @staticmethod 实现逻辑上属于类但不需要实例或类数据的工具函数。

@property

@property 让你能够像访问属性一样访问方法,无需括号。用于从其他属性计算得出、并且作为简单属性访问感觉自然的值。

@property 将方法转换为只读属性。当访问该属性时,方法会运行。这对于从存储数据派生的计算值很有用,也可用于在不更改公共接口的情况下为属性访问添加验证。配套的 @name.setter 让属性变为可写。

@property 是一个描述符:property(fget, fset, fdel, doc)。访问 instance.attr 会调用 fget(instance)@name.setter 设置 fset@name.deleter 设置 fdel。属性在类上查找,而不是在实例上,这就是为什么 instance.__dict__[name] 不会遮蔽属性(数据描述符优先于实例 __dict__)。属性是 Java 风格 getter/setter 方法的 Python 式替代方案。

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)      # 78.53975 (看起来像属性,运行起来像方法)
print(c.diameter)  # 10

属性对于计算值很有用:从其他属性派生的、感觉无需 () 就能自然访问的东西。

实战

一个具有实例属性、方法、@property__str__Player 类:

python
class Player:
    max_lives = 3

    def __init__(self, name: str):
        self.name  = name
        self.score = 0
        self.lives = Player.max_lives

    def earn_points(self, amount: int) -> None:
        self.score += amount

    def take_hit(self) -> bool:
        self.lives -= 1
        return self.lives > 0

    @property
    def is_alive(self) -> bool:
        return self.lives > 0

    def __str__(self) -> str:
        return f"{self.name} | Score: {self.score} | Lives: {self.lives}"

alice = Player("小明")
alice.earn_points(50)
alice.take_hit()
print(alice)            # "小明 | Score: 50 | Lives: 2"
print(alice.is_alive)   # True

一个使用私有属性、带有 @property getter、deactivate 方法和 to_dict 序列化器的 User 类:

python
class User:
    def __init__(self, user_id: int, username: str, email: str):
        self.id       = user_id
        self.username = username
        self.email    = email
        self._active  = True

    @property
    def active(self) -> bool:
        return self._active

    def deactivate(self) -> None:
        self._active = False

    def to_dict(self) -> dict:
        return {
            "id":       self.id,
            "username": self.username,
            "email":    self.email,
            "active":   self._active,
        }

    def __repr__(self) -> str:
        return f"User(id={self.id}, username={self.username!r})"

alice = User(1, "xiaoming", "[email protected]")
print(alice.to_dict())
alice.deactivate()
print(alice.active)   # False

一个 DataSplit 类,将训练/验证切片封装在属性后面,并使用 __repr__ 输出清晰的调试信息:

python
class DataSplit:
    def __init__(self, data: list, train_ratio: float = 0.8):
        split       = int(len(data) * train_ratio)
        self._train = data[:split]
        self._val   = data[split:]

    @property
    def train(self) -> list:
        return self._train

    @property
    def val(self) -> list:
        return self._val

    @property
    def sizes(self) -> tuple[int, int]:
        return len(self._train), len(self._val)

    def __repr__(self) -> str:
        return f"DataSplit(train={len(self._train)}, val={len(self._val)})"

data  = list(range(100))
split = DataSplit(data, train_ratio=0.8)
print(split)         # DataSplit(train=80, val=20)
print(split.sizes)   # (80, 20)

_train_val 上的下划线前缀表示调用者应通过属性访问,而不是直接修改原始列表。Python 不会强制执行,但它确立了一个明确的契约。