欢迎光临
我们一直在努力

Python第七课:深入属性、封装与继承

文章目录

  • 引言
  • 类属性 vs 实例属性
    • 1. 什么是类属性?
    • 2. 如何访问类属性?
    • 3. 实例属性 vs 类属性:一张表看懂
    • 4. 内存示意图
    • 5. 类属性的典型使用场景
    • 6 最容易踩的坑:通过对象修改类属性
    • 7. 总结
  • 封装
    • 1. 什么是封装?
    • 2. 为什么需要封装?
    • 3. Python 的封装哲学
    • 4. 更 Pythonic 的方式:@property 装饰器
    • 5. 只读属性
    • 6. 综合案例:带封装的账户类
    • 7. 总结
  • 类继承
    • 1. 什么是继承?
    • 2. 基本语法
    • 3. 子类可以做什么?
    • 4. super() 的作用
    • 5. 多重继承(简介)
    • 6. 继承 vs 组合
    • 7. 总结

引言

如果把类比作一座房子的设计图,那么:

  • 实例属性是每套房子里主人自己添置的家具
  • 类属性是整栋楼共享的水电管道
  • 封装是把电线和煤气管道藏进墙里,不让人乱碰
  • 继承是在原有设计图基础上加一层楼,复用下面楼层的结构

面向对象的这几个概念,其实就藏在我们身边的生活里 本文会用代码把这些比喻落地,带你从“理解”走向“会用”


在开始之前先检查一下你的装备吧!!!

python环境不会装的看这里:从安装到Hello World:Python环境搭建完整指南

python编辑器不会装的看这里:零基础Python入门:手把手教你安装Python、新版PyCharm和VS Code


类属性 vs 实例属性

在学习此篇前,建议先看上篇:Python第六课:从零理解面向对象

在前面的文章中,我们已经学会了如何在 __init__ 方法里用 self.属性 = 值 给对象添加属性。 这种每个对象各自拥有的属性,叫作实例属性。

但有时候,我们希望某些数据是整个类共享的,比如:

  • 所有狗都属于“哺乳动物”
  • 记录一共创建了多少个学生对象
  • 所有员工的公司名称相同

这时候就需要用到类属性。


1. 什么是类属性?

类属性是定义在类内部、方法外部的变量,它属于类本身,所有通过这个类创建的对象都共享这一份数据。

class Dog:
# 类属性
species = '哺乳动物'

def __init__(self, name):
self.name = name # 实例属性

在这个例子中,species 就是类属性,name 是实例属性。

拿更简单的例子解释: 想象一下,类就像是一个做饼干的模具,实例就是用这个模具做出来的一块块饼干。

  • 类属性,就是刻在模具本身上的一个标记,比如“保质期:12个月”。 不管用这个模具做出多少块饼干,每一块饼干上都会有这个“保质期”信息,因为它们来自同一个模具。
  • 实例属性,则是每一块饼干独有的特征,比如“这块是巧克力味的,那块是草莓味的”。 每块饼干可以有自己的口味,互不影响。

2. 如何访问类属性?

可以通过类名访问,也可以通过对象访问:

print(Dog.species) # 哺乳动物(通过类名)

dog1 = Dog('旺财')
dog2 = Dog('来福')

print(dog1.species) # 哺乳动物(通过对象)
print(dog2.species) # 哺乳动物(通过对象)

所有对象访问到的都是同一个 species。


3. 实例属性 vs 类属性:一张表看懂

对比维度实例属性类属性
定义位置 通常在 __init__ 中,用 self.属性 定义 直接在类内部,方法外部定义
归属 属于具体的对象 属于类本身
内存存储 每个对象独立保存一份 类对象(类的命名空间)中只保存一份,所有对象共享
访问方式 必须通过对象访问 可通过对象或类名访问
修改影响 只影响当前对象 通过类名修改会影响所有对象;通过对象修改则会创建同名的实例属性,不影响类属性
典型用途 描述对象特有的数据(姓名、年龄) 描述类别共有特征(物种、常量)、计数器、默认配置

4. 内存示意图

