14 - 面向对象编程

14 - 面向对象编程

面向对象编程(OOP)是编程里的一种"思维方式"。把数据和操作数据的方法打包在一起,形成"对象"。这章讲基础概念,下一章讲继承和多态。


为什么需要面向对象

假设你要写一个游戏,里面有各种角色。每个角色有血量、攻击力这些属性,也有攻击、加血这些行为。

不用面向对象的话,你可能用一堆字典来存:

python 复制代码
hero1 = {"name": "勇士", "hp": 100, "attack": 15}
hero2 = {"name": "法师", "hp": 80, "attack": 25}

def attack(attacker, target):
    target["hp"] -= attacker["attack"]
    print(f"{attacker['name']} 攻击了 {target['name']}")

def heal(target, amount):
    target["hp"] += amount

这也能用,但数据和操作散落在各处,项目大了就很难管理。

面向对象的方式:

python 复制代码
class Hero:
    def __init__(self, name, hp, attack):
        self.name = name
        self.hp = hp
        self.attack_power = attack

    def attack(self, target):
        target.hp -= self.attack_power
        print(f"{self.name} 攻击了 {target.name},造成 {self.attack_power} 点伤害")

    def heal(self, amount):
        self.hp += amount
        print(f"{self.name} 恢复了 {amount} 点血量,当前血量 {self.hp}")

数据和行为打包在一起,清晰多了。


类和实例

  • 类(class):一个模板/蓝图,定义了"这种东西"有什么属性和行为
  • 实例(instance):根据模板创建出来的具体对象
python 复制代码
class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

# 创建实例
dog1 = Dog("旺财", "金毛")
dog2 = Dog("小黑", "哈士奇")

Dog 是类(模板),dog1dog2 是实例(具体对象)。每个实例有自己的 namebreed


__init__ 方法

__init__ 是初始化方法,在创建实例时自动调用。它负责设置对象的初始状态。

python 复制代码
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
        self.created_at = "2024-01-01"
        self.is_active = True

self 是什么?它代表"当前这个实例"。每个方法第一个参数都是 self(这个名字是约定,你也可以写别的,但千万别这么干,所有人都在用 self)。

创建实例时不需要手动传 self,Python 会自动传:

python 复制代码
user = User("小明", "xm@example.com")
# 等价于 User.__init__(user, "小明", "xm@example.com")

属性和方法

实例属性

python 复制代码
class Circle:
    def __init__(self, radius):
        self.radius = radius  # 实例属性

c = Circle(5)
print(c.radius)  # 5
c.radius = 10    # 可以直接修改

实例方法

python 复制代码
class Circle:
    def __init__(self, radius):
        self.radius = radius

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

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

c = Circle(5)
print(c.area())       # 78.53975
print(c.perimeter())  # 31.4159

类属性

所有实例共享的属性:

python 复制代码
class Dog:
    species = "犬科"  # 类属性,所有实例共享

    def __init__(self, name):
        self.name = name  # 实例属性,每个实例不同

dog1 = Dog("旺财")
dog2 = Dog("小黑")

print(dog1.species)  # 犬科
print(dog2.species)  # 犬科
print(Dog.species)   # 犬科(通过类名也能访问)

类属性适合存所有实例都一样的值,比如配置项、常量。

类属性 vs 实例属性

python 复制代码
class Dog:
    tricks = []  # 类属性(所有实例共享!)

    def __init__(self, name):
        self.name = name
        self.toys = []  # 实例属性(每个实例独立)

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

    def add_toy(self, toy):
        self.toys.append(toy)

dog1 = Dog("旺财")
dog2 = Dog("小黑")

dog1.add_trick("握手")
dog1.add_toy("球")

print(dog1.tricks)  # ['握手']
print(dog2.tricks)  # ['握手'] ← 也被影响了!因为共享的
print(dog1.toys)    # ['球']
print(dog2.toys)    # [] ← 没被影响,因为独立的

可变对象(列表、字典)作为类属性时要特别小心,容易被意外共享。


self 的理解

很多初学者搞不明白 self。其实很简单------它就是"我自己"。

python 复制代码
class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1

    def get_count(self):
        return self.count

当你调用 c.increment() 时,Python 实际做的是 Counter.increment(c)self 就是 c

所以你在方法里用 self.count 就是在访问"这个实例自己的 count"。不同的实例有不同的 count,互不影响。


属性的访问控制

Python 没有像 Java 那样的 privatepublic 关键字。它用命名约定来表示:

公有属性

正常的名字,谁都能访问:

