1. 面向对象编程(OOP)入门
1.1 什么是面向对象
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程思想,核心是将现实世界中的事物抽象为"类",将具体的事物实例化为"对象",通过对象之间的交互来实现功能。
简单理解:
- 类:是一个"模板"或"蓝图",描述一类事物的共同特征(属性)和行为(方法)。比如"人"是一个类,特征有"姓名、年龄",行为有"吃饭、睡觉"。
- 对象:是类的"具体实例",根据类这个模板创建出来的具体事物。比如"张三"是"人"这个类的一个对象,"李四"是另一个对象。
1.2 面向对象 vs 面向过程
|-------|----------------------|----------------------------|
| 对比维度 | 面向过程 | 面向对象 |
| 核心思想 | 以"过程"为核心,关注"怎么做"(步骤) | 以"对象"为核心,关注"谁来做"(对象的属性和行为) |
| 代码复用性 | 较低,代码冗余度高 | 较高,通过继承、封装实现代码复用 |
| 可维护性 | 较低,修改一处可能影响多处 | 较高,代码结构清晰,模块化强 |
| 适用场景 | 简单的、步骤明确的小项目 | 复杂的、需求多变的大项目(如AI大模型、Web开发) |
1.3 面向对象的三大核心特性
- 封装:将属性和方法封装在类中,隐藏内部细节,仅对外提供必要的访问接口。
- 继承:子类继承父类的属性和方法,实现代码复用,同时可以扩展或重写父类功能。
- 多态:不同对象对同一方法的调用可能产生不同的行为,提高代码的灵活性。
2. 类与对象的基础
2.1 类的定义格式
Python中用class关键字定义类,格式如下:
class 类名:
# 类的属性(变量)
# 类的方法(函数)
命名规范:类名采用大驼峰命名法 (每个单词首字母大写,无下划线),比如Person、Student、AiModel。
2.2 最简单的类与对象示例
2.3 构造方法__init__:初始化对象属性
上面的例子中,所有对象的属性都是固定的(都是"张三"、20岁),但现实中每个对象的属性应该不同。__init__是Python的构造方法 (魔法方法的一种),用于在创建对象时初始化对象的属性,让每个对象有不同的属性值。
核心说明
__init__方法在创建对象时自动调用,无需手动调用- 第一个参数必须是
self,代表当前对象本身,用于访问对象的属性和方法 - 可以通过
__init__的参数传递属性值
带__init__的完整示例
class Person:
# 构造方法:创建对象时自动调用,用于初始化属性
def __init__(self, name, age):
# self.name:对象的属性(实例属性)
# name:传入的参数
self.name = name # 将传入的name赋值给对象的name属性
self.age = age # 将传入的age赋值给对象的age属性
# 实例方法:必须带self参数,代表当前对象
def introduce(self):
# 通过self访问对象的属性
print(f"大家好,我叫{self.name},今年{self.age}岁")
def eat(self, food):
print(f"{self.name}在吃{food}")
# 创建对象:必须传入__init__要求的参数(self不需要手动传,Python自动传)
# 格式:对象名 = 类名(参数1, 参数2, ...)
person1 = Person("张三", 20) # 创建person1,传入name="张三",age=20
person2 = Person("李四", 22) # 创建person2,传入name="李四",age=22
# 访问对象的属性
print("person1的姓名:", person1.name) # 输出:张三
print("person2的年龄:", person2.age) # 输出:22
# 调用对象的方法
person1.introduce() # 输出:大家好,我叫张三,今年20岁
person2.eat("苹果") # 输出:李四在吃苹果
2.4 类内访问 vs 类外访问
-
类内访问 :在类的方法内部,通过
self.属性名或self.方法名()访问当前对象的属性和方法 -
类外访问 :在类的外部,通过
对象名.属性名或对象名.方法名()访问对象的属性和方法class Person:
def init(self, name):
self.name = name # 类内:通过self定义属性def say_hello(self): # 类内:通过self访问自己的属性 print(f"你好,我是{self.name}") # 类内:通过self调用自己的方法 self.say_bye() def say_bye(self): print(f"{self.name}说再见")类外:创建对象
person = Person("王五")
类外:通过对象名访问属性
print(person.name) # 输出:王五
类外:通过对象名调用方法
person.say_hello()
输出:
你好,我是王五
王五说再见
2.5 self解释
self 是 类中实例方法的第一个参数 ,它的作用是:代表当前创建的对象本身。
我用最通俗、最接地气 的方式,给你把 Python 面向对象里的 self 讲透,包含是什么、为什么用、怎么用、常见坑,新手也能秒懂!
(a)先搞懂:self 到底是什么?
核心一句话
self 是 类中实例方法的第一个参数 ,它的作用是:代表当前创建的对象本身。
- self = 当前这个人自己
定义类的时候,不知道未来会创建谁,所以用self占位;
当你创建 张三 时,self就是张三;
当你创建 李四 时,self就是李四。
补充关键知识点
self不是 Python 关键字 (只是行业约定俗成的名字,你写成this/me也能运行,但绝对不推荐);- 调用方法时,不需要手动传
self,Python 会自动把当前对象传进去; self只能用在 类的内部。
(b)为什么必须用 self?
看一个反例你就懂了:
如果没有 self,类无法区分不同对象的属性。
比如两个人:张三(18岁)、李四(20岁)
没有 self,类不知道你说的年龄是张三的还是李四的;
有了 self,self.age 就代表当前对象的年龄。
(c)self 怎么用?(最全用法 + 代码示例)
我们用一个 Person 类,演示 self 的 3 种核心用法:
1. 定义实例方法 → 必须写 self 作为第一个参数
类里的普通方法(实例方法),第一个参数固定是 self。
class Person:
# 构造方法:创建对象时自动执行,初始化属性
def __init__(self, name, age):
# 2. 用 self 给 当前对象 绑定属性
self.name = name # self.name = 对象的名字
self.age = age # self.age = 对象的年龄
# 普通实例方法:必须写 self
def speak(self):
# 3. 用 self 访问 当前对象 的属性/方法
print(f"我叫{self.name},今年{self.age}岁")
# 创建对象1:张三
p1 = Person("张三", 18)
# 创建对象2:李四
p2 = Person("李四", 20)
# 调用方法 → 不用传 self!Python 自动传
p1.speak() # 我叫张三,今年18岁
p2.speak() # 我叫李四,今年20岁
执行过程(重点!)
p1.speak()→ Python 自动把p1传给speak的self- 此时
self=p1,self.name= 张三 p2.speak()→self=p2,self.name= 李四
2. 用 self 访问对象的属性
在类内部,想要用当前对象的变量 ,必须写 self.属性名。
❌ 错误写法(直接写变量名,找不到)
def speak(self):
print(name) # 报错!不知道 name 是谁
✅ 正确写法
def speak(self):
print(self.name) # self 指明:当前对象的 name
3. 用 self 调用对象的其他方法
类内部,一个方法调用另一个方法,用 self.方法名()。
class Person:
def __init__(self, name):
self.name = name
def run(self):
print(f"{self.name}在跑步")
def do(self):
# 调用当前对象的 run() 方法
self.run()
print("运动结束")
p = Person("小明")
p.do()
# 输出:
# 小明在跑步
# 运动结束
(d)新手必看:3 个高频坑
坑1:定义方法时漏写 self → 直接报错
def speak(): # 漏写 self
print("你好")
p1 = Person()
p1.speak() # 报错:speak() takes 0 positional arguments but 1 was given
✅ 解决:实例方法必须写 self。
坑2:调用方法时手动传 self → 多余
p1.speak(self) # 报错!
✅ 解决:self 由 Python 自动传递,调用时不用写。
坑3:把 self 当成全局变量
self 只属于当前对象 ,不同对象的 self 互不干扰。
(e)极简总结(背会这 4 句)
- self = 当前对象本身;
- 实例方法第一个参数必须是 self;
- 类内部用
self.属性、self.方法()访问当前对象; - 调用方法时不用传 self,Python 自动处理。
(f)拓展:self 只用于「实例方法」
Python 类里有 3 种方法,只有实例方法 用 self:
|------|-------|-------------|
| 方法类型 | 第一个参数 | 用途 |
| 实例方法 | self | 操作当前对象(最常用) |
| 类方法 | cls | 操作类本身 |
| 静态方法 | 无参数 | 普通工具函数 |
3. 类属性与对象属性
Python中类的属性分为两种:类属性 和对象属性(实例属性),新手必须分清两者的区别。
3.1 定义与区别
|------|--------------------------|------------------------------|
| 对比维度 | 类属性 | 对象属性(实例属性) |
| 定义位置 | 直接在类中定义,不在方法内 | 在__init__方法中通过self.属性名定义 |
| 所属 | 属于类本身,所有对象共享 | 属于具体对象,每个对象有独立的副本 |
| 内存占用 | 所有对象共用一份,节省内存 | 每个对象一份,内存占用较高 |
| 适用场景 | 所有对象的共同特征(比如"人类的物种都是智人") | 每个对象的独特特征(比如"每个人的姓名、年龄不同") |
3.2 代码示例
class Person:
# 类属性:直接在类中定义,所有对象共享
species = "智人" # 人类的物种都是智人,所有对象都一样
def __init__(self, name, age):
# 对象属性:在__init__中通过self定义,每个对象独立
self.name = name
self.age = age
# 创建两个对象
person1 = Person("张三", 20)
person2 = Person("李四", 22)
# 1. 访问类属性
# 方式1:通过类名访问(推荐)
print("通过类名访问类属性:", Person.species) # 输出:智人
# 方式2:通过对象名访问(不推荐,容易混淆)
print("person1访问类属性:", person1.species) # 输出:智人
print("person2访问类属性:", person2.species) # 输出:智人
# 2. 修改类属性(只能通过类名修改,通过对象名修改会变成对象属性)
# 正确:通过类名修改,所有对象的类属性都会变
Person.species = "现代智人"
print("修改后通过类名访问:", Person.species) # 输出:现代智人
print("person1访问类属性:", person1.species) # 输出:现代智人
print("person2访问类属性:", person2.species) # 输出:现代智人
# 错误:通过对象名修改,不会修改类属性,而是给person1新增一个同名的对象属性
person1.species = "原始智人"
print("person1的对象属性:", person1.species) # 输出:原始智人(这是person1的对象属性)
print("person2访问类属性:", person2.species) # 输出:现代智人(类属性没变)
print("通过类名访问类属性:", Person.species) # 输出:现代智人(类属性没变)
3.3 核心注意事项
- 永远通过类名访问和修改类属性,不要通过对象名修改类属性,否则会创建同名的对象属性,覆盖类属性的访问
- 对象属性优先于类属性:如果对象有同名的对象属性,访问时会优先访问对象属性,而不是类属性
4. 封装:私有属性与私有方法
4.1 封装的概念
封装是面向对象的三大特性之一,核心思想是:将对象的属性和方法封装在类内部,隐藏内部实现细节,仅对外提供必要的访问接口,从而保证数据的安全性和代码的可维护性。
简单理解:就像一台电视机,内部的电路板、零件是隐藏的(私有),你只能通过遥控器(公有接口)来操作电视机,不能直接修改内部零件,这样既安全又方便。
4.2 私有属性与私有方法的定义
Python中用双下划线 __开头 的属性或方法,称为私有属性 或私有方法,外部无法直接访问,只能在类内部访问。
代码示例
class Person:
def __init__(self, name, age):
self.name = name # 公有属性:外部可以直接访问
self.__age = age # 私有属性:双下划线__开头,外部无法直接访问
def introduce(self):
# 类内部可以访问私有属性
print(f"大家好,我叫{self.name},今年{self.__age}岁")
# 类内部可以调用私有方法
self.__secret_method()
# 私有方法:双下划线__开头,外部无法直接调用
def __secret_method(self):
print("这是私有方法,只有类内部能调用")
# 创建对象
person = Person("张三", 20)
# 1. 访问公有属性:可以直接访问
print(person.name) # 输出:张三
# 2. 访问私有属性:外部直接访问会报错
# print(person.__age) # 报错:AttributeError: 'Person' object has no attribute '__age'
# 3. 调用公有方法:可以直接调用
person.introduce()
# 输出:
# 大家好,我叫张三,今年20岁
# 这是私有方法,只有类内部能调用
# 4. 调用私有方法:外部直接调用会报错
# person.__secret_method() # 报错:AttributeError: 'Person' object has no attribute '__secret_method'
4.3 如何访问私有成员:通过公有方法
虽然私有属性和方法外部无法直接访问,但我们可以提供公有方法作为"接口",让外部通过这些公有方法来间接访问或修改私有属性,这样可以在方法中添加验证逻辑,保证数据的安全性。
代码示例:通过公有方法访问和修改私有属性
class Person:
def __init__(self, name, age):
self.name = name
self.__age = age # 私有属性:年龄
# 公有方法:获取私有属性__age的值(getter方法)
def get_age(self):
return self.__age
# 公有方法:修改私有属性__age的值(setter方法)
def set_age(self, new_age):
# 在修改前添加验证逻辑,保证数据安全
if 0 < new_age < 120:
self.__age = new_age
print("年龄修改成功")
else:
print("年龄修改失败:年龄必须在0-120之间")
# 创建对象
person = Person("张三", 20)
# 1. 通过getter方法获取私有属性
print("初始年龄:", person.get_age()) # 输出:20
# 2. 通过setter方法修改私有属性(合法值)
person.set_age(25) # 输出:年龄修改成功
print("修改后的年龄:", person.get_age()) # 输出:25
# 3. 通过setter方法修改私有属性(非法值,验证失败)
person.set_age(150) # 输出:年龄修改失败:年龄必须在0-120之间
print("年龄未改变:", person.get_age()) # 输出:25
4.4 单下划线_的含义(约定俗成的私有)
Python中还有一种用单下划线 _开头 的属性或方法,称为"保护属性/方法",它是约定俗成的私有,技术上外部可以直接访问,但大家约定:"不要直接访问单下划线开头的属性/方法,它是内部使用的"。
class Person:
def __init__(self, name):
self.name = name
self._age = 20 # 单下划线开头:约定俗成的私有,外部尽量不要访问
def _internal_method(self):
print("这是内部方法,外部尽量不要调用")
person = Person("张三")
# 技术上可以访问,但不推荐
print(person._age) # 输出:20
person._internal_method() # 输出:这是内部方法,外部尽量不要调用
5. 继承:代码复用的核心
5.1 继承的概念
继承是面向对象的三大特性之一,核心是:子类(派生类)继承父类(基类)的属性和方法,实现代码复用,同时子类可以扩展新的属性和方法,或者重写父类的方法。
简单理解:就像儿子继承父亲的财产(属性和方法),同时儿子可以有自己的新财产(扩展),也可以对父亲的财产进行改造(重写)。
- 父类(基类):被继承的类
- 子类(派生类):继承父类的类
5.2 单继承:一个子类继承一个父类
单继承是最基础的继承方式,格式如下:
class 子类名(父类名):
# 子类的属性和方法
示例1:子类继承父类的属性和方法
# 1. 定义父类(基类):Animal
class Animal:
def __init__(self, name):
self.name = name # 父类的属性:名字
def eat(self):
print(f"{self.name}在吃东西") # 父类的方法:吃东西
def sleep(self):
print(f"{self.name}在睡觉") # 父类的方法:睡觉
# 2. 定义子类(派生类):Dog,继承Animal
class Dog(Animal):
# 子类可以扩展新的方法
def bark(self):
print(f"{self.name}在汪汪叫")
# 3. 创建子类对象
dog = Dog("旺财")
# 4. 子类可以直接使用父类的属性和方法
print(dog.name) # 访问父类的属性:输出 旺财
dog.eat() # 调用父类的方法:输出 旺财在吃东西
dog.sleep() # 调用父类的方法:输出 旺财在睡觉
# 5. 子类可以使用自己扩展的方法
dog.bark() # 调用子类自己的方法:输出 旺财在汪汪叫
示例2:子类重写父类的方法
子类可以对父类的方法进行重写(Override),即子类定义一个和父类同名的方法,覆盖父类的方法,实现自己的逻辑。
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name}在吃东西")
def make_sound(self):
print("动物发出声音") # 父类的方法
# 子类Dog继承Animal
class Dog(Animal):
# 重写父类的make_sound方法
def make_sound(self):
print(f"{self.name}在汪汪叫") # 子类自己的逻辑
# 子类Cat继承Animal
class Cat(Animal):
# 重写父类的make_sound方法
def make_sound(self):
print(f"{self.name}在喵喵叫") # 子类自己的逻辑
# 创建对象
dog = Dog("旺财")
cat = Cat("咪咪")
# 调用重写后的方法
dog.make_sound() # 输出:旺财在汪汪叫(调用的是子类的方法,不是父类的)
cat.make_sound() # 输出:咪咪在喵喵叫(调用的是子类的方法,不是父类的)
示例3:子类调用父类的方法
- 如果子类重写了父类的方法,但还想调用父类的原方法,可以用
super()函数。
多继承中super()函数调用最近的
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name}在吃东西")
# 子类Dog继承Animal
class Dog(Animal):
def __init__(self, name, breed):
# 方式1:用super()调用父类的__init__方法(推荐)
super().__init__(name) # 调用父类的构造方法,初始化name属性
# 子类扩展新的属性
self.breed = breed # 品种
def eat(self):
# 先调用父类的eat方法
super().eat()
# 再添加子类自己的逻辑
print(f"{self.name}(品种:{self.breed})在吃骨头")
# 创建对象
dog = Dog("旺财", "中华田园犬")
# 调用重写后的eat方法
dog.eat()
# 输出:
# 旺财在吃东西(父类的逻辑)
# 旺财(品种:中华田园犬)在吃骨头(子类的逻辑)
-
或者使用
父类名.父类函数名(self)的方法精准访问class Master:
# 1.1 属性
def init(self):
self.kongfu = '[古法煎饼果子配方]'# 1.2 行为 def make_cake(self): print(f'运用{self.kongfu}制作煎饼果子')2. 黑马学校类
class School:
# 2.1 属性
def init(self):
self.kongfu = '[菜狗AI煎饼果子配方]'# 2.2 行为 def make_cake(self): print(f'运用{self.kongfu}制作煎饼果子')3. 徒弟类
class Prentice(School, Master):
# 3.1 属性
def init(self):
self.kongfu = '[新式独创煎饼果子配方]'# 3.2 行为 def make_cake(self): print(f'运用{self.kongfu}制作煎饼果子') # 3.3 调用父类的功能. def make_master_cake(self): Master.__init__(self) Master.make_cake(self) def make_school_cake(self): School.__init__(self) School.make_cake(self)4. 测试.
if name == 'main':
# 4.1 创建徒弟类对象.
p = Prentice()
# 4.2 访问属性.
print(p.kongfu) # 独创
# 4.3 调用函数.
p.make_cake() # 独创
p.make_master_cake() # 古法
p.make_school_cake() # AI
print('-' * 34)
p.make_cake() # AI
5.3 多层继承:子类→父类→祖父类
多层继承是指一个子类继承一个父类,而这个父类又继承另一个祖父类,形成"继承链"。
# 1. 祖父类:Animal
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name}在吃东西")
# 2. 父类:Mammal(哺乳动物),继承Animal
class Mammal(Animal):
def give_birth(self):
print(f"{self.name}是胎生的")
# 3. 子类:Dog,继承Mammal
class Dog(Mammal):
def bark(self):
print(f"{self.name}在汪汪叫")
# 创建Dog对象
dog = Dog("旺财")
# 可以调用祖父类的方法
dog.eat() # 输出:旺财在吃东西(来自祖父类Animal)
# 可以调用父类的方法
dog.give_birth() # 输出:旺财是胎生的(来自父类Mammal)
# 可以调用自己的方法
dog.bark() # 输出:旺财在汪汪叫(来自自己)
5.4 多继承:一个子类继承多个父类
Python支持多继承,即一个子类可以继承多个父类,格式如下:
class 子类名(父类1, 父类2, ...):
# 子类的属性和方法
代码示例
# 父类1:Flyable(会飞的)
class Flyable:
def fly(self):
print("我会飞")
# 父类2:Swimmable(会游泳的)
class Swimmable:
def swim(self):
print("我会游泳")
# 子类:Duck(鸭子),继承Flyable和Swimmable
class Duck(Flyable, Swimmable):
def quack(self):
print("嘎嘎嘎")
# 创建Duck对象
duck = Duck()
# 可以调用父类1的方法
duck.fly() # 输出:我会飞
# 可以调用父类2的方法
duck.swim() # 输出:我会游泳
# 可以调用自己的方法
duck.quack() # 输出:嘎嘎嘎
多继承中同名方法调用原则:按照继承顺序
当多个父类存在同名方法时,Python 会按照「子类定义时的继承顺序(从左到右)」去查找方法,先找到的就优先使用------ 谁在继承列表里更靠左,谁就「更近」,方法就优先用谁的。
# 父类1:Flyable(会飞的)
class Flyable:
def fly(self):
print("我会飞")
# 新增:和Swimmable同名的方法move
def move(self):
print("我用翅膀飞")
# 父类2:Swimmable(会游泳的)
class Swimmable:
def swim(self):
print("我会游泳")
# 新增:和Flyable同名的方法move
def move(self):
print("我用脚划水游")
# 子类:Duck(鸭子),继承顺序:先Flyable,后Swimmable
class Duck(Flyable, Swimmable):
def quack(self):
print("嘎嘎嘎")
# 创建对象
duck = Duck()
# ✅ 测试同名方法:调用move()
duck.move() # 输出:我用翅膀飞
为什么输出的是 Flyable 的 move?
因为继承顺序是 class Duck(Flyable, Swimmable):
- Python 查找
move时,先看子类 Duck 自己有没有 → 没有 - 再看左边第一个父类 Flyable → 找到了
move(),就直接用它,不再去右边的Swimmable里找了 - 这就是「就近原则」:左边的父类更近,方法优先生效
多继承的注意事项:方法解析顺序(MRO)
如果多个父类中有同名的方法 ,子类调用时会按照MRO (Method Resolution Order,方法解析顺序)来决定调用哪个父类的方法。可以用类名.__mro__或类名.mro()查看MRO顺序。
class A:
def say_hello(self):
print("Hello from A")
class B(A):
def say_hello(self):
print("Hello from B")
class C(A):
def say_hello(self):
print("Hello from C")
# 子类D继承B和C
class D(B, C):
pass
# 查看MRO顺序
print(D.__mro__)
# 输出:(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
# 创建D对象,调用say_hello
d = D()
d.say_hello() # 输出:Hello from B(按照MRO顺序,先找B,B有就调用B的)
6. 多态:不同对象的同一行为
6.1 多态的概念
多态是面向对象的三大特性之一,核心是:不同的对象,调用同一个方法,可能产生不同的行为。
简单理解:比如"动物发出声音"这个方法,狗调用是"汪汪叫",猫调用是"喵喵叫",鸟调用是"叽叽叫",同一个方法,不同对象调用,结果不同,这就是多态。
6.2 多态的实现条件
多态的实现需要满足三个条件:
- 继承:子类继承父类
- 重写:子类重写父类的方法
- 父类引用指向子类对象:用父类的变量(或参数)接收子类的对象
从多态的严格意义上来看,Python即使不满足这些条件也可实现,所以Python的多态也叫伪多态,究其原因是因为它是一门弱类型语言。
6.3 代码示例
# 1. 父类:Animal
class Animal:
def __init__(self, name):
self.name = name
# 父类的方法:子类会重写
def make_sound(self):
print("动物发出声音")
# 2. 子类Dog继承Animal,重写make_sound
class Dog(Animal):
def make_sound(self):
print(f"{self.name}在汪汪叫")
# 3. 子类Cat继承Animal,重写make_sound
class Cat(Animal):
def make_sound(self):
print(f"{self.name}在喵喵叫")
# 4. 子类Bird继承Animal,重写make_sound
class Bird(Animal):
def make_sound(self):
print(f"{self.name}在叽叽叫")
# 5. 定义一个函数,接收Animal类型的参数(父类引用)
def let_animal_sound(animal):
# 调用make_sound方法:不同的animal对象,调用结果不同
animal.make_sound()
# 创建不同的子类对象
dog = Dog("旺财")
cat = Cat("咪咪")
bird = Bird("小鸟")
# 调用函数,传入不同的子类对象(父类引用指向子类对象)
let_animal_sound(dog) # 输出:旺财在汪汪叫
let_animal_sound(cat) # 输出:咪咪在喵喵叫
let_animal_sound(bird) # 输出:小鸟在叽叽叫
7. 常用魔法方法
魔法方法(Magic Methods)是Python中以双下划线 __开头和结尾 的特殊方法,也叫"双下划线方法"(Dunder Methods)。它们不需要手动调用,会在特定的时机自动调用 ,比如__init__在创建对象时自动调用。
7.1 __init__:构造方法
- 调用时机:创建对象时自动调用(分为有参数和无参数,只能选其一,没有重载)
- 作用:初始化对象的属性
- 示例:见第2.3节
7.2 __str__:对象的字符串表示(print()时调用)
- 调用时机 :用
print()打印对象时自动调用 - 作用:返回对象的"友好"字符串表示,方便用户阅读
- 要求:必须返回一个字符串
代码示例
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# __str__方法:print()时自动调用
def __str__(self):
# 返回一个友好的字符串
return f"Person[name={self.name}, age={self.age}]"
# 创建对象
person = Person("张三", 20)
# 打印对象:自动调用__str__方法
print(person) # 输出:Person[name=张三, age=20]
# 如果没有__str__方法,会输出类似:<__main__.Person object at 0x...>
插值表达式说明:
插值表达式 = 把「变量 / 代码 / 计算结果」直接塞进字符串里的语法
- 字符串里的
{ }就是插值占位符 - 大括号里面写的内容,就是插值表达式
- 运行时,Python 会自动把
{ }替换成里面表达式的结果
简单说:给字符串插动态的值,不用拼接,不用复杂格式。
Python 3.6+ 最常用的插值语法:f-string 写法:f"字符串{表达式}字符串"
- 开头加
f - 大括号
{}里写任意合法的 Python 代码(变量、属性、运算、函数)
7.3 __repr__:对象的官方字符串表示(repr()时调用)
- 调用时机 :用
repr()函数获取对象的官方表示时自动调用,或者在交互式解释器中直接输入对象名时调用 - 作用:返回对象的"官方"字符串表示,通常用于开发者调试,应该能通过这个字符串重新创建对象
- 要求:必须返回一个字符串
代码示例
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# __str__:用户友好的表示
def __str__(self):
return f"Person[name={self.name}, age={self.age}]"
# __repr__:官方表示,用于调试
def __repr__(self):
# 通常返回能重新创建对象的字符串
return f"Person('{self.name}', {self.age})"
# 创建对象
person = Person("张三", 20)
# print()调用__str__
print(person) # 输出:Person[name=张三, age=20]
# repr()调用__repr__
print(repr(person)) # 输出:Person('张三', 20)
# 如果没有__str__,print()会调用__repr__
7.4 __del__:析构方法(对象销毁时调用)
- 调用时机:对象被销毁(垃圾回收)时自动调用
- 作用:清理资源(比如关闭文件、释放内存等)
- 注意 :Python有自动垃圾回收机制,
__del__通常不需要手动定义
代码示例
class Person:
def __init__(self, name):
self.name = name
print(f"{self.name}被创建了")
# 析构方法:对象销毁时自动调用
def __del__(self):
print(f"{self.name}被销毁了")
# 创建对象
person = Person("张三")
# 输出:张三被创建了
# 手动删除对象(或者程序结束时自动销毁)
del person
# 输出:张三被销毁了
7.5 __len__:定义len()的行为
- 调用时机 :用
len()函数获取对象长度时自动调用 - 作用 :让自定义类的对象支持
len()函数 - 要求:必须返回一个整数
代码示例
class MyList:
def __init__(self, data):
self.data = data # 用列表存储数据
# __len__方法:len()时自动调用
def __len__(self):
# 返回内部列表的长度
return len(self.data)
# 创建对象
my_list = MyList([1, 2, 3, 4, 5])
# 调用len():自动调用__len__
print(len(my_list)) # 输出:5
7.6 __eq__:定义==的行为
- 调用时机 :用
==比较两个对象是否相等时自动调用 - 作用:自定义对象的相等比较逻辑
- 参数 :
self和另一个对象other - 要求:必须返回一个布尔值(True或False)
代码示例
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# __eq__方法:==比较时自动调用
def __eq__(self, other):
# 先判断other是否是Person类型
if isinstance(other, Person):
# 如果姓名和年龄都相等,就认为两个对象相等
return self.name == other.name and self.age == other.age
# 不是Person类型,直接返回False
return False
# 创建两个对象
person1 = Person("张三", 20)
person2 = Person("张三", 20)
person3 = Person("李四", 22)
# 用==比较
print(person1 == person2) # 输出:True(姓名和年龄都相等)
print(person1 == person3) # 输出:False(姓名或年龄不相等)
print(person1 == "张三") # 输出:False(不是Person类型)
8. 实例方法、类方法与静态方法
Python中类的方法分为三种:实例方法 、类方法 和静态方法,新手必须分清三者的区别。
8.1 实例方法(最常用)
- 定义 :没有装饰器,第一个参数必须是
self(代表当前对象) - 访问权限 :可以访问对象属性(
self.属性名)和类属性(类名.属性名) - 调用方式 :通过对象调用(
对象名.方法名()) - 适用场景:需要访问或修改对象属性的方法
代码示例
class Person:
# 类属性
species = "智人"
def __init__(self, name, age):
# 对象属性
self.name = name
self.age = age
# 实例方法:第一个参数是self
def introduce(self):
# 访问对象属性
print(f"我叫{self.name},今年{self.age}岁")
# 访问类属性
print(f"我是{Person.species}")
# 创建对象
person = Person("张三", 20)
# 通过对象调用实例方法
person.introduce()
8.2 类方法
- 定义 :用
@classmethod装饰器装饰,第一个参数必须是cls(代表当前类)(换了字母也不报错但是强烈建议使用cls) - 访问权限 :可以访问类属性(
cls.属性名),不能访问对象属性(因为没有self) - 调用方式 :通过类调用(
类名.方法名()),也可以通过对象调用(不推荐) - 适用场景:只需要访问或修改类属性的方法
代码示例
class Person:
# 类属性
species = "智人"
def __init__(self, name):
self.name = name # 对象属性
# 类方法:@classmethod装饰,第一个参数是cls
@classmethod
def get_species(cls):
# 访问类属性
return cls.species
# 类方法:修改类属性
@classmethod
def set_species(cls, new_species):
cls.species = new_species
# 通过类调用类方法(推荐)
print(Person.get_species()) # 输出:智人
Person.set_species("现代智人")
print(Person.get_species()) # 输出:现代智人
# 也可以通过对象调用(不推荐)
person = Person("张三")
print(person.get_species()) # 输出:现代智人
8.3 静态方法
- 定义 :用
@staticmethod装饰器装饰,没有特殊参数(没有self或cls) - 访问权限:不能访问对象属性,也不能访问类属性(除非通过类名.属性名)
- 调用方式 :通过类调用(
类名.方法名()),也可以通过对象调用(不推荐) - 适用场景:既不需要访问对象属性,也不需要访问类属性,只是一个放在类里的"工具函数"
代码示例
class Calculator:
# 静态方法:@staticmethod装饰,无特殊参数
@staticmethod
def add(a, b):
# 不需要访问类属性或对象属性,只是一个工具函数
return a + b
@staticmethod
def multiply(a, b):
return a * b
# 通过类调用静态方法(推荐)
print(Calculator.add(1, 2)) # 输出:3
print(Calculator.multiply(3, 4)) # 输出:12
# 也可以通过对象调用(不推荐)
calc = Calculator()
print(calc.add(5, 6)) # 输出:11
8.4 三者的区别总结
|--------|------------|----------------|-----------------|
| 对比维度 | 实例方法 | 类方法 | 静态方法 |
| 装饰器 | 无 | @classmethod | @staticmethod |
| 第一个参数 | self(对象) | cls(类) | 无 |
| 访问对象属性 | 可以 | 不可以 | 不可以 |
| 访问类属性 | 可以(通过类名) | 可以(通过cls) | 可以(通过类名) |
| 调用方式 | 对象调用 | 类调用(推荐) | 类调用(推荐) |
| 适用场景 | 访问/修改对象属性 | 访问/修改类属性 | 工具函数,无需访问类/对象属性 |
9.抽象类和抽象方法
9.1 先搞懂:为什么需要抽象类?
我们先从场景 出发,不用记概念:
假设你要写「动物」相关的类,所有动物(狗、猫、猪)都必须会 eat() 吃饭,但每种动物吃饭的方式不一样。
- 你不能直接定义一个通用的
Animal.eat()(因为不知道怎么吃); - 你又想强制所有子类必须实现
eat()方法,不实现就报错。
这时候就需要 抽象类 来定规矩!
9.2 核心定义
1. 抽象类(Abstract Class)
- 是一个**不能创建对象(不能实例化)**的特殊类;
- 只能当父类 被继承,专门用来制定规范/约束;
- 必须包含至少一个抽象方法。
2. 抽象方法(Abstract Method)
- 是抽象类里的只有声明、没有具体实现的方法;
- 作用:强制子类必须重写这个方法,不重写就无法创建对象;
- 语法上必须加装饰器
@abstractmethod。
3. Python 实现依赖
Python 没有原生的抽象关键字,必须用内置模块 abc(Abstract Base Class,抽象基类):
- 抽象类继承
ABC - 抽象方法加
@abstractmethod
9.3 关键:抽象方法中的 pass 到底是什么?
1. 语法规则
Python 规定:方法必须有代码体(缩进块),不能只写方法名就结束。
2. pass 的作用(在抽象方法中)
pass 是 空语句、占位符 ,没有任何功能,唯一作用:
满足Python语法要求,告诉解释器:「这个方法没有代码,别报错」。
3. 核心区别
- 普通方法 + pass:有实现(空实现),子类可以不重写;
- 抽象方法 + pass :没有实现 ,仅做声明,子类必须重写。
9.4 完整语法(一步到位)
# 1. 导入抽象类必需的工具
from abc import ABC, abstractmethod
# 2. 定义抽象类:必须继承 ABC
class Animal(ABC):
# 3. 定义抽象方法:加装饰器 + 方法体只有 pass
@abstractmethod
def eat(self):
# 这里必须写代码,所以用 pass 占位(无实际功能)
pass
# 普通方法:抽象类里可以写普通方法(有实现)
def sleep(self):
print("动物都要睡觉")
9.5 抽象类的 4 条铁律(违反就报错)
-
抽象类不能直接创建对象
animal = Animal() # 报错!抽象类无法实例化
-
子类继承抽象类,必须重写所有抽象方法
错误:子类没有重写 eat() 抽象方法
class Dog(Animal):
passdog = Dog() # 报错!子类也是抽象类,不能实例化
-
重写抽象方法后,子类才能正常使用
正确:重写所有抽象方法
class Dog(Animal):
# 必须实现父类的抽象方法
def eat(self):
print("小狗啃骨头")dog = Dog() # ✅ 正常创建对象
dog.eat() # 输出:小狗啃骨头
dog.sleep() # 调用抽象类的普通方法 -
抽象方法可以有参数,规则和普通方法一致
@abstractmethod
def eat(self, food):
pass
9.6 完整可运行代码(对照学习)
from abc import ABC, abstractmethod
# 🔴 抽象类:制定规则,不能创建对象
class Animal(ABC):
# 🔴 抽象方法:强制子类实现,只有pass占位
@abstractmethod
def eat(self):
pass
# 🟢 普通方法:有实现,子类可直接用
def sleep(self):
print("动物睡觉~")
# ------------------------------
# 子类1:必须重写抽象方法
class Dog(Animal):
def eat(self):
print("小狗吃骨头")
# 子类2:必须重写抽象方法
class Cat(Animal):
def eat(self):
print("小猫吃鱼")
# ------------------------------
# 测试
dog = Dog()
dog.eat() # 小狗吃骨头
dog.sleep() # 动物睡觉~
cat = Cat()
cat.eat() # 小猫吃鱼
9.7 进阶:抽象方法里可以不写 pass 吗?
可以,但完全没必要,且不推荐:
@abstractmethod
def eat(self):
print("抽象方法的实现") # 写了代码也没用
⚠️ 即使抽象方法写了实现,子类依然必须重写 ,否则报错。
所以抽象方法永远只写 pass,这是行业标准写法。
9.8 抽象类的使用场景(什么时候用?)
- 制定统一规范 :比如所有支付方式(微信、支付宝)必须实现
pay()方法; - 强制子类重写:避免子类遗漏核心方法;
- 顶层设计:在大型项目、框架中最常用(比如你刚学的 LangChain、Streamlit 底层大量用抽象类)。
10. 其他常见面向对象知识
10.1 isinstance():判断对象是否是某个类的实例
-
格式 :
isinstance(对象, 类名) -
作用:判断一个对象是否是某个类(或其子类)的实例,返回布尔值
-
代码示例:
class Animal:
passclass Dog(Animal):
pass创建对象
animal = Animal()
dog = Dog()判断
print(isinstance(dog, Dog)) # 输出:True(dog是Dog的实例)
print(isinstance(dog, Animal)) # 输出:True(dog是Animal子类的实例)
print(isinstance(animal, Dog)) # 输出:False(animal不是Dog的实例)
print(isinstance(dog, (Dog, int)))# 输出:True(只要是其中一个类的实例就返回True)
10.2 issubclass():判断一个类是否是另一个类的子类
-
格式 :
issubclass(子类名, 父类名) -
作用:判断一个类是否是另一个类的子类,返回布尔值
-
代码示例:
class Animal:
passclass Dog(Animal):
pass判断
print(issubclass(Dog, Animal)) # 输出:True(Dog是Animal的子类)
print(issubclass(Animal, Dog)) # 输出:False(Animal不是Dog的子类)
print(issubclass(Dog, Dog)) # 输出:True(一个类是它自己的子类)
10.3 组合:"有一个"的关系
继承是"是一个"的关系(比如"狗是动物"),而组合是"有一个"的关系(比如"人有一个手机")。组合是指在一个类中,将另一个类的对象作为属性,实现代码复用。
代码示例
# 手机类
class Phone:
def __init__(self, brand):
self.brand = brand
def call(self):
print(f"用{self.brand}手机打电话")
# 人类
class Person:
def __init__(self, name, phone):
self.name = name
# 组合:将Phone类的对象作为Person的属性
self.phone = phone # Person"有一个"Phone
def use_phone_call(self):
# 调用phone对象的方法
print(f"{self.name}在使用手机:")
self.phone.call()
# 创建Phone对象
phone = Phone("华为")
# 创建Person对象,传入Phone对象
person = Person("张三", phone)
# 调用方法
person.use_phone_call()
# 输出:
# 张三在使用手机:
# 用华为手机打电话
10.4 @property:属性装饰器
@property可以将一个方法 转为属性来访问,让代码更简洁,同时可以在方法中添加验证逻辑。
代码示例
class Person:
def __init__(self, name, age):
self.name = name
self.__age = age # 私有属性
# @property:将方法转为属性访问(getter)
@property
def age(self):
return self.__age
# @age.setter:对应属性的setter方法(必须和@property的方法名相同)
@age.setter
def age(self, new_age):
if 0 < new_age < 120:
self.__age = new_age
print("年龄修改成功")
else:
print("年龄修改失败")
# 创建对象
person = Person("张三", 20)
# 访问属性:像访问属性一样访问方法
print(person.age) # 输出:20(不需要加(),像访问属性一样)
# 修改属性:像修改属性一样调用setter
person.age = 25 # 输出:年龄修改成功
print(person.age) # 输出:25
# 非法值
person.age = 150 # 输出:年龄修改失败