Python进阶基础--面向对象编程(OOP)


1. 面向对象编程(OOP)入门

1.1 什么是面向对象

面向对象编程(Object-Oriented Programming,简称OOP)是一种编程思想,核心是将现实世界中的事物抽象为"类",将具体的事物实例化为"对象",通过对象之间的交互来实现功能。

简单理解:

  • :是一个"模板"或"蓝图",描述一类事物的共同特征(属性)和行为(方法)。比如"人"是一个类,特征有"姓名、年龄",行为有"吃饭、睡觉"。
  • 对象:是类的"具体实例",根据类这个模板创建出来的具体事物。比如"张三"是"人"这个类的一个对象,"李四"是另一个对象。

1.2 面向对象 vs 面向过程

|-------|----------------------|----------------------------|
| 对比维度 | 面向过程 | 面向对象 |
| 核心思想 | 以"过程"为核心,关注"怎么做"(步骤) | 以"对象"为核心,关注"谁来做"(对象的属性和行为) |
| 代码复用性 | 较低,代码冗余度高 | 较高,通过继承、封装实现代码复用 |
| 可维护性 | 较低,修改一处可能影响多处 | 较高,代码结构清晰,模块化强 |
| 适用场景 | 简单的、步骤明确的小项目 | 复杂的、需求多变的大项目(如AI大模型、Web开发) |

1.3 面向对象的三大核心特性

  1. 封装:将属性和方法封装在类中,隐藏内部细节,仅对外提供必要的访问接口。
  2. 继承:子类继承父类的属性和方法,实现代码复用,同时可以扩展或重写父类功能。
  3. 多态:不同对象对同一方法的调用可能产生不同的行为,提高代码的灵活性。

2. 类与对象的基础

2.1 类的定义格式

Python中用class关键字定义类,格式如下:

复制代码
class 类名:
    # 类的属性(变量)
    # 类的方法(函数)

命名规范:类名采用大驼峰命名法 (每个单词首字母大写,无下划线),比如PersonStudentAiModel

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 就是李四。

补充关键知识点
  1. self不是 Python 关键字 (只是行业约定俗成的名字,你写成 this/me 也能运行,但绝对不推荐);
  2. 调用方法时,不需要手动传 self,Python 会自动把当前对象传进去;
  3. self 只能用在 类的内部
(b)为什么必须用 self?

看一个反例你就懂了:

如果没有 self,类无法区分不同对象的属性

比如两个人:张三(18岁)、李四(20岁)

没有 self,类不知道你说的年龄是张三的还是李四的;

有了 selfself.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岁
执行过程(重点!)
  1. p1.speak() → Python 自动把 p1 传给 speakself
  2. 此时 self = p1self.name = 张三
  3. p2.speak()self = p2self.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 句)
  1. self = 当前对象本身
  2. 实例方法第一个参数必须是 self
  3. 类内部用 self.属性self.方法() 访问当前对象;
  4. 调用方法时不用传 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:子类调用父类的方法
  1. 如果子类重写了父类的方法,但还想调用父类的原方法,可以用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()
# 输出:
# 旺财在吃东西(父类的逻辑)
# 旺财(品种:中华田园犬)在吃骨头(子类的逻辑)
  1. 或者使用父类名.父类函数名(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()  # 输出:我用翅膀飞
为什么输出的是 Flyablemove

因为继承顺序是 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 多态的实现条件

多态的实现需要满足三个条件:

  1. 继承:子类继承父类
  2. 重写:子类重写父类的方法
  3. 父类引用指向子类对象:用父类的变量(或参数)接收子类的对象

从多态的严格意义上来看,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,抽象基类):

  1. 抽象类继承 ABC
  2. 抽象方法加 @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 条铁律(违反就报错)

  1. 抽象类不能直接创建对象

    animal = Animal() # 报错!抽象类无法实例化

  2. 子类继承抽象类,必须重写所有抽象方法

    错误:子类没有重写 eat() 抽象方法

    class Dog(Animal):
    pass

    dog = Dog() # 报错!子类也是抽象类,不能实例化

  3. 重写抽象方法后,子类才能正常使用

    正确:重写所有抽象方法

    class Dog(Animal):
    # 必须实现父类的抽象方法
    def eat(self):
    print("小狗啃骨头")

    dog = Dog() # ✅ 正常创建对象
    dog.eat() # 输出:小狗啃骨头
    dog.sleep() # 调用抽象类的普通方法

  4. 抽象方法可以有参数,规则和普通方法一致

    @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 抽象类的使用场景(什么时候用?)

  1. 制定统一规范 :比如所有支付方式(微信、支付宝)必须实现 pay() 方法;
  2. 强制子类重写:避免子类遗漏核心方法;
  3. 顶层设计:在大型项目、框架中最常用(比如你刚学的 LangChain、Streamlit 底层大量用抽象类)。

10. 其他常见面向对象知识

10.1 isinstance():判断对象是否是某个类的实例

  • 格式isinstance(对象, 类名)

  • 作用:判断一个对象是否是某个类(或其子类)的实例,返回布尔值

  • 代码示例

    class Animal:
    pass

    class 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:
    pass

    class 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   # 输出:年龄修改失败

相关推荐
开心码农1号2 小时前
RabbitMQ 生产运维命令大全
linux·开发语言·ruby
网安INF2 小时前
数据结构第二章复习:线性表
java·开发语言·数据结构
superior tigre2 小时前
某为25.9.28 Yolo检测器中的anchor聚类(python实现)
python·yolo·聚类
aq55356002 小时前
Laravel10.X核心特性全解析
java·开发语言·spring boot·后端
这个人懒得名字都没写2 小时前
PyCharm图像查看器插件PixelLens
ide·python·pycharm
油墨香^_^2 小时前
Spring Boot集成WebSocket,实现后台向前端推送信息
开发语言
Chasing Aurora2 小时前
整理常用的开发工具使用问题和小贴士(二)——软件和浏览器
redis·python·mysql·maven
我星期八休息2 小时前
Python-基础语法大全
开发语言·python