python 复制代码
class User:
    def __init__(self, name):
        self.name = name  # 公有属性

user = User("小明")
print(user.name)  # 随便访问

受保护属性(约定)

一个下划线开头,表示"这是内部用的,别从外面直接动":

python 复制代码
class User:
    def __init__(self, name):
        self._age = 25  # 受保护(约定)

user = User("小明")
print(user._age)  # 技术上能访问,但不应该

这只是个约定,Python 不会阻止你访问。但好的程序员会遵守这个约定。

私有属性(名称改写)

两个下划线开头,Python 会对名字做"改写"(name mangling):

python 复制代码
class User:
    def __init__(self, name):
        self.__password = "secret"  # 私有

    def check_password(self, pwd):
        return pwd == self.__password

user = User("小明")
# print(user.__password)  # AttributeError!
print(user.check_password("secret"))  # True

# 实际上还是能访问,只是名字被改了
print(user._User__password)  # secret(不建议这么用)

两个下划线不是为了"绝对安全",而是为了防止子类意外覆盖父类的属性。别把它当成 Java 的 private。


property 装饰器

如果你想控制属性的读写(比如设置年龄时做校验),用 @property

python 复制代码
class User:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        """获取年龄"""
        return self._age

    @age.setter
    def age(self, value):
        """设置年龄(带校验)"""
        if value < 0 or value > 150:
            raise ValueError("年龄不合理")
        self._age = value


user = User("小明", 25)
print(user.age)   # 25(调用 getter)
user.age = 30     # 调用 setter
# user.age = -5   # ValueError: 年龄不合理

用起来跟直接访问属性一样(user.age),但背后有方法帮你做校验。这个设计很优雅------以后想加校验逻辑,不用改调用方的代码。


魔术方法(dunder methods)

双下划线开头和结尾的方法叫"魔术方法"或"特殊方法"。它们让你的类可以跟 Python 的内置语法配合:

python 复制代码
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # print() 时显示什么
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    # str() 时返回什么
    def __str__(self):
        return f"({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

    # 支持 len()
    def __len__(self):
        return int((self.x**2 + self.y**2) ** 0.5)


v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1)          # (1, 2)
print(v1 + v2)     # (4, 6)
print(v1 == v2)    # False
print(repr(v1))    # Vector(1, 2)

常用的魔术方法:

方法 作用 对应语法
__init__ 初始化 Class()
__str__ 字符串表示 str(), print()
__repr__ 开发者表示 repr()
__len__ 长度 len()
__eq__ 相等比较 ==
__lt__ 小于比较 <
__add__ 加法 +
__getitem__ 索引访问 obj[key]
__iter__ 迭代 for x in obj
__contains__ 包含检查 x in obj

不用全部记住,需要的时候查文档就行。


一个完整的例子

python 复制代码
class TodoList:
    """待办事项列表"""

    def __init__(self, name):
        self.name = name
        self._items = []

    def add(self, item: str):
        """添加待办事项"""
        self._items.append({"text": item, "done": False})

    def complete(self, index: int):
        """标记完成"""
        if 0 <= index < len(self._items):
            self._items[index]["done"] = True
        else:
            raise IndexError("无效的索引")

    @property
    def pending(self):
        """未完成的事项"""
        return [item for item in self._items if not item["done"]]

    @property
    def done(self):
        """已完成的事项"""
        return [item for item in self._items if item["done"]]

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

    def __repr__(self):
        return f"TodoList('{self.name}', {len(self)} 项)"

    def __str__(self):
        lines = [f"📋 {self.name}"]
        for i, item in enumerate(self._items):
            status = "✅" if item["done"] else "⬜"
            lines.append(f"  {status} {i}. {item['text']}")
        return "\n".join(lines)


# 使用
todos = TodoList("今日任务")
todos.add("学 Python")
todos.add("写代码")
todos.add("摸鱼")

todos.complete(2)

print(todos)
print(f"\n{todos}")
print(f"待完成:{len(todos.pending)} 项")

__slots__:内存优化

默认情况下,Python 对象用 __dict__(一个字典)来存储属性。这很灵活(可以动态添加属性),但每个对象都要维护一个字典,内存开销不小。

如果你需要创建大量对象,可以用 __slots__ 来优化:

python 复制代码
# 普通类
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 使用 __slots__ 的类
class PointSlots:
    __slots__ = ["x", "y"]

    def __init__(self, x, y):
        self.x = x
        self.y = y

两个类功能一样,但内存差距很大:

python 复制代码
import sys

p1 = Point(1, 2)
p2 = PointSlots(1, 2)

