文章
合集Python 核心与面向对象第 2 / 5 篇

Python 面向对象基础

类与实例

类(Class) 是对象的蓝图或模板,定义了一类事物共同拥有的属性和行为。
实例(Instance) 是根据类创建的具体对象,每个实例拥有独立的数据。

class Dog:
    species = "Canis"              # 类属性:所有实例共享

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

    def bark(self):
        print(f"{self.name} 汪汪!")

dog1 = Dog("小黑", 3)
dog2 = Dog("小白", 2)
dog1.bark()   # 小黑 汪汪!
dog2.bark()   # 小白 汪汪!

self 是 Python 约定的第一个参数,代表当前实例本身。调用方法时 Python 自动将实例传入,无需手动传递。

类与实例的关系:

对比项类实例
是什么模板 / 蓝图具体对象
属性类属性(共享)实例属性(独立)
创建方式class Dog: ...Dog("小黑", 3)
内存存储方法和类属性存储各自的实例数据

检查类型与继承关系:

print(isinstance(dog1, Dog))     # True:dog1 是 Dog 的实例
print(isinstance(dog1, object))  # True:所有类都继承自 object
print(issubclass(Dog, object))   # True:检查类之间的继承关系
print(type(dog1))                # <class '__main__.Dog'>
print(type(dog1) is Dog)         # True

属性

实例属性 vs 类属性

实例属性:在 __init__ 中通过 self 定义,每个实例独立持有,修改不影响其他实例。
类属性:定义在类体中(__init__ 之外),所有实例共享同一份数据。

class Counter:
    count = 0          # 类属性:记录创建了多少个实例

    def __init__(self, name):
        Counter.count += 1       # 通过类名修改类属性
        self.name = name         # 实例属性
        self.id = Counter.count

c1 = Counter("A")
c2 = Counter("B")
print(Counter.count)  # 2
print(c1.id, c2.id)   # 1 2

实例属性遮蔽类属性:当实例定义了与类属性同名的属性时,实例会优先使用自己的,但类属性不受影响。

class Dog:
    species = "Canis"

d1 = Dog()
d1.species = "改了"      # 在 d1 的 __dict__ 里新建了同名属性
print(d1.species)        # 改了(访问的是实例属性)
print(Dog.species)       # Canis(类属性不变)

d2 = Dog()
print(d2.species)        # Canis(d2 没有同名实例属性,回退到类属性)

属性查找顺序

Python 查找属性的顺序(LEGB 的类版本):

实例 __dict__ → 类 __dict__ → 父类链(按 MRO 顺序)

更精确的顺序(涉及描述符时)见 元编程 → 「描述符」。

__dict__:属性的底层存储

实例和类的属性,本质上都存储在字典里:

class Person:
    role = "human"
    def __init__(self, name):
        self.name = name

p = Person("Alice")
print(p.__dict__)       # {'name': 'Alice'}
print(Person.__dict__)  # {'role': 'human', '__init__': <function ...>, ...}

方法也是属性——它们是存储在类 __dict__ 里的函数对象。通过实例访问时,Python 会自动将其包装成绑定方法(bound method),把实例作为第一个参数传入。

方法

方法的本质区别在于绑定对象不同,决定了方法内部能访问什么。

实例方法

第一个参数是 self(当前实例),可以访问实例属性和类属性,是最常用的方法类型。

class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):          # 实例方法
        print(f"{self.name} 汪汪")

类方法(@classmethod)

第一个参数是 cls(类本身),不能访问实例属性。最典型的用途是作为替代构造函数(工厂方法):

class Date:
    def __init__(self, year, month, day):
        self.year, self.month, self.day = year, month, day

    @classmethod
    def from_string(cls, s):          # 从字符串构建
        year, month, day = map(int, s.split("-"))
        return cls(year, month, day)  # cls 而非 Date,子类继承后仍正确

    @classmethod
    def today(cls):                   # 从系统时间构建
        import datetime
        d = datetime.date.today()
        return cls(d.year, d.month, d.day)

d = Date.from_string("2024-03-15")
print(d.year, d.month, d.day)   # 2024 3 15

静态方法(@staticmethod)

没有 self 也没有 cls,与类本身没有绑定关系,只是"逻辑上属于这个类"的工具函数。

class MathUtils:
    @staticmethod
    def clamp(value, min_val, max_val):
        return max(min_val, min(max_val, value))

print(MathUtils.clamp(15, 0, 10))   # 10

三种方法对比

方法类型装饰器第一个参数能访问实例属性能访问类属性典型用途
实例方法无self✅✅操作实例数据
类方法@classmethodcls❌✅替代构造函数、操作类级别数据
静态方法@staticmethod无❌❌(需显式类名)工具函数

封装

封装(Encapsulation):隐藏内部实现,只暴露必要接口。

访问控制约定

Python 没有语言级别的强制访问控制,而是用命名约定表达意图:

命名含义示例
name公开(public),随意访问self.name
_name受保护(protected),约定不在类外直接访问self._age
__name私有(private),触发名称重整self.__secret

