面向对象(一)

前言

  • 这一篇是面向对象的入门三块:四个基本特性(封装 / 继承 / 多态 / 抽象)、类的语法(__init__、实例方法、类属性、类方法、静态方法)、以及魔术方法(生命周期、字符串表示、属性访问、容器协议、运算符、上下文管理)。
  • 所有示例都在 Python 3.13.9 上实跑过,输出原样贴在代码块下面。原笔记里有几处注释和实际行为对不上(比如 __setattr__ 在 __init__ 里也会触发、只写 __eq__ 会让对象不能当字典键),都按实测改过来了。
  • 继承的深入内容(super()、访问权限、多态细节)在《面向对象(二)》,这里只到「能写出来、知道为什么」为止。

面向对象:四个基本特性

面向对象编程(Object-Oriented Programming, OOP)把「数据」和「操作数据的行为」绑在一起。Python 里一切皆对象,类就是你自己造对象的模板。

  • 封装(Encapsulation)

    把内部状态藏起来,只暴露该暴露的方法。Python 没有真正的 private,靠命名约定:单下划线 _x 表示「内部使用,别碰」,双下划线 __x 会触发名字改写(name mangling)。

    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
    class BankAccount:
    def __init__(self, holder, balance=0):
    self._holder = holder # 保护属性:约定俗成别直接改
    self.__balance = balance # 私有属性:会被改名

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

    def withdraw(self, amount):
    if 0 < amount <= self.__balance:
    self.__balance -= amount

    def get_balance(self):
    return self.__balance

    acc = BankAccount('小明', 100)
    acc.deposit(50)
    acc.withdraw(30)
    print('余额:', acc.get_balance())
    print('直接读 __balance:', end=' ')
    try:
    acc.__balance
    except AttributeError:
    print('AttributeError')
    print('改名后仍能读到:', acc._BankAccount__balance) # 所以「私有」只是改名,不是真隐藏
    print('实例字典里的真实键:', list(vars(acc)))

    输出:

    1
    2
    3
    4
    余额: 120
    直接读 __balance: AttributeError
    改名后仍能读到: 120
    实例字典里的真实键: ['_holder', '_BankAccount__balance']
  • 继承(Inheritance)

    子类拿到父类的属性和方法,可以重写(override),也可以多层继承。

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

    def speak(self):
    return 'Some sound'

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

    class Cat(Animal):
    def speak(self):
    return 'Meow!'

    class RobotDog(Dog): # 继承 Dog,再重写一次
    def speak(self):
    return 'Beep boop!'

    print(Dog('Buddy').speak(), Cat('Whiskers').speak(), RobotDog('Rex').speak())
    print('RobotDog 也是 Animal:', isinstance(RobotDog('Rex'), Animal))
    print('没重写的属性照样继承:', RobotDog('Rex').name)

    输出:

    1
    2
    3
    Woof! Meow! Beep boop!
    RobotDog 也是 Animal: True
    没重写的属性照样继承: Rex
  • 多态(Polymorphism)

    同一个调用,不同对象给出不同响应。调用方只关心「有没有这个方法」,不关心具体是哪个类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Animal:
    def __init__(self, name):
    self.name = name

    def speak(self):
    return 'Some sound'

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

    class Cat(Animal):
    def speak(self):
    return 'Meow!'

    class RobotDog(Dog):
    def speak(self):
    return 'Beep boop!'

    def animal_sound(animal):
    print(animal.speak()) # 不检查类型,只要有 speak 就行

    for a in (Dog('Buddy'), Cat('Whiskers'), RobotDog('Rex')):
    animal_sound(a)

    输出:

    1
    2
    3
    Woof!
    Meow!
    Beep boop!
  • 抽象(Abstraction)

    用抽象基类(ABC)规定「子类必须实现哪些方法」,把接口和实现分开。没实现完就实例化会直接报错。

    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
    from abc import ABC, abstractmethod

    class Shape(ABC):
    @abstractmethod
    def area(self):
    ...

    @abstractmethod
    def perimeter(self):
    ...

    class Rectangle(Shape):
    def __init__(self, width, height):
    self.width, self.height = width, height

    def area(self):
    return self.width * self.height

    def perimeter(self):
    return 2 * (self.width + self.height)

    r = Rectangle(3, 4)
    print('面积 / 周长:', r.area(), r.perimeter())
    try:
    Shape() # 抽象类本身不能实例化
    except TypeError as e:
    print('实例化抽象类:', e)

    class Half(Shape): # 只实现了一个抽象方法
    def area(self):
    return 0

    try:
    Half()
    except TypeError:
    print('没实现完抽象方法: TypeError')

    输出:

    1
    2
    3
    面积 / 周长: 12 14
    实例化抽象类: Can't instantiate abstract class Shape without an implementation for abstract methods 'area', 'perimeter'
    没实现完抽象方法: TypeError
  • 整体示例(四个特性一起用)

    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
    from abc import ABC, abstractmethod

    # 抽象:规定接口
    class Account(ABC):
    def __init__(self, owner, balance=0):
    self.owner = owner
    self.__balance = balance # 封装:状态藏起来

    @abstractmethod
    def fee(self, amount):
    ...

    def deposit(self, amount):
    self.__balance += amount - self.fee(amount)

    def balance(self):
    return self.__balance

    # 继承 + 多态:两种账户手续费不同
    class FreeAccount(Account):
    def fee(self, amount):
    return 0

    class VipAccount(Account):
    def fee(self, amount):
    return amount * 0.01

    accounts = [FreeAccount('小明'), VipAccount('小红')]
    for acc in accounts: # 多态:同一个 deposit,不同手续费
    acc.deposit(100)
    print(type(acc).__name__, acc.owner, acc.balance())
    print('都是 Account:', all(isinstance(a, Account) for a in accounts))

    实跑输出:

    1
    2
    3
    FreeAccount 小明 100
    VipAccount 小红 99.0
    都是 Account: True

