面向对象编程(Object-Oriented Programming,简称 OOP)是 Python 中最重要的编程范式之一。学会它,你的代码会更有条理、更易复用、更易维护。
一、什么是面向对象?(先建立直觉)
1. 两种编程思维对比
过程式编程(你之前可能写过的):
# 用变量和函数来描述dog1_name = "旺财"dog1_age = 3dog2_name = "来福"dog2_age = 5def dog_bark(name): print(f"{name} 在汪汪叫!")def dog_run(name): print(f"{name} 在跑!")dog_bark(dog1_name)dog_run(dog2_name)
面向对象编程:
# 把"狗"当作一个整体来设计class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): print(f"{self.name} 在汪汪叫!") def run(self): print(f"{self.name} 在跑!")dog1 = Dog("旺财", 3)dog2 = Dog("来福", 5)dog1.bark()dog2.run()
💡 核心思想:把现实世界中的事物抽象成"类"(模板),然后用"类"创建出具体的"对象"(实例)。数据(属性)和行为(方法)绑定在一起。
二、类和对象:最基本的概念
概念 | 类比 | 代码中 |
类(Class) | 设计图纸 | class Dog:
|
对象(Object) | 根据图纸造出的实物 | dog1 = Dog(...)
|
属性(Attribute) | 对象的特征(数据) | name, age
|
方法(Method) | 对象的行为(函数) | bark(), run()
|
三、定义类和创建对象
1. 最简单的类
class Dog: """这是一个狗的类""" pass # 暂时什么都不做,占位# 创建对象(实例化)dog1 = Dog()dog2 = Dog()print(dog1) # <__main__.Dog object at 0x...>print(dog2) # <__main__.Dog object at 0x...># 判断类型print(type(dog1)) # <class '__main__.Dog'>print(isinstance(dog1, Dog)) # True
📌 pass 是空语句,表示"这里先占个位置,以后填代码"。
2. 给对象添加属性(不推荐的方式)
class Dog: passdog1 = Dog()dog1.name = "旺财" # 直接给对象绑定属性dog1.age = 3dog2 = Dog()dog2.name = "来福"print(dog1.name) # 旺财print(dog2.name) # 来福
⚠️ 这种方式太随意了,不同对象的属性可能不一致。正规做法是用 __init__ 方法。
四、__init__ 构造方法(非常重要)
__init__ 是一个特殊方法,在创建对象时自动调用,用于初始化对象的属性。
class Dog: def __init__(self, name, age): """ self: 代表当前正在创建的对象自己 name, age: 创建对象时传入的参数 """ self.name = name # 把传入的 name 存到对象的 name 属性中 self.age = age # 把传入的 age 存到对象的 age 属性中# 创建对象时,参数传给 __init__dog1 = Dog("旺财", 3)dog2 = Dog("来福", 5)print(f"{dog1.name} 今年 {dog1.age} 岁") # 旺财 今年 3 岁print(f"{dog2.name} 今年 {dog2.age} 岁") # 来福 今年 5 岁
self 到底是什么?
class Dog: def __init__(self, name): print(f"self 是:{self}") # 打印 self 的内存地址 self.name = namedog1 = Dog("旺财")print(f"dog1 是:{dog1}") # 你会发现地址和 self 一样!
结论:self 就是对象自己。Python 会自动把调用方法的对象作为第一个参数传给 self。
💡 你也可以把 self 改成别的名字(如 me),但全世界都用 self,别特立独行。
五、实例方法
在类中定义的函数,第一个参数是 self,就是实例方法。只能通过对象来调用。
class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): """实例方法:狗叫""" print(f"{self.name}:汪汪汪!") def introduce(self): """实例方法:自我介绍""" print(f"大家好,我叫{self.name},今年{self.age}岁。") def have_birthday(self): """实例方法:过生日,年龄加 1""" self.age += 1 print(f"{self.name} 过生日了!现在 {self.age} 岁了。")# 使用dog1 = Dog("旺财", 3)dog1.bark() # 旺财:汪汪汪!dog1.introduce() # 大家好,我叫旺财,今年3岁。dog1.have_birthday() # 旺财 过生日了!现在 4 岁了。dog1.have_birthday() # 旺财 过生日了!现在 5 岁了。
六、类属性 vs 实例属性
1. 实例属性(每个对象独有的)
class Dog: def __init__(self, name): self.name = name # 实例属性dog1 = Dog("旺财")dog2 = Dog("来福")print(dog1.name) # 旺财print(dog2.name) # 来福
2. 类属性(所有对象共享的)
class Dog: species = "犬科" # 类属性:所有狗都属于犬科 count = 0 # 类属性:记录创建了多少只狗 def __init__(self, name): self.name = name # 实例属性 Dog.count += 1 # 每创建一只狗,计数加 1dog1 = Dog("旺财")dog2 = Dog("来福")dog3 = Dog("大黄")print(dog1.species) # 犬科(通过对象访问)print(Dog.species) # 犬科(通过类访问)print(Dog.count) # 3(共创建了 3 只狗)
📌 区别:
3. 注意陷阱:不要通过对象修改类属性
class Dog: species = "犬科"dog1 = Dog()dog2 = Dog()dog1.species = "猫科" # ⚠️ 这不是修改类属性!而是给 dog1 新建了一个实例属性!print(dog1.species) # 猫科(dog1 自己的实例属性)print(dog2.species) # 犬科(类属性没变)print(Dog.species) # 犬科(类属性没变)
正确修改类属性的方式:
Dog.species = "哺乳纲" # 通过类名修改print(dog2.species) # 哺乳纲
七、类方法和静态方法
1. 类方法:@classmethod
类方法的第一个参数是 cls(代表类本身),而不是 self(代表对象)。通常用于创建替代构造器或操作类属性。
class Dog: count = 0 def __init__(self, name, age): self.name = name self.age = age Dog.count += 1 @classmethod def get_count(cls): """类方法:获取当前创建了多少只狗""" return cls.count @classmethod def from_birth_year(cls, name, birth_year): """ 类方法:替代构造器 传入出生年份,自动计算年龄 """ age = 2026 - birth_year return cls(name, age) # cls 就是 Dog 类# 使用类方法dog1 = Dog("旺财", 3)dog2 = Dog("来福", 5)print(Dog.get_count()) # 2# 使用替代构造器dog3 = Dog.from_birth_year("大黄", 2020)print(dog3.name) # 大黄print(dog3.age) # 6
2. 静态方法:@staticmethod
静态方法不接收 self 也不接收 cls,就是一个普通函数,只是逻辑上属于这个类。
class MathUtils: @staticmethod def add(a, b): return a + b @staticmethod def is_even(n): return n % 2 == 0# 调用方式 1:通过类名(推荐)print(MathUtils.add(3, 5)) # 8print(MathUtils.is_even(4)) # True# 调用方式 2:通过对象(也可以,但不常用)util = MathUtils()print(util.add(10, 20)) # 30
📌 什么时候用静态方法?当某个函数逻辑上属于这个类,但又不需要访问类或对象的任何属性/方法时。
八、继承(Inheritance)
继承是 OOP 的核心特性之一:子类可以继承父类的属性和方法,还可以添加或修改自己的。
1. 最基本的继承
class Animal: # 父类(基类) def __init__(self, name): self.name = name def eat(self): print(f"{self.name} 在吃东西") def sleep(self): print(f"{self.name} 在睡觉")class Dog(Animal): # 子类(派生类),继承自 Animal def bark(self): print(f"{self.name}:汪汪汪!")class Cat(Animal): # 另一个子类 def meow(self): print(f"{self.name}:喵喵喵!")# 使用dog = Dog("旺财")dog.eat() # 旺财 在吃东西(继承自 Animal)dog.sleep() # 旺财 在睡觉(继承自 Animal)dog.bark() # 旺财:汪汪汪!(Dog 自己的)cat = Cat("咪咪")cat.eat() # 咪咪 在吃东西cat.meow() # 咪咪:喵喵喵!
💡 Dog(Animal) 表示 Dog 继承自 Animal。Dog 自动拥有 Animal 的所有方法。
2. 子类重写父类方法(Override)
子类可以重新定义父类的方法,覆盖父类的实现。
class Animal: def __init__(self, name): self.name = name def speak(self): print(f"{self.name} 发出声音")class Dog(Animal): def speak(self): # 重写父类的 speak 方法 print(f"{self.name}:汪汪汪!")class Cat(Animal): def speak(self): # 重写父类的 speak 方法 print(f"{self.name}:喵喵喵!")# 使用animals = [Dog("旺财"), Cat("咪咪"), Animal("某动物")]for animal in animals: animal.speak()# 输出:# 旺财:汪汪汪!# 咪咪:喵喵喵!# 某动物 发出声音
3. super() —— 调用父类的方法
子类重写方法后,如果想保留父类的部分逻辑,用 super()。
class Animal: def __init__(self, name): print("Animal.__init__ 被调用") self.name = name def eat(self): print(f"{self.name} 在吃东西")class Dog(Animal): def __init__(self, name, breed): # 调用父类的 __init__,让父类先初始化 name super().__init__(name) print("Dog.__init__ 被调用") self.breed = breed # 子类自己再加一个品种属性 def eat(self): # 先执行父类的 eat super().eat() # 再添加子类自己的逻辑 print(f"{self.name} 吃得很开心,还摇尾巴!")dog = Dog("旺财", "金毛")# 输出:# Animal.__init__ 被调用# Dog.__init__ 被调用dog.eat()# 输出:# 旺财 在吃东西# 旺财 吃得很开心,还摇尾巴!
📌 super() 的完整写法是 super(Dog, self),但 Python 3 中可以简写为 super()。
4. 多继承(一个类继承多个父类)
Python 支持多继承,但新手建议先掌握单继承。
class Flyable: def fly(self): print("飞起来了!")class Swimmable: def swim(self): print("在游泳!")class Duck(Flyable, Swimmable): # 鸭子既能飞又能游泳 def __init__(self, name): self.name = namedonald = Duck("唐老鸭")donald.fly() # 飞起来了!donald.swim() # 在游泳!
⚠️ 多继承可能引发"菱形继承"问题(两个父类有同名方法)。Python 使用 MRO(方法解析顺序) 来决定调用哪个,可以通过 类名.__mro__ 查看顺序。
九、封装(Encapsulation)
封装就是把内部实现细节隐藏起来,只暴露必要的接口。
1. 私有属性:以双下划线开头 __name
class BankAccount: def __init__(self, owner, balance): self.owner = owner self.__balance = balance # 私有属性(双下划线开头) def deposit(self, amount): """存钱""" if amount > 0: self.__balance += amount print(f"存入 {amount} 元,当前余额:{self.__balance}") else: print("存款金额必须大于 0") def withdraw(self, amount): """取钱""" if 0 < amount <= self.__balance: self.__balance -= amount print(f"取出 {amount} 元,当前余额:{self.__balance}") else: print("余额不足或金额无效") def get_balance(self): """获取余额(对外提供的接口)""" return self.__balance# 使用account = BankAccount("小明", 1000)# 通过正规接口操作account.deposit(500) # 存入 500 元,当前余额:1500account.withdraw(200) # 取出 200 元,当前余额:1300print(account.get_balance()) # 1300# 试图直接访问私有属性# print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'# ⚠️ 但其实 Python 的私有是"伪私有",可以通过特殊方式访问(不推荐):print(account._BankAccount__balance) # 1300(名称改编 name mangling)
💡 命名改编(Name Mangling):Python 会把 __balance 改成 _BankAccount__balance,目的是防止子类意外覆盖,而不是绝对安全。
2. 私有方法
class Car: def __init__(self): self.__speed = 0 def accelerate(self): self.__speed += 10 self.__check_speed() # 调用私有方法 def __check_speed(self): # 私有方法 """内部检查,不对外暴露""" if self.__speed > 120: print("警告:超速了!") else: print(f"当前速度:{self.__speed} km/h")car = Car()car.accelerate() # 当前速度:10 km/hcar.accelerate() # 当前速度:20 km/h# car.__check_speed() # 报错,无法访问
3. 单下划线 _name(约定俗成的"保护")
class Person: def __init__(self, name): self._name = name # 单下划线:表示"内部使用,不建议外部直接访问"p = Person("小明")print(p._name) # 可以访问,但 IDE 可能会警告你
📌 单下划线只是约定,Python 不会阻止你访问。双下划线 __name 会触发名称改编,有一定保护效果。
十、多态(Polymorphism)
多态的核心理念:不同的对象,对同一消息(方法调用)做出不同的响应。
class Animal: def speak(self): raise NotImplementedError("子类必须实现 speak 方法")class Dog(Animal): def speak(self): return "汪汪汪"class Cat(Animal): def speak(self): return "喵喵喵"class Duck(Animal): def speak(self): return "嘎嘎嘎"# 多态的体现:同一个函数,传入不同对象,表现不同def animal_concert(animal): print(f"{type(animal).__name__} 说:{animal.speak()}")# 传入不同的对象animal_concert(Dog()) # Dog 说:汪汪汪animal_concert(Cat()) # Cat 说:喵喵喵animal_concert(Duck()) # Duck 说:嘎嘎嘎
💡 Python 是动态类型语言,多态非常自然。你不需要像 Java 那样声明接口,只要对象有 speak() 方法,就能传给 animal_concert()。
十一、魔术方法(Magic Methods / Dunder Methods)
以双下划线 __ 开头和结尾的方法,是 Python 内置的特殊方法,在特定场景下自动调用。
1. __str__ 和 __repr__:对象的字符串表示
class Dog: def __init__(self, name, age): self.name = name self.age = age def __str__(self): """给用户看的,友好描述(print 时调用)""" return f"狗狗:{self.name},{self.age}岁" def __repr__(self): """给开发者看的,精确描述(交互式环境直接显示对象时调用)""" return f"Dog(name='{self.name}', age={self.age})"dog = Dog("旺财", 3)print(dog) # 狗狗:旺财,3岁(调用 __str__)print(str(dog)) # 狗狗:旺财,3岁print(repr(dog)) # Dog(name='旺财', age=3)(调用 __repr__)
2. __eq__:判断两个对象是否相等
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): """判断两个 Point 是否相等""" if not isinstance(other, Point): return False return self.x == other.x and self.y == other.yp1 = Point(1, 2)p2 = Point(1, 2)p3 = Point(3, 4)print(p1 == p2) # True(因为 __eq__ 被重写了)print(p1 == p3) # False
📌 如果不写 __eq__,默认用 is 比较(比较内存地址),p1 == p2 会是 False。
3. __len__:让对象支持 len()
class Classroom: def __init__(self): self.students = [] def add_student(self, name): self.students.append(name) def __len__(self): return len(self.students)cls = Classroom()cls.add_student("小明")cls.add_student("小红")print(len(cls)) # 2(调用了 __len__)
4. __getitem__ / __setitem__:让对象像字典/列表一样用 []
class MyDict: def __init__(self): self._data = {} def __setitem__(self, key, value): print(f"设置 {key} = {value}") self._data[key] = value def __getitem__(self, key): print(f"获取 {key}") return self._data.get(key, "不存在") def __delitem__(self, key): print(f"删除 {key}") if key in self._data: del self._data[key]d = MyDict()d["name"] = "小明" # 设置 name = 小明print(d["name"]) # 获取 name → 小明print(d["age"]) # 获取 age → 不存在del d["name"] # 删除 name
5. 常用魔术方法速查
魔术方法 | 触发场景 | 示例 |
__init__
| 创建对象时 | obj = Class()
|
__str__
| print(obj) / str(obj)
| print(dog)
|
__repr__
| 交互式显示 / repr(obj) | >>> dog
|
__eq__
| obj1 == obj2
| p1 == p2
|
__ne__
| obj1 != obj2
| p1 != p2
|
__lt__
| obj1 < obj2
| p1 < p2
|
__len__
| len(obj)
| len(classroom)
|
__getitem__
| obj[key]
| d["name"]
|
__setitem__
| obj[key] = value
| d["name"] = "x"
|
__delitem__
| del obj[key]
| del d["name"]
|
__iter__
| for x in obj
| for s in classroom
|
__contains__
| item in obj
| "x" in obj
|
__call__
| obj()(把对象当函数调用)
| obj()
|
__add__
| obj1 + obj2
| p1 + p2
|
十二、@property 装饰器:把方法变成属性
有时候你想像访问属性一样访问方法的结果,但又希望在获取/设置时做一些校验。
class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): """获取摄氏度(像属性一样访问)""" return self._celsius @celsius.setter def celsius(self, value): """设置摄氏度(可以加入校验)""" if value < -273.15: raise ValueError("温度不能低于绝对零度!") self._celsius = value @property def fahrenheit(self): """华氏度(只读属性,没有 setter)""" return self._celsius * 9/5 + 32# 使用t = Temperature(25)# 像访问属性一样读取print(t.celsius) # 25print(t.fahrenheit) # 77.0# 像设置属性一样设置t.celsius = 30print(t.celsius) # 30# 尝试设置非法值# t.celsius = -300 # ValueError: 温度不能低于绝对零度!# 华氏度是只读的# t.fahrenheit = 100 # AttributeError: can't set attribute
💡 @property 的妙处:外部代码看起来是在访问属性,实际上是在调用方法。你可以随时在方法里添加逻辑,而不需要修改外部调用代码。
十三、抽象基类(ABC):强制子类实现方法
如果你希望父类只定义接口,强制所有子类必须实现某些方法,用 abc 模块。
from abc import ABC, abstractmethodclass Animal(ABC): # 继承 ABC,成为抽象基类 @abstractmethod def speak(self): """抽象方法:子类必须实现""" pass @abstractmethod def move(self): """抽象方法:子类必须实现""" pass def sleep(self): """普通方法:子类可以直接继承使用""" print("睡觉中...")class Dog(Animal): def speak(self): print("汪汪汪") def move(self): print("四条腿跑")class Fish(Animal): def speak(self): print("咕噜咕噜") def move(self): print("在水里游")# 使用dog = Dog()dog.speak() # 汪汪汪dog.sleep() # 睡觉中...# animal = Animal() # 报错!不能实例化抽象基类
📌 如果子类没有实现所有 @abstractmethod 装饰的方法,也不能实例化。
十四、综合实战:设计一个图书管理系统
把上面学的知识串起来:
from abc import ABC, abstractmethodfrom datetime import datetime# ========== 抽象基类 ==========class LibraryItem(ABC): def __init__(self, item_id, title): self.item_id = item_id self.title = title self.is_borrowed = False self.borrower = None self.borrow_date = None @abstractmethod def get_item_type(self): pass def borrow(self, user): if self.is_borrowed: print(f"《{self.title}》已被借出,无法借阅") return False self.is_borrowed = True self.borrower = user self.borrow_date = datetime.now() print(f"{user} 成功借阅《{self.title}》") return True def return_item(self): if not self.is_borrowed: print(f"《{self.title}》未被借出") return False days = (datetime.now() - self.borrow_date).days self.is_borrowed = False self.borrower = None self.borrow_date = None print(f"《{self.title}》已归还,借阅时长:{days} 天") return True def __str__(self): status = "已借出" if self.is_borrowed else "在馆" return f"[{self.get_item_type()}] {self.item_id}: 《{self.title}》({status})"# ========== 具体子类 ==========class Book(LibraryItem): def __init__(self, item_id, title, author, pages): super().__init__(item_id, title) self.author = author self.pages = pages def get_item_type(self): return "图书" def __str__(self): base = super().__str__() return f"{base} | 作者:{self.author} | {self.pages}页"class Magazine(LibraryItem): def __init__(self, item_id, title, issue_number): super().__init__(item_id, title) self.issue_number = issue_number def get_item_type(self): return "杂志" def __str__(self): base = super().__str__() return f"{base} | 第{self.issue_number}期"# ========== 图书馆类 ==========class Library: def __init__(self, name): self.name = name self.__items = {} # 私有:所有馆藏 self.__borrow_history = [] # 私有:借阅历史 def add_item(self, item): if item.item_id in self.__items: print(f"ID {item.item_id} 已存在") return self.__items[item.item_id] = item print(f"入库成功:{item.title}") def find_item(self, item_id): return self.__items.get(item_id) def list_items(self): print(f"\n===== {self.name} 馆藏列表 =====") for item in self.__items.values(): print(item) def borrow(self, item_id, user): item = self.find_item(item_id) if item and item.borrow(user): self.__borrow_history.append({ 'user': user, 'item': item.title, 'action': '借阅', 'time': datetime.now() }) def return_item(self, item_id): item = self.find_item(item_id) if item: item.return_item() @property def total_items(self): return len(self.__items) @property def borrowed_count(self): return sum(1 for item in self.__items.values() if item.is_borrowed)# ========== 运行演示 ==========lib = Library("市立图书馆")# 添加馆藏lib.add_item(Book("B001", "Python编程", "张三", 350))lib.add_item(Book("B002", "数据结构", "李四", 280))lib.add_item(Magazine("M001", "科学世界", 202407))# 查看馆藏lib.list_items()# [图书] B001: 《Python编程》(在馆) | 作者:张三 | 350页# [图书] B002: 《数据结构》(在馆) | 作者:李四 | 280页# [杂志] M001: 《科学世界》(在馆) | 第202407期# 借阅lib.borrow("B001", "小明")lib.borrow("M001", "小红")# 再次查看lib.list_items()# B001 显示"已借出",M001 显示"已借出"# 查看统计print(f"\n总馆藏:{lib.total_items},已借出:{lib.borrowed_count}")# 归还lib.return_item("B001")# 尝试直接操作私有属性(被封装保护)# lib.__items # 报错
十五、面向对象设计原则(新手进阶方向)
- DRY(Don't Repeat Yourself):不要重复代码,用继承或组合复用。
- 组合优于继承:如果"有一个"关系,用组合;如果"是一个"关系,用继承。
- 单一职责
- 开闭原则