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.通过类调用成员函数
- 语法规则
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) 完全一样,只是写法不同。
- 打印结果的含义
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 专用,用户不要自定义 | init、dict |
封装实现
二、封装的实现:用 __xxx 保护数据
- 代码示例解析
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)
- 外部直接访问会报错
a = Account("Huang")
print(a.__balance)
# 报错:AttributeError: 'Account' object has no attribute '__balance'
- 报错的原因是:Python 对 __xxx 做了名称修饰,它实际变成了 _Account__balance,所以你用 __balance 找不到它。
这就是封装的体现:外部不能直接修改数据,只能通过公开的接口(如 deposit())来操作,保证数据安全。
non-public vs private:Python 访问控制的本质
- 关键区别
- C++/Java 的 private:是强制访问控制,编译器会阻止外部访问,是真正的 “ 私有”。
- Python 的 __xxx:只是命名约定 + 名称修饰,不是真正的强制私有。
你依然可以通过 a._Account__balance 强行访问到它:
print(a._Account__balance) # 输出 0
- Python 官方文档明确说
“Python 中不存在那种只能从对象内部访问的‘私有’实例变量。”
类成员
一、核心概念:什么是类属性和实例属性?
- 定义与位置
类属性:写在 class 里、但不在 init 方法里的变量,属于类本身,所有实例共享同一份数据。
例:Account.interest = 0.2
实例属性:写在 init 里、以 self.xxx 定义的变量,每个实例都有自己独立的一份数据。
例:self.balance = 0、self.holder = account_holder - 关键区别(对比表)
| 特性 | 类属性 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 的执行逻辑,是理解实例属性、类属性的关键:
- 基本语法
expr.name 的执行分为两步:
先计算 expr,得到一个对象(可以是类对象或实例对象)
再访问该对象的 name 成员
- 赋值场景(expr.name = value)
- 如果 expr 是实例对象:直接给这个实例绑定一个新的实例属性,不会影响类属性。
- 如果 expr 是类对象:直接修改类属性,会影响所有没有同名实例属性的对象。
这就是之前例子中 a.interest = 0.3 的本质:给实例 a 新增了一个同名实例属性,而不是修改类属性。
- 取值场景(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() 方法,代码就能运行。这大大增加了程序的灵活性。
多态成立的三个条件
有继承(子类继承父类)
子类重写父类方法
用父类引用指向子类对象,调用方法自动走子类实现
- 父类 + 两个子类
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)

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

多继承可能引起的问题
-
属性冲突(不同基类之间存在同名属性)
-
如何确定调用关系?
• 按照派生类-基类的顺序依次查询绑定关系
• 申明多个基类时,按照基类的申明顺序依次进行查找
• 多个基类的申明顺序会影响程序的运行结果
• 如需调用特定类的属性,可以用类名限定 -
如何调用基类方法完成指定功能
经典问题:如何不重复访问基类功能?
以下是详细解释:
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() 保证了调用链能顺滑地向下传递。 通过这种方式,原本复杂的多重继承关系被梳理成了清晰的单向调用流,确保了每一个层级的逻辑都能且仅能被执行一次。

浙公网安备 33010602011771号