类的语法

  • 定义与实例化

    1
    2
    3
    4
    5
    6
    7
    class MyClass:
    """这是一个简单的类示例""" # 类的文档字符串,help() 会显示
    pass

    obj = MyClass() # 调用类 = 创建实例
    print(type(obj).__name__, isinstance(obj, MyClass))
    print('文档字符串:', MyClass.__doc__)

    输出:

    1
    2
    MyClass True
    文档字符串: 这是一个简单的类示例
  • __init__ 与实例方法

    __init__ 是初始化方法(不是「构造函数」,真正创建对象的是 __new__),第一个参数固定是 self,指向当前实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person:
    def __init__(self, name, age):
    self.name = name # 实例属性:每个实例各一份
    self.age = age

    def greet(self):
    return f"Hello, my name is {self.name} and I'm {self.age} years old."

    p = Person('小明', 18)
    print(p.greet())
    print('实例属性存在实例字典里:', vars(p))

    输出:

    1
    2
    Hello, my name is 小明 and I'm 18 years old.
    实例属性存在实例字典里: {'name': '小明', 'age': 18}
  • 类属性 vs 实例属性

    类属性挂在类上、所有实例共享;实例属性挂在实例上。读的时候先找实例再找类,所以给实例赋值同名属性会「遮蔽」类属性,而不是改它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Circle:
    pi = 3.14159 # 类属性

    def __init__(self, radius):
    self.radius = radius

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

    c1, c2 = Circle(1), Circle(2)
    print('共享类属性:', c1.pi == c2.pi, '| 面积:', c1.area(), c2.area())
    c1.pi = 3 # 这只是给 c1 加了个实例属性
    print('遮蔽后 c1 / c2 / 类:', c1.pi, c2.pi, Circle.pi)
    print('c1 字典里多了 pi:', 'pi' in vars(c1), '| c2 没有:', 'pi' not in vars(c2))

    输出:

    1
    2
    3
    共享类属性: True | 面积: 3.14159 12.56636
    遮蔽后 c1 / c2 / 类: 3 3.14159 3.14159
    c1 字典里多了 pi: True | c2 没有: True

    另一个经典坑:类属性是可变对象时,所有实例共享同一个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Bad:
    tags = [] # 类属性是列表 -> 大家共用一个

    class Good:
    def __init__(self):
    self.tags = [] # 每个实例自己建

    b1, b2 = Bad(), Bad()
    b1.tags.append('x')
    print('Bad 共享:', b2.tags)
    g1, g2 = Good(), Good()
    g1.tags.append('x')
    print('Good 独立:', g2.tags)

    输出:

    1
    2
    Bad 共享: ['x']
    Good 独立: []
  • 类方法与静态方法

    装饰器 第一个参数 能访问什么 典型用途
    无(实例方法) self 实例 + 类 操作实例状态
    @classmethod cls 类(含类属性),拿不到实例 替代构造函数、计数器
    @staticmethod 无 都拿不到(除非显式传) 与类相关但不依赖状态的工具函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Counter:
    count = 0 # 类属性当计数器

    def __init__(self):
    Counter.count += 1

    @classmethod
    def total(cls):
    return cls.count # cls 就是类本身,子类调用时 cls 是子类

    @staticmethod
    def add(a, b):
    return a + b # 不需要 self / cls

    Counter()
    Counter()
    print('实例方法造了两个:', Counter.total())
    print('类方法也能从实例调:', Counter().total())
    print('静态方法:', Counter.add(1, 2), Counter().add(3, 4))

    输出:

    1
    2
    3
    实例方法造了两个: 2
    类方法也能从实例调: 3
    静态方法: 3 7
  • 整体示例(扑克牌换手)

    题目:小明左手 ♥K、右手 ♠A,交换两手的牌后分别是什么?思路是先找对象(人、手、牌),抽象成类,再按题意创建对象、调方法。

    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
    class Poker:
    def __init__(self, color, number):
    self.color = color
    self.number = number

    def __str__(self):
    return '{}{}'.format(self.color, self.number)

    class Hand:
    def __init__(self, poker=None):
    self.poker = poker

    def hold_poker(self, poker):
    self.poker = poker

    class Person:
    def __init__(self, name, left_hand, right_hand):
    self.name = name
    self.left_hand = left_hand
    self.right_hand = right_hand

    def show(self):
    print('{}张开手'.format(self.name))
    print('左手:{}'.format(self.left_hand.poker))
    print('右手:{}'.format(self.right_hand.poker))

    def swap(self):
    # 元组解包一行完成交换,不需要临时变量
    self.left_hand.poker, self.right_hand.poker = self.right_hand.poker, self.left_hand.poker
    print('{}交换两手的牌'.format(self.name))

    left_hand, right_hand = Hand(Poker('♥', 'K')), Hand(Poker('♠', 'A'))
    xiaoming = Person('小明', left_hand, right_hand)
    xiaoming.show()
    xiaoming.swap()
    xiaoming.show()

    实跑输出:

    1
    2
    3
    4
    5
    6
    7
    小明张开手
    左手:♥K
    右手:♠A
    小明交换两手的牌
    小明张开手
    左手:♠A
    右手:♥K

