Python 面向对象编程笔记:从 Class 到 Dunder Method
本文最后更新于 31 天前,如有失效请评论区留言。

最近在刷 CS61A 的时候发现,Python 的面向对象编程(OOP)这块内容其实挺有意思的。之前写 Python 一直是函数式风格,能用 function 就不用 class,但学到后面发现 class 这东西有些场景下是真的好用。今天就来聊聊 Python OOP 的那些事儿。

为什么要用 Class?

说实话,Python 里很多东西不用 class 也能搞。但是当你需要把数据操作数据的方法绑在一起的时候,class 就很香了。比如你在做一个游戏,有很多角色,每个角色有名字、血量、攻击力,还能攻击、回血——这时候用 dict 来管理就很混乱了。

举个简单的例子:

class Character:
    def __init__(self, name, hp, attack):
        self.name = name
        self.hp = hp
        self.attack = attack
    
    def is_alive(self):
        return self.hp > 0
    
    def take_damage(self, damage):
        self.hp -= damage
        if self.hp < 0:
            self.hp = 0

这样每个角色就是一个 Character 对象,数据和行为都在一起。比起用一堆 dict 加一堆函数,可读性好太多了。

__init__ 和 self 到底是什么

Python 新手最困惑的可能就是 self 这个东西了。每次定义方法都要写 self 作为第一个参数,调用的时候又不用传,这是什么鬼?

self 就是指向当前实例的引用。当你调用 hero.take_damage(10) 的时候,Python 会自动把它变成 Character.take_damage(hero, 10)。所以 self 是 Python 帮你传进去的,你不用手动写。

__init__ 是一个 special method(也叫 dunder method,因为两边都是 double underscore),它在你创建对象的时候被调用。注意它不是 constructor,严格来说 Python 的 constructor 是 __new__,但 99% 的情况下你只需要管 __init__ 就行了。

# 这两行是等价的
hero = Character("Alice", 100, 25)
hero = Character.__init__(hero, "Alice", 100, 25)  # 不推荐这样写,但意思到了

Inheritance:继承这东西其实挺好理解的

继承就是让你基于已有的 class 创建新的 class。比如我们有 Character,现在想加一个 Wizard 角色,他除了普通攻击还能放魔法。不需要重写一遍所有代码,直接继承:

class Wizard(Character):
    def __init__(self, name, hp, attack, mana):
        super().__init__(name, hp, attack)  # 调用父类的 __init__
        self.mana = mana
    
    def cast_spell(self, spell_name, cost):
        if self.mana >= cost:
            self.mana -= cost
            print(f"{self.name} casts {spell_name}!")
            return True
        print("Not enough mana!")
        return False

super().__init__() 这个调用很重要。如果不写的话,父类的 __init__ 就不会被调用,你的 Wizard 对象就不会有 namehp 这些属性。

Python 支持多重继承(multiple inheritance),但说实话日常开发中很少用到。用多了容易出问题,比如著名的 diamond problem。MRO(Method Resolution Order)能帮你解决一部分,但我觉得能避免就避免。

Dunder Methods:让对象更好用

Dunder method(double underscore method)是 Python OOP 里最酷的部分。通过定义这些 special method,你可以让你的对象像内置类型一样使用。

几个最常用的:

  • __str__:定义 print(obj) 时显示的内容
  • __repr__:定义在交互式环境里直接输入 obj 显示的内容
  • __len__:定义 len(obj) 的行为
  • __eq__:定义 obj1 == obj2 的比较逻辑
  • __add__:定义 obj1 + obj2 的行为
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __len__(self):
        # 让 len() 返回向量的维度
        return 2

# 用起来就很自然
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # Vector(4, 6)
print(v1 == v2)  # False
print(len(v1))   # 2

一开始我也搞不懂这些 dunder method 有什么用,后来发现一旦你开始写 library 或者 API,这些东西就变得非常重要了。让你的 class 用起来像 Python 原生类型,用户就不需要记新的接口。

Property:让方法看起来像属性

有时候你想在访问属性的时候做一些额外操作(比如验证、计算),但又不想改调用方式。这时候 @property 就派上用场了:

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value
    
    @property
    def area(self):
        """计算面积,不需要括号"""
        import math
        return math.pi * self._radius ** 2

c = Circle(5)
print(c.area)      # 78.53...  注意没有括号
c.radius = 10      # 用的是 setter
print(c.area)      # 314.15...

用了 @property 之后,c.area 看起来像是在访问一个普通属性,但实际上每次访问都会调用方法来计算。这样 API 的使用者不需要知道背后是方法还是属性,用起来更自然。

Class Variable vs Instance Variable

这个区别刚开始很容易搞混。简单来说:

  • Instance variable:每个实例各有一份,定义在 __init__ 里的 self.xxx
  • Class variable:所有实例共享一份,定义在 class body 里
class Dog:
    species = "Canis familiaris"  # class variable,所有狗共享
    dog_count = 0                 # 统计创建了多少只狗
    
    def __init__(self, name):
        self.name = name          # instance variable,每只狗不同
        Dog.dog_count += 1

d1 = Dog("Buddy")
d2 = Dog("Max")
print(d1.species)       # Canis familiaris
print(d2.species)       # Canis familiaris
print(Dog.dog_count)    # 2

一个常见的坑:如果你试图通过 instance 去修改 class variable,实际上会创建一个同名的 instance variable,而不是修改 class variable:

d1.species = "Wolf"     # 创建了一个 instance variable
print(d1.species)        # Wolf(instance variable)
print(d2.species)        # Canis familiaris(class variable 没变)
print(Dog.species)       # Canis familiaris

实用技巧和 Pattern

最后分享几个实用的 pattern:

1. dataclass —— 懒人福音

Python 3.7 引入的 dataclass 可以自动生成 __init____repr____eq__ 等方法,省去大量样板代码:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float
    
    def distance_to(self, other):
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

p = Point(1.0, 2.0)
print(p)  # Point(x=1.0, y=2.0)  自动生成的 __repr__

2. Composition over Inheritance

虽然继承很方便,但在很多情况下 composition(组合)是更好的选择。简单说就是”has-a” 比 “is-a” 更灵活:

class Engine:
    def start(self):
        print("Engine started")

class Car:
    def __init__(self):
        self.engine = Engine()  # Car has an Engine
    
    def start(self):
        self.engine.start()
        print("Car is ready to go")

3. __slots__ 节省内存

如果你要创建大量对象(比如几百万个),可以用 __slots__ 来减少内存占用:

class Point:
    __slots__ = ['x', 'y']  # 只允许这两个属性
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 每个实例省掉了 __dict__ 的开销,内存少用约 40%

总结

Python 的 OOP 跟 Java、C++ 那套不太一样,它更灵活也更 “Pythonic”。几个关键点:

  • self 就是实例引用,Python 自动传
  • 继承用 super() 调用父类方法
  • Dunder method 让你的对象像内置类型一样好用
  • @property 让方法伪装成属性
  • Class variable 是所有实例共享的,instance variable 是每个实例独有的
  • 能用 dataclass 就用,省事
  • Composition 很多时候比 Inheritance 更好

好了,以上就是我对 Python OOP 的一些理解和笔记。写 class 这件事,一开始会觉得很繁琐,但用熟了之后会发现它确实能让代码组织得更清楚。尤其是在写稍微大一点的项目的时候,没有 class 真的会很乱。

版权声明:除特殊说明,博客文章均为原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