print(sys.getsizeof(p1) + sys.getsizeof(p1.__dict__))  # 大约 152 字节
print(sys.getsizeof(p2))                                 # 大约 56 字节

__slots__ 告诉 Python:"这个类只会有这些属性,别给我分配 __dict__。" 内存能省 40%-50%。

限制

python 复制代码
class Point:
    __slots__ = ["x", "y"]

p = Point()
p.x = 1
p.y = 2
# p.z = 3  # AttributeError! 不能动态添加 __slots__ 之外的属性

什么时候用?

  • 大量创建同类型对象时(比如游戏里的粒子、数据处理的记录)
  • 追求内存效率

日常写类不需要加 __slots__,灵活性比那点内存重要。只有当你创建了几十万个对象、发现内存不够用的时候,再考虑。


本章小结

  • 类是模板,实例是根据模板创建的对象
  • __init__ 初始化实例,self 代表当前实例
  • 类属性被所有实例共享,实例属性各自独立
  • 一个下划线是"受保护"约定,两个下划线会做名称改写
  • @property 可以控制属性的读写
  • 魔术方法让类跟 Python 内置语法配合(+len()print() 等)
  • __slots__ 可以节省内存,但失去了动态添加属性的灵活性

面试题

Q1:__str____repr__ 有什么区别?
点击查看答案

  • __str__:面向用户,返回可读性好的字符串。print()str() 调用。
  • __repr__:面向开发者,返回无歧义的字符串(理想情况下能用来重建对象)。repr() 和交互式解释器调用。
python 复制代码
class Point:
    def __str__(self):
        return "(3, 4)"
    def __repr__(self):
        return "Point(3, 4)"

如果只定义了一个,优先定义 __repr__,因为 __str__ 没定义时 Python 会自动用 __repr__ 代替。

Q2:类属性和实例属性有什么区别?什么时候用类属性?
点击查看答案

  • 类属性:定义在类中(方法外),所有实例共享同一个值
  • 实例属性 :定义在 __init__ 中,每个实例有自己独立的值

用类属性的场景:

  1. 所有实例共享的常量(如 PI = 3.14159
  2. 实例计数器
  3. 默认配置

注意:可变对象(列表、字典)作为类属性容易被意外共享,应谨慎使用。

Q3:Python 中如何实现私有属性?真的"私有"吗?
点击查看答案

Python 用命名约定实现"伪私有":

  • _name:约定为受保护,外部不应访问(但不强制)
  • __name:名称改写为 _ClassName__name,增加访问难度

但都不是真正的私有------技术上仍然可以访问:

python 复制代码
obj._Class__name  # 可以访问双下划线属性

Python 的理念是"大家都是成年人",靠约定而非强制。如果需要真正的封装(如防止外部修改内部状态),用 @property 提供受控的访问接口。

Q4:@property 装饰器的作用是什么?
点击查看答案

@property 把方法变成"属性"来访问,实现受控的属性读写:

python 复制代码
class User:
    @property
    def age(self):          # getter
        return self._age

    @age.setter
    def age(self, value):   # setter
        if value < 0:
            raise ValueError
        self._age = value

user.age = 25   # 调用 setter
print(user.age) # 调用 getter

好处:

  1. 语法跟直接访问属性一样简洁
  2. 可以加校验、计算逻辑、日志等
  3. 以后从普通属性改为 property 不需要改调用方代码(向后兼容)

相关推荐
py小王子2 小时前
期刊复现| Python 实现带边缘密度与残差检验的回归拟合图
python·期刊复现
莫***妞2 小时前
2026年java后端开发还有未来吗? 就业形式如何?
java·开发语言
知识分享小能手2 小时前
Flask入门学习教程,从入门到精通,Flask智能租房——列表页 知识点详解(7)
python·学习·flask
MC皮蛋侠客2 小时前
C++17 多线程系列(一):线程基础——std::thread 完全指南
开发语言·c++·多线程
nickel3692 小时前
Qoder相关使用
java·开发语言·vue.js·spring boot
极客小云2 小时前
【从 while 循环到可视化智能体:深入拆解 Agent Loop、Codex 风格工具调用、OpenClaw 与 Hermes 背后的技术细节】
数据库·python·大模型·agent·codex·openclaw·hermes
两年半的个人练习生^_^2 小时前
Java IO流之BIO
java·开发语言
wh_xia_jun2 小时前
HttpRunner 编写测试用例
开发语言·lua
Larcher2 小时前
Python List、切片与大模型:从入门到实践的优雅之旅
python·ai编程