| Day 15 | 类与对象:数据 + 行为(class / __init__ / self) |

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!)。只要返回了字符串,printstr()、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.属性
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