高级特性

  • 多重继承与 MRO

    一个类可以有多个父类。方法查找按 MRO(Method Resolution Order,C3 线性化)走,类.__mro__ 能直接看。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Flyable:
    def fly(self):
    return 'I can fly!'

    class Swimmable:
    def swim(self):
    return 'I can swim!'

    class Duck(Flyable, Swimmable):
    pass

    d = Duck()
    print(d.fly(), d.swim())
    print('MRO:', [c.__name__ for c in Duck.__mro__])

    输出:

    1
    2
    I can fly! I can swim!
    MRO: ['Duck', 'Flyable', 'Swimmable', 'object']

    两个父类有同名方法时,排在前面的父类优先;super() 也是按 MRO 找下一个,不是「找父类」。

  • 类装饰器

    装饰器不仅能装饰函数,也能装饰类:拿到类对象,改它,再还回去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def add_method(cls):
    def new_method(self):
    return 'This is a dynamically added method'
    cls.new_method = new_method # 往类上挂一个方法
    return cls

    @add_method # 等价于 MyClass = add_method(MyClass)
    class MyClass:
    pass

    print(MyClass().new_method())
    print('方法真的在类上:', 'new_method' in vars(MyClass))

    输出:

    1
    2
    This is a dynamically added method
    方法真的在类上: True
  • 数据类(Python 3.7+)

    @dataclass 按字段声明自动生成 __init__ / __repr__ / __eq__,省掉一堆样板代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from dataclasses import dataclass

    @dataclass
    class Point:
    x: float
    y: float
    z: float = 0.0 # 带默认值的字段要放后面

    p = Point(1.5, 2.5)
    print(p) # 自动 __repr__
    print('自动 __eq__:', Point(1, 2) == Point(1, 2))
    print('字段:', [f.name for f in __import__('dataclasses').fields(Point)])

    输出:

    1
    2
    3
    Point(x=1.5, y=2.5, z=0.0)
    自动 __eq__: True
    字段: ['x', 'y', 'z']
  • 整体示例(高级特性)

    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
    from dataclasses import dataclass, field

    # 1) 类装饰器:给一批类统一加「描述」能力
    def describable(cls):
    cls.describe = lambda self: f'{type(self).__name__}({vars(self)})'
    return cls

    # 2) dataclass + 多重继承
    class Serializable:
    def to_dict(self):
    return dict(vars(self))

    @describable
    @dataclass
    class User(Serializable):
    name: str
    age: int = 0
    tags: list = field(default_factory=list) # 可变默认值必须用 default_factory

    u = User('小明', 18)
    u.tags.append('vip')
    print(u)
    print('describe:', u.describe())
    print('to_dict:', u.to_dict())
    print('MRO:', [c.__name__ for c in User.__mro__])
    print('两个实例的 tags 独立:', User('a').tags == [])

    实跑输出:

    1
    2
    3
    4
    5
    User(name='小明', age=18, tags=['vip'])
    describe: User({'name': '小明', 'age': 18, 'tags': ['vip']})
    to_dict: {'name': '小明', 'age': 18, 'tags': ['vip']}
    MRO: ['User', 'Serializable', 'object']
    两个实例的 tags 独立: True

