1 类与对象 —— 「数据 + 行为」是怎么绑在一起的
是什么?为什么学?
类(class)是模板,对象(object)是根据模板造出来的具体实例。一个对象把「数据(属性)」和「行为(方法)」装进同一个包里:account.balance 是它的数据,account.deposit() 是它的行为。之前我们学的都是「数据 + 函数」:数据放变量里,函数处理数据;但真实世界里的东西是「数据 + 行为」一体的——一个银行账户既有余额(数据),又有存款、取款(行为)。面向对象编程(OOP)就是把这俩绑在一起,让代码更贴近现实。
底层原理
Python 里一切皆对象:变量存的是指向对象的引用,每个对象内部有一个「属性字典」(__dict__),属性名 → 属性值都存在这里;访问 obj.attr 先查对象自己的 __dict__,查不到再查类的 __dict__(Day 16 学继承后还会继续往父类查)。「对象 = 数据 + 行为」在底层就是两个字典的配合。
生活类比
类像产品模具,对象像模具压出来的产品:模具本身不能卖钱,但每一个产品都按模具的形状出厂,并且自带模具刻好的「使用说明」(方法)。一个模具可以压出成千上万个互不相同的产品——每个产品有自己的编号(属性),但都具备同一种功能(方法)。
注意事项总结
1 两个对象是两个独立的「包」,互不影响:
class Dog:
pass
dog1 = Dog()
dog2 = Dog()
print(dog1 is dog2) # 是不是同一个对象?不是
print(id(dog1), id(dog2)) # 内存地址不同
运行结果:
False
示例输出(内存地址每次运行都不同):2345678901234 2345678905678
class Dog: 定义类,pass 表示暂时什么都不做(占位)。Dog() 加括号才是在「造对象」——类是模板,加括号才是生产。dog1 is dog2 为 False,证明它们是两个独立的对象。
2 **对象内部的属性字典 dict**:
class Dog:
def __init__(self, name):
self.name = name
dog = Dog("旺财")
print(dog.__dict__) # 属性都装在这个字典里
运行结果:
{'name': '旺财'}
对象本质上就是一个带名字的字典,只不过访问时写 dog.name 而不是 dog["name"]。
3 obj.attr 的查找顺序——先找自己,再找类:
class Dog:
species = "犬科" # 类属性:所有狗共享
dog = Dog()
print(dog.species) # 自己没定义,去类里找,找到"犬科"
dog.species = "哈士奇" # 给自己单独存一份
print(dog.species) # 现在自己有,用自己这份
print(Dog.species) # 类里那份还是"犬科"
运行结果:
犬科
哈士奇
犬科
dog.species = "哈士奇" 是在对象自己身上新建一个属性,并不改动类里的那份。
易错点
- 错误写法:
dog = Dog(忘加括号) → 问题:dog 变成类本身而不是对象,访问属性报错 → 正确写法:造对象必须加括号:dog = Dog()。 - 错误写法:类名用小写:
class dog:→ 问题:不报错但违背约定,别人看不懂 → 正确写法:类名用驼峰式大写开头:class Dog:。 - 错误写法:以为
dog1 = Dog(); dog2 = Dog()是同一个对象 → 问题:是两个独立对象,互不影响 → 正确写法:想要同一个对象:dog2 = dog1(复制引用)。
记忆口诀:类是模具对象是货,加括号才出货;先查自己再查类,属性字典装一切。
2 init 的真相 —— 创建对象时发生了什么
是什么?为什么学?
__init__ 是构造方法(更准确说是「初始化方法」):创建对象时自动执行,用来给新对象设置初始属性。没有它,你得创建完对象再手动补属性,忘补就报 AttributeError;有了它,「造出来的对象一定初始化好了」。
底层原理
Dog("旺财", 3) 这一行实际做了两步:第一步 __new__(分配一块内存,得到一个「空白对象」),第二步自动调用 __init__(给这块空白对象填上数据)。__init__ 的第一个参数 self 就是第一步分配出来的那个空白对象本身。注意 __init__ 不能 return 非 None 的值——它只负责「填数据」,不负责「交出新对象」。
生活类比
像入住酒店:__new__ 是前台先给你开好一间房(拿到房卡),__init__ 是你进房间后把行李摆好(放好属性)。你只需要对前台说一句 Dog("旺财", 3),开房和布置都自动完成。
注意事项总结
1 init 是自动调用的——看执行顺序:
class Dog:
def __init__(self, name):
print("开始初始化一个狗对象……")
self.name = name
print("初始化完成,名字是", self.name)
dog = Dog("旺财")
print("创建语句执行完了")
运行结果:
开始初始化一个狗对象……
初始化完成,名字是 旺财
创建语句执行完了
Dog("旺财") 这行代码一执行,__init__ 里的内容抢先跑完,然后才执行下一行——证明它是创建流程里自动触发的一环。
2 参数 → 属性的传递过程:
class Dog:
def __init__(self, name, age):
self.name = name # 参数 name 传进来,存成属性 self.name
self.age = age # 参数 age 传进来,存成属性 self.age
dog = Dog("旺财", 3)
print(dog.name, dog.age)
运行结果:
旺财 3
Dog("旺财", 3) 里的两个括号参数,会原样传给 __init__ 的形参。记住:带 self. 的是属性,不带的是参数。
3 默认参数——有的属性可以「不传」:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
a = BankAccount("张三")
b = BankAccount("李四", 500)
print(a.owner, a.balance)
print(b.owner, b.balance)
运行结果:
张三 0
李四 500
balance=0 是默认参数(Day 8 学过),开户时不传余额就是 0。「构造方法 + 默认参数」的经典组合让类的使用更灵活。
易错点
- 错误写法:
__init__里写return "hello"→ 问题:报TypeError: __init__() should return None→ 正确写法:__init__只赋值,不 return(或return None)。 - 错误写法:属性拼写不一致:
self.name = name却访问dog.nmae→ 问题:报AttributeError: 'Dog' object has no attribute 'nmae'→ 正确写法:属性名统一,写完检查拼写。 - 错误写法:创建时少传参数:
Dog("旺财")但__init__要两个参数 → 问题:报TypeError: __init__() missing 1 required positional argument→ 正确写法:参数个数对齐,或用默认参数兜底。
记忆口诀:new 开房 init 摆行李,自动执行不用你管;带 self. 是属性,不带是参数。
3 self 的本质 —— 方法里的「我自己」
是什么?为什么学?
self 是实例方法的第一个参数,代表调用这个方法的那个对象。不学 self,你只能把每个对象的数据都写成全局变量(互相串味);学会 self,方法内部用 self.xxx 读写的就是「当前这个对象」的属性。
底层原理
方法其实就是「类里定义的函数」。调用 dog.bark() 时,Python 做了两件事:先找到 dog 的类里的 bark 函数,然后把 dog 自己作为第一个参数传进去。所以 dog.bark() 与 Dog.bark(dog) 完全等价——前者是语法糖(写法上的简写),后者是它背后的真相。
生活类比
self 像对讲机里喊「我是」:全班同学共用同一套「回答问题」的规则(方法只有一份),但你回答时说的是你自己的名字。self 就是方法执行时「谁在说话」的那个「谁」。
注意事项总结
1 dog.bark() 与 Dog.bark(dog) 等价;不同对象调用,self 各指各的:
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name}:汪汪!")
def introduce(self):
print("我是", self.name)
a = Dog("旺财")
b = Dog("来福")
a.bark() # 语法糖写法:自动把 a 传给 self
Dog.bark(b) # 本质写法:手动把 b 传给 self
a.introduce()
b.introduce()
运行结果:
旺财:汪汪!
来福:汪汪!
我是 旺财
我是 来福
前两行输出一模一样,证明 dog.bark() 与 Dog.bark(dog) 完全等价——方法定义时必须写 self、调用时绝不写 self。introduce 方法只有一份,但 a.introduce() 时 self 是 a、b.introduce() 时 self 是 b——方法代码共享,属性各归各。
易错点
- 错误写法:方法定义忘了 self:
def bark():→ 问题:调用时报TypeError: bark() takes 0 positional arguments but 1 was given→ 正确写法:实例方法第一个参数必须是self。 - 错误写法:调用时手动传 self:
dog.bark(dog)→ 问题:报参数个数错误(self 已自动传入) → 正确写法:调用时不要传 self。 - 错误写法:在方法里用全局变量存数据 → 问题:多个对象共享一份数据,互相串味 → 正确写法:一律
self.xxx存到对象自己身上。
记忆口诀:方法定义有 self,调用时候自动传;self 就是我自己,属性都往身上挂。
4 str 背后的调用链 —— 打印一个对象时发生了什么
是什么?为什么学?
__str__ 是「转字符串」的魔法方法(魔法方法 = 双下划线开头结尾、Python 自动调用的特殊方法,Day 17 会系统学)。定义它之后,print(对象)、str(对象)、f-string 里 {对象} 都会显示成你设计的文字,而不是 0x... 内存地址。
底层原理
print(obj) 的执行链:print 内部调 str(obj) → str(obj) 去 type(obj)(obj 的类)里找 __str__ 方法 → 调用它拿到字符串再输出。如果类里没定义 __str__,str(obj) 会退而求其次用 __repr__(Day 17 讲),再没有就用基类默认的地址格式 <__main__.Dog object at 0x...>。
生活类比
身份证号能唯一标识一个人,但没人用身份证号介绍自己。__str__ 就是对象的名片:默认的地址格式像一串编号,定义 __str__ 后,对象在「自我介绍」时亮出精心设计的名片。
注意事项总结
1 没有 str 时,打印对象看到的是地址:
class Dog:
def __init__(self, name):
self.name = name
dog = Dog("旺财")
print(dog)
示例输出:
<__main__.Dog object at 0x0000020B12345678>
没有定义 __str__ 时,print(dog) 显示类名 + 内存地址——对调试毫无帮助。
2 定义 str 后,打印变得可读:
class Dog:
def __init__(self, name):
self.name = name
def __str__(self):
return f"小狗{self.name}"
dog = Dog("旺财")
print(dog) # print 自动调用 __str__
print(str(dog)) # str() 也调用 __str__
print(f"这是{dog}") # f-string 里的 {dog} 同样调用
运行结果:
小狗旺财
小狗旺财
这是小狗旺财
__str__ 方法体里用 return 返回一个字符串(注意是 return 不是 print!)。只要返回了字符串,print、str()、f-string 三种场景都会自动展示它。
易错点
- 错误写法:
__str__里写print(...)不 return → 问题:打印时输出 None 或错乱 → 正确写法:__str__必须return一个字符串。 - 错误写法:
__str__里写print(self)→ 问题:又触发__str__,无限递归,报RecursionError→ 正确写法:在__str__里只访问self.属性。 - 错误写法:
__str__返回数字:return self.age→ 问题:报TypeError: __str__ returned non-string→ 正确写法:先转字符串:return str(self.age)。
记忆口诀:print 找 str,str 找方法,return 字符串才算数;不定义就亮地址,定义了就亮名片。
5 类属性 vs 实例属性 —— 大家共享的 vs 自己独有的
是什么?为什么学?
写在类体里(方法外)的变量叫类属性,所有对象共享一份;在 __init__ 里用 self.xxx 创建的是实例属性,每个对象自己独有一份。区分它们能解决两个实际问题:① 让全体对象共享配置 / 计数器;② 避免「明明想独有,结果变成共享」的诡异 bug。
底层原理
类属性存在类的 __dict__ 里,实例属性存在对象的 __dict__ 里。访问 obj.attr 先查实例字典,查不到再查类字典——所以实例属性会「遮住」同名类属性。反过来,给实例赋值不会影响类属性(实例里那份是自己新建的)。这个「遮蔽」规则是理解共享与独有关键。
生活类比
类属性像公司食堂:所有员工都能吃同一锅饭;实例属性像个人饭盒:每个人饭盒里的菜是自己的。你在自己饭盒里加了鸡腿(给实例属性赋值),食堂的锅并不会多一个鸡腿(类属性不变)。
注意事项总结
1 类属性全体共享:
class Dog:
species = "犬科" # 类属性:所有狗共享
a = Dog()
b = Dog()
print(a.species, b.species, Dog.species)
运行结果:
犬科 犬科 犬科
类属性定义在类体最外层(缩进一级、与方法同级)。三处打印同一个值,因为它只有一份,存在类的字典里。
2 给实例赋值 = 在自己的字典里新建,不碰类属性:
class Dog:
species = "犬科"
a = Dog()
a.species = "哈士奇" # 在 a 自己的字典里新建 species
print(a.species) # 自己的那份
print(Dog.species) # 类里那份原封不动
print(a.__dict__) # 看看 a 的字典里有什么
运行结果:
哈士奇
犬科
{'species': '哈士奇'}
a 的 __dict__ 里多了自己的 species,从此 a 访问时「先查自己」命中,不再看类的。这就是「遮蔽」:自己有了就不看公共的。
3 用类属性实现「自动递增的账号编号」:
class Account:
_next_no = 1001 # 类属性:下一个可用的账号(_ 开头表示"内部使用")
def __init__(self, owner):
self.owner = owner
self.account_no = Account._next_no # 把当前编号拿给自己
Account._next_no += 1 # 全局计数器 +1,供下一个对象用
a1 = Account("张三")
a2 = Account("李四")
a3 = Account("王五")
print(a1.account_no, a2.account_no, a3.account_no)
运行结果:
1001 1002 1003
_next_no 是类属性,所有对象共享这一个计数器。注意操作类属性用的是 Account._next_no(通过类名),不是 self._next_no += 1(那会在自己身上新建一份,计数器永远不动)。
易错点
- 错误写法:在方法里
self.count += 1想改类属性 → 问题:实际是在自己身上新建实例属性,类属性纹丝不动 → 正确写法:通过类名改:MyClass.count += 1。 - 错误写法:类属性是列表,一个对象往里 append → 问题:所有对象共享同一个列表,数据互相污染 → 正确写法:可变数据放
__init__里:self.items = []。 - 错误写法:把类属性当「每个对象都有」用 → 问题:一个对象改了,其他对象全变 → 正确写法:想各管各的,用实例属性。
记忆口诀:类属性共享一份,实例属性各自有;先查自己再查类,改类要用类名改。
6 动手实践:银行账户类
做一个 BankAccount 类,支持开户(姓名+初始余额)、存款、取款、查询余额,取款不能透支:
# bank_account.py —— 银行账户类
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # 户主姓名
self.balance = balance # 当前余额
def deposit(self, amount):
if amount <= 0:
print("存款金额必须大于 0")
return
self.balance += amount
print(f"存入 {amount} 元,余额 {self.balance} 元")
def withdraw(self, amount):
if amount <= 0:
print("取款金额必须大于 0")
return
if amount > self.balance:
print("余额不足,取款失败")
return
self.balance -= amount
print(f"取出 {amount} 元,余额 {self.balance} 元")
def show(self):
print(f"户主:{self.owner},余额:{self.balance} 元")
def __str__(self):
return f"银行账户[{self.owner}] 余额 {self.balance} 元"
acc = BankAccount("张三", 1000)
print(acc)
acc.deposit(500)
acc.withdraw(200)
acc.withdraw(99999)
acc.show()
运行结果:
银行账户[张三] 余额 1000 元
存入 500 元,余额 1500 元
取出 200 元,余额 1300 元
余额不足,取款失败
户主:张三,余额:1300 元
升级挑战:给账户加一个 transfer 转账方法(从当前账户转到另一个账户),并给每个账户加一个自动递增的账号 account_no(提示:可以用类属性 _next_no 实现)。
今日总结
类的本质:模板(模具),加括号 Dog() 才造出对象;对象 = 属性(数据)+ 方法(行为)
对象底层是属性字典 __dict__;访问 obj.attr 先查实例字典、再查类字典
__init__ 在创建对象时自动执行,负责初始化属性;__new__ 负责分配内存,__init__ 不能返回非 None
self 是「调用方法的那个对象」;dog.bark() 等价于 Dog.bark(dog),定义写 self、调用不写 self
print / str() / f-string 都会自动调用 __str__,没定义时显示内存地址
类属性全体共享一份(存类的字典),实例属性各存各的;实例属性会遮蔽类属性,改类属性要用类名改
银行账户类可正常开户、存款、取款且不能透支
| 报错 | 原因 | 修复 |
|---|---|---|
TypeError: __init__() missing 1 required positional argument | 创建对象时少传了参数 | 检查 __init__ 的参数列表,创建时全部传上 |
AttributeError: 'Dog' object has no attribute 'name' | 忘了在 __init__ 里 self.name = ...,或拼写不一致 | 在 __init__ 中初始化所有属性 |
TypeError: bark() takes 1 positional argument but 2 were given | 定义方法忘了写 self | 方法第一个参数必须是 self |
NameError: name 'self' is not defined | 在方法里直接写了 self 却没把它当参数 | 确认 self 写在参数列表第一个 |
| 对象属性互相串了 | 用了全局变量存数据,而不是实例属性 self.xxx | 所有对象数据一律存成 self.属性 |