类对象 Dog 的内存空间(只有一份):
+——————-+
| species = '哺乳动物' | ← 类属性
+——————-+
| __init__ 方法 |
| … |
+——————-+

对象 dog1 的内存空间:
+——————-+
| name = '旺财' | ← 实例属性
+——————-+

对象 dog2 的内存空间:
+——————-+
| name = '来福' | ← 实例属性
+——————-+

当通过 dog1.species 访问时,Python 先在 dog1 的实例空间找 species,找不到就去类空间找,所以能拿到 '哺乳动物'。


5. 类属性的典型使用场景

场景一:定义常量

class Circle:
PI = 3.14159 # 类属性,所有圆共用

def __init__(self, radius):
self.radius = radius

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

场景二:计数器(记录创建了多少个实例)

class Student:
count = 0 # 类属性,记录学生总数

def __init__(self, name):
self.name = name
Student.count += 1 # 每创建一个学生,总数加1

s1 = Student('小明')
s2 = Student('小红')
s3 = Student('小刚')

print(Student.count) # 3

场景三:所有对象共享的默认配置

class Config:
debug = False # 所有地方共享同一个调试开关


6 最容易踩的坑:通过对象修改类属性

初学者常犯的错误是:想修改类属性,却通过对象去赋值,结果并没有修改类属性,反而给这个对象创建了一个同名的实例属性,掩盖了类属性。

class Dog:
species = '哺乳动物'

dog1 = Dog()
dog2 = Dog()

dog1.species = '鸟类' # ❌ 你以为在修改类属性,其实在创建实例属性
print(dog1.species) # 鸟类(现在 dog1 有自己的实例属性)
print(dog2.species) # 哺乳动物(类属性没变)
print(Dog.species) # 哺乳动物(类属性确实没变)

正确方式:如果要修改类属性,必须通过类名:

Dog.species = '爬行动物'
print(dog1.species) # 鸟类(dog1 的实例属性还在,所以优先用自己的)
print(dog2.species) # 爬行动物(dog2 没有实例属性,所以用类的)

记住:通过对象只能读取类属性,不能通过对象修改类属性。 赋值时,如果类属性存在,Python 不会修改它,而是给对象创建一个新的实例属性。


7. 总结

类属性和实例属性各有分工:

  • 实例属性:描述对象的个体特征,每个对象独立。
  • 类属性:描述类整体的特征或共享数据,所有对象共用。

简单来说:

  • 实例属性 = 每块饼干独有的标签,各自独立。
  • 类属性 = 刻在模具上的信息,所有饼干共享。

掌握它们的区别,能帮你设计出更清晰、更省内存的类。

现在你已经知道如何让数据“共享”或“私有”,但有些数据我们不仅希望共享,还希望保护起来,不让外部随意修改——这就是下一个主题:封装


封装

通过上一部分的学习,你已经能灵活运用类属性和实例属性来组织数据了。 但写代码时还会遇到一个问题:对象的某些属性,并不希望外部随意修改。 比如一个人的年龄,如果直接被外部改成负数,显然不符合逻辑;一个银行账户的余额,更不能让外部直接赋值。

这就需要引入面向对象的第二大核心概念:封装(Encapsulation)。


1. 什么是封装?