常见的内置函数(魔术方法)

以双下划线开头结尾的方法叫魔术方法(magic / dunder methods),它们在特定操作时被解释器自动调用,让你的类「长得像内置类型」。

  • 对象生命周期

    __new__ / __init__ / __del__

    __new__ 真正创建实例(静态方法,先于 __init__),__init__ 只做初始化,__del__ 在对象被回收时调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class MyClass:
    def __new__(cls, *args, **kwargs):
    print('创建实例 - __new__')
    return super().__new__(cls) # 必须返回一个实例,否则 __init__ 不会跑

    def __init__(self, value):
    print('初始化实例 - __init__')
    self.value = value

    def __del__(self):
    print('销毁实例 - __del__')

    obj = MyClass(10)
    del obj # 引用计数归零,立刻触发 __del__
    print('del 之后')

    输出:

    1
    2
    3
    4
    创建实例 - __new__
    初始化实例 - __init__
    销毁实例 - __del__
    del 之后

    注意 __del__ 不保证一定执行:循环引用要靠 GC、解释器退出时全局变量可能已被清掉。要确定性地释放资源,用 with / close(),别依赖 __del__。

  • 字符串表示

    __str__ / __repr__ / __format__

    __str__ 给人看(print / str),__repr__ 给开发者看(repr、交互式回显、容器里的元素);只写一个时,__repr__ 可以兜底 __str__,反过来不行。

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

    def __str__(self):
    return f'{self.name} ({self.age}岁)'

    def __repr__(self):
    return f"Person('{self.name}', {self.age})"

    def __format__(self, spec): # spec 就是冒号后面那段字符串
    return format(self.name, spec) if spec else str(self)

    p = Person('张三', 30)
    print('str:', str(p))
    print('repr:', repr(p))
    print('列表里用 repr:', [p])
    print('format 带宽度:', f'{p:>6}') # 等价于 format(p, '>6')
    print('format 空规格走 __str__:', f'{p}')

    输出:

    1
    2
    3
    4
    5
    str: 张三 (30岁)
    repr: Person('张三', 30)
    列表里用 repr: [Person('张三', 30)]
    format 带宽度: 张三
    format 空规格走 __str__: 张三 (30岁)

    f'{p:>6}' 这个写法拆开看:冒号前的 p 是要格式化的对象,冒号后的 >6 是格式说明符(format spec)—— > 表示右对齐(< 左对齐、^ 居中),6 表示总宽度,不够就用空格补齐。整段 '>6' 会原样传进 __format__(self, spec),也就是 spec == '>6';这里的 __format__ 又把它转手交给 format(self.name, '>6'),所以宽度规则其实就是字符串那套。

    还有一点容易看岔:宽度按字符个数算,不是按屏幕上的显示宽度。'张三' 只有 2 个字符,于是补 4 个空格变成 张三,在等宽字体里实际占 6 + 2 = 8 列(中文一个字顶两列)。真要做到视觉对齐,得先按显示宽度算出该补几个空格。

  • 属性访问

    __getattr__ / __setattr__ / __delattr__ / __getattribute__

    • __getattr__:只在属性找不到时调用,适合做兜底 / 代理。
    • __setattr__:每次赋值都调用,包括 __init__ 里的 self.x = ...。
    • __getattribute__:每次读属性都调用,优先级高于 __getattr__;里面再读 self.xxx 会无限递归,必须走 super().__getattribute__(name)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class AttributeDemo:
    def __init__(self):
    self.data = {} # 这一行也会触发 __setattr__

    def __getattr__(self, name):
    print(f'访问不存在的属性: {name}')
    return None

    def __setattr__(self, name, value):
    print(f'设置属性: {name} = {value}')
    super().__setattr__(name, value) # 不写这句就永远存不进去

    def __delattr__(self, name):
    print(f'删除属性: {name}')
    super().__delattr__(name)

    obj = AttributeDemo()
    obj.existing = '值'
    print('读已有属性不触发 __getattr__:', obj.existing)
    print('读不存在属性:', obj.nonexistent)
    del obj.existing

    输出:

    1
    2
    3
    4
    5
    6
    设置属性: data = {}
    设置属性: existing = 值
    读已有属性不触发 __getattr__: 值
    访问不存在的属性: nonexistent
    读不存在属性: None
    删除属性: existing
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Trap:
    def __getattribute__(self, name):
    return super().__getattribute__(name) # 正确:交给父类

    class Bad:
    def __getattribute__(self, name):
    return self.__dict__[name] # 错误:self.__dict__ 又触发自己

    t = Trap()
    t.x = 1
    print('正确写法:', t.x)
    try:
    Bad().anything
    except RecursionError:
    print('错误写法: RecursionError')

    输出:

    1
    2
    正确写法: 1
    错误写法: RecursionError
  • 容器类型模拟

    实现 __len__ / __getitem__ / __setitem__ / __delitem__ / __contains__ / __iter__,你的类就能用 len()、下标、in、for。

    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
    class MyList:
    def __init__(self, items):
    self.items = list(items)

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

    def __getitem__(self, index):
    return self.items[index]

    def __setitem__(self, index, value):
    self.items[index] = value

    def __delitem__(self, index):
    del self.items[index]

    def __contains__(self, item):
    return item in self.items

    def __iter__(self):
    return iter(self.items) # 交给列表的迭代器

    lst = MyList([1, 2, 3])
    print('len:', len(lst), '| 下标:', lst[1], '| in:', 3 in lst)
    lst[0] = 9
    del lst[1]
    print('改删之后:', list(lst))
    print('for:', [x for x in lst])

    输出:

    1
    2
    3
    len: 3 | 下标: 2 | in: True
    改删之后: [9, 3]
    for: [9, 3]

    一个有意思的兜底:只实现 __getitem__(从 0 开始的整数下标)也能 for,解释器会按 0,1,2… 一直取到 IndexError。这是老式序列协议,isinstance(x, Iterable) 却仍是 False(详见《函数使用(四)》)。

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

    def __getitem__(self, i):
    return self.items[i] # 越界抛 IndexError,for 靠它结束

    g = OnlyGet([1, 2, 3])
    print('没有 __iter__ 也能 for:', [x for x in g])
    from collections.abc import Iterable
    print('但 isinstance Iterable:', isinstance(g, Iterable))

    输出:

    1
    2
    没有 __iter__ 也能 for: [1, 2, 3]
    但 isinstance Iterable: False
  • 数值运算

    __add__ / __sub__ / __mul__ / __truediv__ / __floordiv__ / __mod__ / __pow__ / __and__ / __or__ / __xor__ 对应 + - * / // % ** & | ^。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Vector:
    def __init__(self, x, y):
    self.x, self.y = x, y

    def __add__(self, other):
    return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
    return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
    return Vector(self.x * scalar, self.y * scalar)

    def __str__(self):
    return f'Vector({self.x}, {self.y})'

    v1, v2 = Vector(2, 3), Vector(1, 4)
    print('v1 + v2:', v1 + v2)
    print('v1 - v2:', v1 - v2)
    print('v1 * 2:', v1 * 2)
    try:
    print(2 * v1) # 左操作数是 int,不会调 Vector.__mul__
    except TypeError as e:
    print('2 * v1:', e)

    输出:

    1
    2
    3
    4
    v1 + v2: Vector(3, 7)
    v1 - v2: Vector(1, -1)
    v1 * 2: Vector(4, 6)
    2 * v1: unsupported operand type(s) for *: 'int' and 'Vector'

    想让 2 * v1 也成立,补一个反射方法 __rmul__(reflect 在这里是「反过来」的意思——左边的类型没有对应方法时,Python 会反过来问右边的类型;和「运行时按名字自省」那个 reflection 不是一回事),它是右操作数版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Vector:
    def __init__(self, x, y):
    self.x, self.y = x, y

    def __mul__(self, scalar):
    return Vector(self.x * scalar, self.y * scalar)

    def __str__(self):
    return f'Vector({self.x}, {self.y})'

    class Vector2(Vector):
    __rmul__ = Vector.__mul__ # 复用同一份实现

    print('2 * v:', 2 * Vector2(2, 3))

    输出:

    1
    2 * v: Vector(4, 6)
  • 比较操作

    __eq__ / __ne__ / __lt__ / __le__ / __gt__ / __ge__ 对应 == != < <= > >=。两个容易忽略的行为:

    • a > b 在 a 没有 __gt__ 时会反射去调 b.__lt__(a),所以只写 __lt__ 也能让 > 工作。
    • 只定义 __eq__ 会让类变得不可哈希(__hash__ 被置成 None),对象就不能当字典键 / 进集合;要么一起定义 __hash__,要么用 @dataclass(frozen=True) / functools.total_ordering 这类工具。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Book:
    def __init__(self, title, pages):
    self.title, self.pages = title, pages

    def __eq__(self, other):
    return self.pages == other.pages

    def __lt__(self, other):
    return self.pages < other.pages

    def __le__(self, other):
    return self.pages <= other.pages

    b1, b2 = Book('Python入门', 300), Book('高级Python', 500)
    print('b1 < b2:', b1 < b2, '| b1 == b2:', b1 == b2)
    print('b1 > b2 走反射:', b1 > b2)
    print('b1 != b2 默认取反 __eq__:', b1 != b2)
    print('定义了 __eq__ 后 __hash__ 是 None:', Book.__hash__ is None)
    try:
    {b1: 1}
    except TypeError:
    print('当字典键: TypeError')

    输出:

    1
    2
    3
    4
    5
    b1 < b2: True | b1 == b2: False
    b1 > b2 走反射: False
    b1 != b2 默认取反 __eq__: True
    定义了 __eq__ 后 __hash__ 是 None: True
    当字典键: TypeError

    懒得写全六个比较方法时,用 @total_ordering:给 __eq__ 加任意一个顺序方法,它补齐其余的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from functools import total_ordering

    class Book:
    def __init__(self, title, pages):
    self.title, self.pages = title, pages

    def __eq__(self, other):
    return self.pages == other.pages

    def __lt__(self, other):
    return self.pages < other.pages

    @total_ordering
    class Book2(Book):
    pass

    c1, c2 = Book2('a', 1), Book2('b', 2)
    print('补齐后 > >= :', c1 > c2, c1 >= c2, c2 >= c1)

    输出:

    1
    补齐后 > >= : False False True
  • 其他重要方法

    • __call__ 让实例能像函数一样被调用。
    • __enter__ / __exit__ 让实例支持 with。
    • __hash__ / __bool__ / __dir__ 分别接管 hash()、真值判断、dir()。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class CallableDemo:
    def __call__(self, *args):
    return f'被调用,参数: {args}'

    print(CallableDemo()(1, 2, 3))
    print('callable() 认它:', callable(CallableDemo()))

    class ContextManagerDemo:
    def __enter__(self):
    print('进入上下文')
    return self

    def __exit__(self, exc_type, exc_val, exc_tb):
    print('退出上下文')
    return False # 返回 True 会吞掉异常,一般别这么干

    with ContextManagerDemo() as cm:
    print('在上下文中')

    输出:

    1
    2
    3
    4
    5
    被调用,参数: (1, 2, 3)
    callable() 认它: True
    进入上下文
    在上下文中
    退出上下文
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Demo:
    def __init__(self, n):
    self.n = n

    def __bool__(self):
    return self.n > 0 # 决定 if obj / bool(obj)

    def __hash__(self):
    return hash(self.n)

    def __dir__(self):
    return ['n'] # dir() 会排序后返回

    d = Demo(3)
    print('bool:', bool(d), bool(Demo(0)))
    print('能当字典键:', {d: 'ok'}[d])
    print('dir:', dir(d))

    输出:

    1
    2
    3
    bool: True False
    能当字典键: ok
    dir: ['n']
  • 整体示例(魔术方法)

    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
    43
    44
    class Playlist:
    """一个既能当容器、又能当上下文、还能直接调用的播放列表"""

    def __init__(self, name, songs=()):
    self.name = name
    self.songs = list(songs)

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

    def __getitem__(self, i):
    return self.songs[i]

    def __iter__(self):
    return iter(self.songs)

    def __contains__(self, song):
    return song in self.songs

    def __str__(self):
    return f'{self.name}({len(self)}首)'

    def __repr__(self):
    return f'Playlist({self.name!r}, {self.songs!r})'

    def __call__(self, song):
    self.songs.append(song) # 像函数一样「加歌」
    return self

    def __enter__(self):
    print('开始播放', self.name)
    return self

    def __exit__(self, *exc):
    print('停止播放', self.name)
    return False

    pl = Playlist('通勤', ['晴天', '稻香'])
    print('str / repr:', str(pl), '|', repr(pl))
    print('len / 下标 / in:', len(pl), pl[0], '稻香' in pl)
    pl('夜曲') # __call__
    with pl as p: # __enter__ / __exit__
    print('循环:', [s for s in p])
    print('结束后长度:', len(pl))

    实跑输出:

    1
    2
    3
    4
    5
    6
    str / repr: 通勤(2首) | Playlist('通勤', ['晴天', '稻香'])
    len / 下标 / in: 2 晴天 True
    开始播放 通勤
    循环: ['晴天', '稻香', '夜曲']
    停止播放 通勤
    结束后长度: 3

设计原则(SOLID)

写类时常用的五条经验,记名字不如记意思:

  1. 单一职责(SRP):一个类只负责一件事,改一个需求不该牵动别的类。
  2. 开放封闭(OCP):对扩展开放、对修改封闭——加功能靠新增子类 / 组合,而不是改老代码。
  3. 里氏替换(LSP):子类要能无缝替换父类,别在重写里收紧前置条件或抛出父类没有的异常。
  4. 接口隔离(ISP):别逼调用方依赖它用不到的方法,接口拆小。
  5. 依赖倒置(DIP):依赖抽象(接口 / ABC)而不是具体实现,上面的「抽象」一节就是它的落地方式。

参考

本文结束 感谢您的阅读
正在加载今日诗词....