python学习笔记——类和对象

具体知识

init 方法

也叫初始化方法,对应 C++ 中的构造函数

特点:创建对象时自动执行,无需手动调用

作用:给对象的属性设置初始值,完成对象的 “出生配置”

self 关键字

代表当前对象实例本身

所有类的方法中,第一个参数都必须是self

用self.属性名来定义和访问对象的成员变量

# 创建一个账户对象,持有人是"李明云"
my_account = Account("李明云")

# 存款1000元
my_account.deposit(1000)
print(my_account.balance)  # 输出:1000

# 取款300元
my_account.withdraw(300)
print(my_account.balance)  # 输出:700

# 查看账户持有人
print(my_account.holder)    # 输出:李明云

bound method &类调用成员函数

具体通过对象使用该类的方法时,第一个参数默认传入该实例,在书写的时候不用写出

1. 为什么类内方法的参数中必须有 self?

class Account:
    def deposit(self, amount):
        self.balance += amount

a = Account("Huang")
a.deposit(100)  # 调用时,你只传了 100
print(a.deposit)

2. 什么是 bound method?

你调用 a.deposit(100) 时,只写了 1 个参数 100,但方法定义里是 def deposit(self, amount):,明明有 2 个参数。
这是因为 Python 解释器会自动把调用方法的对象(也就是 a)作为第一个参数传给 self,这样方法里才能知道要修改哪个对象的 balance。

3.通过类调用成员函数

  1. 语法规则
class Account:
    def deposit(self, amount):
        self.balance += amount

b = Account("Li")
Account.deposit(b, 100)  # 直接通过类调用

这种写法里,Account.deposit 是未绑定的函数(unbound function),它还没有和任何对象关联。
所以你必须手动传入 self 参数,也就是你要操作的对象 b,再加上其他参数 100。
它的效果和 b.deposit(100) 完全一样,只是写法不同。

  1. 打印结果的含义
print(Account.deposit)
# 输出:<function Account.deposit at 0x7fe3e306c0e0>
类型 调用方式 示例 输出形式 特点
function(未绑定) 通过类直接调用 Account.deposit(b, 100) <function Account.deposit at ...> 必须手动传入 self
bound method(已绑定) 通过对象调用 b.deposit(100) <bound method Account.deposit of <...>> 自动传入 self,只需传其他参数

隐私&公开:类的命名约定、隐私封装实现、访问控制本质

命名约定

命名格式 含义与用途 例子
xxx(无下划线) 公开成员,任何人都可以访问,是对外接口 deposit()
_xxx(单下划线开头) 非公开成员,约定上不建议外部直接访问,但仍可被类和子类访问 _balance
__xxx(双下划线开头) 名称修饰(name mangling),Python 会把它改成 _类名__xxx,防止被子类意外覆盖,外部直接访问会报错 __balance
xxx(前后双下划线) 系统内置成员,Python 专用,用户不要自定义 initdict

封装实现

二、封装的实现:用 __xxx 保护数据

  1. 代码示例解析
class Account:
    def __init__(self, account_holder):
        self.__balance = 0   # 双下划线开头的私有属性
        self.__holder = account_holder

    def deposit(self, amount):
        self.__balance += amount  # 类内部可以正常访问

    def print(self):
        print(self.__holder, self.__balance)
  1. 外部直接访问会报错
a = Account("Huang")
print(a.__balance)
# 报错:AttributeError: 'Account' object has no attribute '__balance'
  • 报错的原因是:Python 对 __xxx 做了名称修饰,它实际变成了 _Account__balance,所以你用 __balance 找不到它。
    这就是封装的体现:外部不能直接修改数据,只能通过公开的接口(如 deposit())来操作,保证数据安全。

non-public vs private:Python 访问控制的本质

  1. 关键区别
  • C++/Java 的 private:是强制访问控制,编译器会阻止外部访问,是真正的 “ 私有”。
  • Python 的 __xxx:只是命名约定 + 名称修饰,不是真正的强制私有。
    你依然可以通过 a._Account__balance 强行访问到它:
print(a._Account__balance)  # 输出 0
  1. Python 官方文档明确说

“Python 中不存在那种只能从对象内部访问的‘私有’实例变量。”