封装有两层含义:

  • 把数据(属性)和操作数据的方法(行为)打包在一起——这一点我们在定义类时已经做到了。
  • 隐藏对象的内部状态,仅通过定义好的接口与外界交互,防止数据被意外修改。
  • 简单说:给属性加上“访问权限控制”,不让外部随便改。


    2. 为什么需要封装?

    看一个没有封装的例子:

    class Student:
    def __init__(self, name, age):
    self.name = name
    self.age = age

    s = Student('小明', 20)
    print(s.age) # 20

    # 外部直接修改
    s.age = 5 # 年龄变成负数,不合理,但程序不报错

    年龄为负数显然不符合业务逻辑,但 Python 并不会阻止这种操作。 为了保障数据的合理性,我们需要把 age “藏起来”,只允许通过特定的方法去修改,并在修改时进行校验。


    3. Python 的封装哲学

    Python 不像 Java 那样有严格的 private 关键字。 Python 的设计哲学是:信任程序员,通过约定而非强制来实现封装。

    Python 提供了两种“提示”机制:单下划线 和 双下划线。


    单下划线 _name:受保护的属性(约定)

    在属性名前加一个下划线,例如 _age,表示“这是一个受保护的属性,外部不要直接访问,但如果你非要访问,我也不拦着”。

    class Student:
    def __init__(self, name, age):
    self.name = name
    self._age = age # 受保护的属性

    def get_age(self):
    return self._age

    def set_age(self, age):
    if age <= 0:
    print('年龄必须大于0')
    else:
    self._age = age

    s = Student('小明', 20)
    print(s._age) # 20(虽然能访问,但 IDE 可能会警告)
    s._age = 5 # 仍然能改,不报错
    s.set_age(5) # 通过方法修改,会触发校验

    单下划线的作用:

    • 告诉其他开发者:“这个属性是内部使用的,请别动。”
    • 在 IDE 或工具中可能会给出警告。
    • 它不会阻止实际访问,只是一种约定。

    双下划线 __name:名称修饰 如果你想让属性更难被意外覆盖,可以用双下划线开头,例如 __age。 Python 会把这个属性名“修饰”成 _类名__age,从而在一定程度上避免直接访问。

    class Student:
    def __init__(self, name, age):
    self.name = name
    self.__age = age # 私有属性

    def get_age(self):
    return self.__age

    def set_age(self, age):
    if age <= 0:
    print('年龄必须大于0')
    else:
    self.__age = age

    s = Student('小明', 20)
    # print(s.__age) # ❌ AttributeError: 'Student' object has no attribute '__age'
    print(s._Student__age) # 20(通过修饰后的名字依然能访问,但不推荐)

    双下划线的实际效果:

    • 属性名被改成 _类名__属性名,防止子类意外覆盖。
    • 外部直接通过原属性名访问会报错,但通过修饰后的名字仍能访问——不是真正的私有,只是防君子不防小人。
    • 主要用于避免继承时的属性冲突。

    4. 更 Pythonic 的方式:@property 装饰器

    上面的 get_age() 和 set_age() 虽然实现了封装,但调用起来不够优雅: s.get_age() 和 s.set_age(18) 看起来不如 s.age 和 s.age = 18 直观。

    Python 提供了 @property 装饰器,让我们可以像访问属性一样调用方法,同时保留封装的好处。

    class Student:
    def __init__(self, name, age):
    self.name = name
    self._age = age # 用单下划线表示“受保护”

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

    @age.setter
    def age(self, value):
    """setter:设置年龄,带校验"""
    if not isinstance(value, int):
    raise ValueError('年龄必须是整数')
    if value <= 0 or value > 120:
    raise ValueError('年龄必须在1-120之间')
    self._age = value

    s = Student('小明', 20)
    print(s.age) # 20,像访问属性一样调用 getter
    s.age = 25 # 像赋值一样调用 setter
    # s.age = -5 # ❌ ValueError: 年龄必须在1-120之间

    原理:

    • @property 把一个方法变成属性调用,s.age 会执行被 @property 装饰的方法。
    • @age.setter 是配套的装饰器,当执行 s.age = … 时自动调用。

    这样既保留了简单的访问语法,又在背后实现了校验逻辑,完美体现了封装的思想。


    5. 只读属性

    如果你只想让外部读取属性,不允许修改,可以只定义 @property,不定义 @xxx.setter:

    class Config:
    def __init__(self):
    self._version = '1.0.0'

    @property
    def version(self):
    return self._version

    c = Config()
    print(c.version) # 1.0.0
    # c.version = '2.0.0' # ❌ AttributeError: can't set attribute

    这在定义配置项、常量等场景非常实用。


    6. 综合案例:带封装的账户类

    class BankAccount:
    def __init__(self, owner, balance):
    self.owner = owner
    self.__balance = balance # 私有属性

    @property
    def balance(self):
    """余额只读,不允许直接修改"""
    return self.__balance

    def deposit(self, amount):
    """存款"""
    if amount <= 0:
    print('存款金额必须大于0')
    return
    self.__balance += amount
    print(f'存款成功,当前余额:{self.__balance}')

    def withdraw(self, amount):
    """取款"""
    if amount <= 0:
    print('取款金额必须大于0')
    return
    if amount > self.__balance:
    print('余额不足')
    return
    self.__balance -= amount
    print(f'取款成功,当前余额:{self.__balance}')

    acc = BankAccount('张三', 1000)
    print(acc.balance) # 1000
    # acc.balance = 2000 # ❌ 不能直接赋值
    acc.deposit(500) # 存款成功,当前余额:1500
    acc.withdraw(2000) # 余额不足

    这个例子中:

    • __balance 被隐藏,外部无法直接修改。
    • 通过 deposit 和 withdraw 方法操作余额,保证了业务逻辑的正确性。
    • balance 属性只读,方便查询余额。

    7. 总结

    方式含义是否强制推荐用途
    name 公有属性 可以直接暴露、无需保护的数据
    _name 受保护属性(约定) 否(仅提示) 内部使用,子类可访问,但外部不应直接修改
    __name 私有属性(名称修饰) 一定程度避免意外覆盖 防止继承时属性名冲突,但不绝对安全
    @property 属性化访问 + 控制逻辑 是(通过代码实现) 需要校验或计算后返回的属性,或只读属性

    记住:Python 的封装靠的是程序员的自觉和良好的设计,而不是语言的强约束。使用 _ 和 @property 已经足够写出健壮的代码。


    类继承

    在前两部分中,我们学习了如何定义类的属性(实例属性与类属性)以及如何通过封装保护数据。这些都是针对单个类的设计。

    但现实中,类与类之间往往存在关联。比如“学生”和“老师”都是“人”,他们共享姓名、年龄等属性,也都有介绍自己的行为。如果分别为它们编写独立的类,就会产生大量重复代码。

    面向对象提供的解决方案是:继承(Inheritance)。


    1. 什么是继承?

    继承 允许我们基于一个已有的类创建一个新类。已有的类称为父类(Parent Class)或基类(Base Class),新创建的类称为子类(Child Class)或派生类(Derived Class)。

    子类会自动获得父类的所有属性和方法,同时还可以添加自己的新属性和新方法,或者重写(覆盖)父类的方法。

    用一句话概括:继承是为了复用代码,并建立类之间的层次关系。


    2. 基本语法

    定义子类时,在类名后面的括号里写上父类名:

    class Parent:
    pass

    class Child(Parent): # Child 继承自 Parent
    pass

    来看一个具体的例子:

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

    def introduce(self):
    print(f'我叫{self.name},今年{self.age}岁')

    class Student(Person): # Student 继承 Person
    pass

    s = Student('小明', 18)
    s.introduce() # 我叫小明,今年18岁(子类直接使用了父类的方法)

    即使 Student 类里什么也没写,它也已经有了 name、age 属性和 introduce 方法——这些都是从 Person 继承来的。


    3. 子类可以做什么?

    添加新的方法

    子类可以在继承父类的基础上,增加自己特有的方法:

    class Student(Person):
    def study(self, course):
    print(f'{self.name} 正在学习 {course}')

    s = Student('小明', 18)
    s.introduce() # 我叫小明,今年18岁
    s.study('数学') # 小明 正在学习 数学

    添加新的属性

    如果子类需要额外的属性,可以在自己的 __init__ 中添加。但要注意:父类的 __init__ 不会自动执行,所以需要在子类的 __init__ 中手动调用父类的 __init__ 来初始化父类定义的属性。

    class Student(Person):
    def __init__(self, name, age, student_id):
    # 调用父类的 __init__ 初始化 name 和 age
    super().__init__(name, age)
    # 然后添加自己的属性
    self.student_id = student_id

    s = Student('小明', 18, '2024001')
    print(s.name, s.age, s.student_id) # 小明 18 2024001

    这里用到了 super(),它的作用是获取父类的引用,从而调用父类的方法。后面会详细说明。

    重写父类方法

    如果父类的方法不满足子类的需求,子类可以重新定义该方法,这就叫方法重写(Override)。

    class Student(Person):
    def __init__(self, name, age, student_id):
    super().__init__(name, age)
    self.student_id = student_id

    # 重写 introduce 方法
    def introduce(self):
    print(f'我叫{self.name},今年{self.age}岁,学号是{self.student_id}')

    s = Student('小明', 18, '2024001')
    s.introduce() # 我叫小明,今年18岁,学号是2024001

    重写后,调用子类对象的 introduce 会执行子类的版本,父类的版本被覆盖了。

    如果想在子类方法中继续使用父类方法的逻辑,可以用 super() 调用父类版本:

    class Student(Person):
    def __init__(self, name, age, student_id):
    super().__init__(name, age)
    self.student_id = student_id

    def introduce(self):
    # 先调用父类的方法,打印基本信息
    super().introduce()
    # 再补充自己的信息
    print(f'我的学号是{self.student_id}')

    s = Student('小明', 18, '2024001')
    s.introduce()
    # 输出:
    # 我叫小明,今年18岁
    # 我的学号是2024001

    4. super() 的作用

    super() 是一个特殊函数,返回一个代表父类的对象,让你能够调用父类的方法。

    最常用的场景就是在子类的 __init__ 中调用父类的 __init__,确保父类定义的属性被正确初始化。同样,在其他方法中如果需要复用父类的逻辑,也可以通过 super() 调用。

    为什么不用直接写 Person.__init__(self, name, age)? 在单继承下两者效果一样,但 super() 更简洁,而且在多重继承中能正确处理方法解析顺序(MRO),避免手动指定类名带来的问题。因此,始终推荐使用 super()。

    5. 多重继承(简介)

    Python 支持一个子类继承多个父类,称为多重继承。

    语法很简单:

    class A:
    def method(self):
    print('A method')

    class B:
    def method(self):
    print('B method')

    class C(A, B): # C 同时继承 A 和 B
    pass

    c = C()
    c.method() # 输出什么?

    输出是 A method。因为 Python 会按照**方法解析顺序(MRO,Method Resolution Order)**从左到右查找方法。可以使用 类名.__mro__ 查看顺序:

    print(C.__mro__)
    # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

    多重继承很有用,但也容易带来复杂性,比如两个父类有同名方法时容易混淆,或者“钻石继承”问题。对于初学者,建议优先使用单继承,除非你非常清楚自己在做什么。

    如果需要复用多个类的功能,有时组合是比多重继承更清晰的选择(后面会简单对比)。


    6. 继承 vs 组合

    继承描述的是 “是一个”(is-a) 关系:

    • 学生 是 一个人
    • 轿车 是 一种交通工具

    组合描述的是 “有一个”(has-a) 关系:

    • 汽车 有 一个引擎
    • 班级 有 多个学生

    组合的实现很简单:在一个类中使用另一个类的对象作为属性。

    class Engine:
    def start(self):
    print('引擎启动')

    class Car:
    def __init__(self):
    self.engine = Engine() # 组合

    def start(self):
    self.engine.start()
    print('汽车启动')

    什么时候用继承,什么时候用组合?

    • 如果两个类确实是 is-a 关系,并且子类需要复用父类的大部分实现,用继承。

    • 如果只是想复用某个类的功能,但两者没有本质的 is-a 关系,用组合更灵活,耦合度更低。

    业界有句原则:“优先使用组合而不是继承”(Favor composition over inheritance)。因为组合更灵活,不容易破坏封装。初学者可以先掌握继承,但也要了解组合的存在。


    7. 总结

    通过继承,我们能够以更少的代码构建类之间的层次关系,实现有效的复用。但同时也要注意:

    • 不要滥用继承,只有真正的 is-a 关系才适合。
    • 多重继承虽然强大,但会增加复杂性,使用需谨慎。
    • 学会用组合作为继承的补充。

    至此,我们已经完整地学习了 Python 面向对象的三大核心:属性分类(类属性 vs 实例属性)、封装和继承。这三块拼图组合起来,你就能设计出结构清晰、易于扩展的 Python 程序了。

    赞(0)
    未经允许不得转载:171主机测评 » Python第七课:深入属性、封装与继承
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址