名称重整(Name Mangling)

__xxx(双下划线前缀)会被 Python 改写成 _ClassName__xxx,防止在子类中被意外覆盖:

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance      # 实际存储为 _BankAccount__balance

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

    def get_balance(self):
        return self.__balance

acc = BankAccount(1000)
# acc.__balance       # ❌ AttributeError
# acc._BankAccount__balance  # 可以访问,但强烈不推荐
print(acc.get_balance())      # 1000

核心理解:Python 的私有不是"绝对禁止",而是"意图声明 + 防止误用"。约定优于强制,责任在开发者。

用 property 控制访问(推荐方式)

相比裸的 getter/setter 方法,property 提供了更 Pythonic 的方式:

class Person:
    def __init__(self, age):
        self._age = age

    @property
    def age(self):              # 对外表现得像普通属性
        return self._age

    @age.setter
    def age(self, value):       # 但赋值时可以做校验
        if not isinstance(value, int) or value < 0:
            raise ValueError("年龄必须是非负整数")
        self._age = value

p = Person(20)
print(p.age)    # 20,像访问普通属性
p.age = 25      # 触发 setter 校验
p.age = -1      # ❌ ValueError

详见 python-oop-advanced → 「property」

继承

继承(Inheritance) 让子类复用父类的属性和方法,建立 "is-a"(是一个)的关系。

基本继承

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

    def eat(self):
        print(f"{self.name} 在吃东西")

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

class Dog(Animal):    # Dog 继承 Animal
    def speak(self):  # 方法重写(override)
        print(f"{self.name} 汪汪!")

class Cat(Animal):
    def speak(self):
        print(f"{self.name} 喵喵~")

d = Dog("小黑")
d.eat()    # 小黑 在吃东西(继承自 Animal)
d.speak()  # 小黑 汪汪!(重写后的版本)

super() 调用父类

重写方法时,用 super() 调用父类版本,实现"扩展而不是替换":

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

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # 必须调用,否则 self.name 不会被设置
        self.breed = breed

    def speak(self):
        print(f"{self.name}({self.breed})汪汪!")

d = Dog("小黑", "拉布拉多")
d.speak()   # 小黑(拉布拉多)汪汪!

super() 不是"调用父类",而是"沿着 MRO 链调用下一个类",详见 python-oop-advanced → 「super()」。

继承与私有属性

父类的私有属性(__xxx)会进行名称重整,子类无法直接访问:

class Animal:
    def __init__(self):
        self.__secret = "动物的秘密"

class Dog(Animal):
    def reveal(self):
        # print(self.__secret)          # ❌ AttributeError
        print(self._Animal__secret)     # ✅ 能访问,但不应这么做

多态

多态(Polymorphism):同一套调用接口,不同对象产生不同行为。

animals = [Dog("小黑", "拉布拉多"), Cat("小白"), Dog("旺财", "柴犬")]

for animal in animals:
    animal.speak()   # 调用方式完全相同
# 小黑(拉布拉多)汪汪!
# 小白 喵喵~
# 旺财(柴犬)汪汪!

多态的价值:

# ❌ 没有多态时,需要类型判断
for a in animals:
    if isinstance(a, Dog):
        a.speak()   # 每新增一种动物就要改这里
    elif isinstance(a, Cat):
        a.speak()

# ✅ 有多态后,新增 Bird 类完全不需要修改遍历逻辑
class Bird(Animal):
    def speak(self):
        print(f"{self.name} 啾啾~")

多态让代码对扩展开放(新增类型)、对修改关闭(无需改动已有逻辑)。

鸭子类型的详细讨论见 python-oop-advanced → 「鸭子类型」

__new__ 与 __init__

创建一个实例实际分两步:

  1. __new__(cls):向操作系统申请内存,创建并返回空白对象,这一步决定"返回哪个对象"
  2. __init__(self):接收 __new__ 返回的对象,对其进行初始化,这一步决定"如何填充数据"
class Demo:
    def __new__(cls, *args, **kwargs):
        print("1. __new__:分配内存,创建对象")
        instance = super().__new__(cls)
        return instance           # 必须返回实例,否则 __init__ 不会被调用

    def __init__(self, value):
        print("2. __init__:初始化对象")
        self.value = value

d = Demo(42)
# 1. __new__:分配内存,创建对象
# 2. __init__:初始化对象

什么时候需要重写 __new__?

绝大多数情况下只需要重写 __init__。__new__ 主要用于:

  • 单例模式:控制只创建一个实例(见 python-design-patterns → 「单例模式」)
  • 继承不可变类型:如 int、str、tuple,它们的值在创建时确定,只能在 __new__ 中修改
class PositiveInt(int):
    def __new__(cls, value):
        if value <= 0:
            raise ValueError("必须是正整数")
        return super().__new__(cls, value)

n = PositiveInt(5)    # ✅
m = PositiveInt(-1)   # ❌ ValueError

单例模式的 __new__ 实现见 python-design-patterns → 「单例模式」

进阶内容见 → python-oop-advanced