类成员

一、核心概念:什么是类属性和实例属性?

  1. 定义与位置
    类属性:写在 class 里、但不在 init 方法里的变量,属于类本身,所有实例共享同一份数据。
    例:Account.interest = 0.2
    实例属性:写在 init 里、以 self.xxx 定义的变量,每个实例都有自己独立的一份数据。
    例:self.balance = 0、self.holder = account_holder
  2. 关键区别(对比表)
特性 类属性 interest 实例属性 balance/holder
归属 类本身 每个实例对象
共享性 所有实例共享 每个实例独立
修改方式 Account.interest = 0.4(修改类属性) a.balance += 100(修改实例属性)
访问方式 Account.interest / a.interest a.balance

3.例子

class Account:
    interest = 0.2  # 类属性
    def __init__(self, account_holder):
        self.balance = 0  # 实例属性
        self.holder = account_holder  # 实例属性

a = Account("Huang")
b = Account("Li")

# 1. 初始状态:实例访问类属性
print(a.interest)      # 0.2(找类属性)
print(b.interest)      # 0.2(找类属性)
print(Account.interest)# 0.2(类本身的属性)

# 2. 给实例a绑定同名属性(关键!)
a.interest = 0.3
print(a.interest)      # 0.3(现在找实例自己的interest)
print(b.interest)      # 0.2(还是找类属性,没受影响)
print(Account.interest)# 0.2(类属性本身没变)

# 3. 修改类属性
Account.interest = 0.4
print(a.interest)      # 0.3(实例自己的属性,不受类修改影响)
print(b.interest)      # 0.4(找不到实例属性,用修改后的类属性)
print(Account.interest)# 0.4(类属性被修改)

4.坑

如果类属性是可变对象(比如列表),修改它会影响所有实例:

class Account:
    data = []  # 类属性,可变对象
a = Account()
b = Account()
a.data.append(100)
print(b.data)  # 输出 [100],因为所有实例共享同一份列表

这种情况通常不建议用类属性存可变数据,除非你就是想让所有实例共享它。

成员访问(dot expression)的完整规则——expr.name 的执行

Python 中 expr.name 的执行逻辑,是理解实例属性、类属性的关键:

  1. 基本语法
    expr.name 的执行分为两步:
先计算 expr,得到一个对象(可以是类对象或实例对象)
再访问该对象的 name 成员
  1. 赋值场景(expr.name = value)
  • 如果 expr 是实例对象:直接给这个实例绑定一个新的实例属性,不会影响类属性。
  • 如果 expr 是类对象:直接修改类属性,会影响所有没有同名实例属性的对象。

这就是之前例子中 a.interest = 0.3 的本质:给实例 a 新增了一个同名实例属性,而不是修改类属性。

  1. 取值场景(x = expr.name)

查找顺序是先实例,后类

  • 先在实例自己的属性列表里找 name,找到就返回。
  • 找不到,再去实例所属的类的属性列表里找,找到就返回。
  • 类里也找不到,就会抛出 AttributeError。

@staticmethod修饰器: 不需要 self 的类工具函数

你的 printCount()、getCount() 都不需要 self,所以必须加:

class CountedAccount:
    __count = 0

    def __init__(self, name):
        CountedAccount.__count +=1

    def __del__(self):
        CountedAccount.__count -=1

    @staticmethod
    def printCount():
        print(CountedAccount.__count)

    @staticmethod
    def getCount():
        return CountedAccount.__count

@staticmethod = 不需要 self 的类工具函数
加了它,类和对象都能调用
不加它,对象调用会报错

继承 (Inheritance)

继承允许一个类(派生类/子类)获得另一个类(基类/父类)的所有属性和方法。

1. 为什么需要继承?

为了代码复用。比如在 PPT 的案例中,Account 是一个基础账户,而 CheckingAccount(支票账户)在基础账户之上增加了“手续费”功能。我们不需要重写存款逻辑,直接继承即可。

2. 基本语法

class Account:
    def deposit(self, amount):
        self.balance += amount

class CheckingAccount(Account):  # 括号内填入父类名
    def withdraw_charge(self, amount):
        self.balance -= (amount + 1) # 调用父类的balance属性

3. 关键点:方法重写 (Overriding)

子类可以重新定义父类的方法,以满足特殊需求:

如果子类里定义了一个与父类同名的方法,调用时优先使用子类的方法。
如何调用父类方法? 使用 super() 函数。这是推荐做法,它会根据 MRO(方法解析顺序)自动找到父类,无需硬编码父类名称。

4. 初始化函数的继承

如果子类不写 init,它会直接使用父类的。
如果子类重写了 init,通常必须在子类的 init 中显式调用 super().init(...)(因为子类应该负责所有属性的初始化,包括从基类中继承的部分)

Account.__init__(self, acc_holder)
或者
super().__init__(acc_holder)

成员访问(dot expression)的完整查找规则

赋值场景(expr.name = value)

如果 expr 是实例对象:直接给实例新增一个同名属性,不会影响类属性。
如果 expr 是类对象:直接修改类属性,会影响所有未定义同名实例属性的对象。

取值场景(x = expr.name)

按以下顺序查找,找到即停止:
实例自己的属性列表(self.dict
实例所属类的属性列表(type(self).dict
基类(父类、祖先类)的属性列表(沿继承链向上查找)

多态 (Polymorphism)

多态的含义是:“多种形态”。在 Python 中,这意味着同一个函数名或接口,可以作用于不同类型的对象,并表现出不同的行为。

消息的多态

在 PPT 的例子中,你可以对 Account 和 CheckingAccount 对象都执行 .withdraw() 方法。

对于外部调用者(函数)来说,它不需要知道对象的具体类型,只需要知道该对象支持 withdraw 这个“消息”。
这种“不管对象具体是什么,只要它能处理这个请求就行”的特性,正是多态的基础。

类型的多态

# 1. issubclass(a, b):判断 a 是否是 b 的子类
print(issubclass(Account, CheckingAccount))       # False(父类不是子类的子类)
print(issubclass(CheckingAccount, Account))       # True(子类是父类的子类)

# 2. isinstance(obj, cls):判断 obj 是否是 cls 或其子类的实例
bob = Account("Bob")
jack = CheckingAccount("Jack")
print(isinstance(bob, Account))          # True
print(isinstance(bob, CheckingAccount))  # False(父类实例不是子类实例)
print(isinstance(jack, Account))         # True(子类实例也是父类实例)
print(isinstance(jack, CheckingAccount)) # True

Python 中的“鸭子类型” (Duck Typing)

这是 Python 多态的精髓:“如果它走起来像鸭子,叫起来也像鸭子,那么它就是鸭子。”

区别于 Java/C++:在静态语言中,多态通常通过接口(Interface)强制约束;而在 Python 中,不看类的继承关系,只看对象是否拥有对应的方法。
代码示例:

    def make_it_fly(animal):
        animal.fly()  # 不在乎animal是鸭子还是飞机,只要有fly方法就行
只要传入的对象有 fly() 方法,代码就能运行。这大大增加了程序的灵活性。

多态成立的三个条件

有继承(子类继承父类)
子类重写父类方法
用父类引用指向子类对象,调用方法自动走子类实现

  1. 父类 + 两个子类
class Account:
    def __init__(self, name):
        self.holder = name
        self.balance = 0

    # 父类通用方法
    def show_info(self):
        print(f"账户持有人:{self.holder},余额:{self.balance}")


# 子类1:学生账户
class StudentAccount(Account):
    def __init__(self, name, school):
        super().__init__(name)
        self.school = school

    # 重写父类方法
    def show_info(self):
        print(f"学生账户|持有人:{self.holder}|学校:{self.school}")


# 子类2:企业账户
class CompanyAccount(Account):
    def __init__(self, name, company):
        super().__init__(name)
        self.company = company

    # 重写父类方法
    def show_info(self):
        print(f"企业账户|持有人:{self.holder}|公司:{self.company}")



# 父类类型变量,装不同子类对象
def print_account_info(acc: Account):
    # 同一个方法名,自动调用子类自己的实现
    acc.show_info()


# 传入不同子类实例
s = StudentAccount("张三", "南京大学")
c = CompanyAccount("阿里", "阿里巴巴")

print_account_info(s)   # 走 StudentAccount 的 show_info
print_account_info(c)   # 走 CompanyAccount 的 show_info
运行结果:
学生账户|持有人:张三|学校:南京大学
企业账户|持有人:阿里|公司:阿里巴巴

Python 多态特点(和 Java 不一样)

Python 是动态鸭子类型,甚至不需要显式继承也能实现多态:
只要类里有同名方法,就能统一调用,照样多态。

class Dog:
    def speak(self):
        print("汪汪")

class Cat:
    def speak(self):
        print("喵喵")

def animal_speak(a):
    a.speak()  # 不管是什么类,只要有 speak 就可以

animal_speak(Dog())
animal_speak(Cat())

两种核心的代码复用方式:继承(is-a)和组合(has-a)

b3492d99d883e1e9954514b6fbefdeaa

MRO规则

当存在多个同名属性时,按照 申明顺序(NewAccount, CheckingAccount, SavingsAccount, Account, object)在类中寻找属性名的绑定(MRO)

a5518b1c6245f6d7c0ab03bb3fd80aae

多继承可能引起的问题

  • 属性冲突(不同基类之间存在同名属性)

  • 如何确定调用关系?
    • 按照派生类-基类的顺序依次查询绑定关系
    • 申明多个基类时,按照基类的申明顺序依次进行查找
    • 多个基类的申明顺序会影响程序的运行结果
    • 如需调用特定类的属性,可以用类名限定

  • 如何调用基类方法完成指定功能

经典问题:如何不重复访问基类功能?
以下是详细解释:


1. 代码结构与继承关系(图片1)
代码构建了一个典型的“菱形继承”结构:


顶层基类:Account。
中间层:CheckingAccount 和 SavingsAccount 都继承自 Account。
底层子类:NewAccount 同时继承自 CheckingAccount 和 SavingsAccount(多重继承)。

关键逻辑:每个子类都重写了 printInfo 方法,并在内部使用了 super().printInfo()。这一步非常关键,它告诉 Python 在执行完当前类的逻辑后,继续去调用 MRO 列表中的下一个类的方法。


2. 执行机制(图片2 & 图片3)
当你执行 NewAccount().printInfo() 时,Python 并非简单地从左到右调用父类,而是通过以下机制保证逻辑正确:


MRO(方法解析顺序):Python 内部维护了一个 MRO 列表,用来决定调用链的顺序。对于 NewAccount,其调用顺序(MRO)大致为: NewAccount -> CheckingAccount -> SavingsAccount -> Account -> object。
super() 的智能调度:在 Python 的多重继承中,super() 并不直接指向父类,而是根据 MRO 列表 指向下一个类。
在 NewAccount 中调用 super(),它会跳转到 CheckingAccount;
在 CheckingAccount 中调用 super(),它会跳转到 SavingsAccount(而不是 Account);
在 SavingsAccount 中调用 super(),最后才跳转到 Account。

3. 核心目的:为何要这样设计?
图片3 提到的“依次不重复调用”是为了解决多重继承中的经典难题:


避免重复调用:如果没有 super() 和 MRO 的机制,手动分别调用父类方法极易导致 Account 的代码被重复执行多次(例如 CheckingAccount 和 SavingsAccount 各自调用了一次 Account)。
代码的扩展性与一致性:通过 super(),整个调用链条就像一条流水线。每个类只需要关注“我之后是谁”,不需要硬编码父类的名称。这实现了“开闭原则”,即如果你以后想在中间加入一个 CreditAccount 类,只需要修改继承顺序,而不需要修改各个 printInfo 里的具体调用逻辑。

总结
这套逻辑是 Python 处理多继承复杂性的“银弹”:


MRO 保证了调用链是线性的、唯一的。
super() 保证了调用链能顺滑地向下传递。 通过这种方式,原本复杂的多重继承关系被梳理成了清晰的单向调用流,确保了每一个层级的逻辑都能且仅能被执行一次。
posted @ 2026-05-10 13:15  RReally  阅读(35)  评论(0)    收藏  举报
//一下两个链接最好自己保存下来,再上传到自己的博客园的“文件”选项中