没有类的时候,数据用字典装、操作用函数写,两边分开。
谁都能往字典里塞一个错的键,函数也不知道自己该处理什么形状的数据。
类把两者捆在一起:数据是属性,操作是方法。方法跟着数据走,这是类和字典最大的区别。
__init__ 是「造一个出来的时候自动跑」的那个函数,self 就是刚造出来的那个对象本身。
前七章的代码都是「一堆函数 + 一堆变量」。这一章换个组织方式。 教材第 8 章开头两节是【自定义类:远离意大利面条】和【保存试算表数据的自定义类】。 「意大利面条」是讲一堆互相调用的函数绞在一起,看不出哪段归哪段。类是解这个的。 本篇是全书第一个 ⭐⭐⭐:能读懂的代码不算学会,能自己写出来才算。 演示里的类都不长, 建议照着敲一遍。
一、先看没有类的时候
用字典存一条货:{'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5}。
算金额写个函数 total_of(rec),格式化再写一个。
能跑。演示第一段算出来 180.00,格式化也正常。问题不在跑不跑得起来,在两个月以后。
第一个问题:函数和数据是分开的。
谁都能往 rec 里塞一个错的键,写 rec['单介'] = 1.5 也不会报错,等你算钱的时候才发现是 0。
第二个问题:total_of 不知道自己该处理什么形状的字典。传一个缺「单价」的进去,它抛 KeyError;
传一个单价是字符串的进去,它算出一串拼接。
字段一多、函数一多,就乱了。
你会有一堆 total_of、fmt_of、check_of,名字都带 _of,因为它们都在操作同一种字典。
但语言层面看不出这层关系。
这就是「意大利面条」:面条本身没问题,问题是它们互相缠着,你找不到线头。
二、换成类
class Item: 开头,def __init__(self, name, qty, price): 造对象,def total(self): 算金额,
def __str__(self): 决定 print 出来是什么样。
it = Item('六角螺栓 M8', 120, 1.5) 之后,it.total() 得 180.00,
print(it) 得「六角螺栓 M8 × 120 = 180.00 元」。
__init__ 是「造一个出来的时候自动跑」的那个函数。 你不调它,Item(...) 这句就替你调了。
它的作用是把参数存进对象里。
self 就是刚造出来的那个对象本身。
写 self.name = name 意思是「把传进来的 name 存到这个对象的 name 上」。
self 不用你自己传。 写 it.total() 的时候,Python 自动把 it 塞进 self。
这就是为什么定义时要写 self、调用时不写。
一个高频错误:写 it.total 少了括号。
拿到的是方法本身,不是算出来的结果。 打印出来是 <bound method ...>,报错信息也不太好懂。
三、每个对象各有一份数据
同一个类造两个对象,a 是六角螺栓 120 个,b 是不锈钢垫片 500 个。
演示里改了 a.qty 之后,a 的金额变了,b 一点没受影响。
各存各的数据,共用同一套方法。 这是类最核心的一件事。
跟字典比,多出来的是「方法和数据的绑定关系」:a.total() 里的 total 知道自己在算 a 的金额,
不用你把 a 传进去。
这一条看着小,实际影响很大。 函数和数据绑在一起之后,「这段逻辑属于谁」就清楚了。 两个月后回来看代码,一眼知道该改哪个类。
四、类变量是所有人共用的
写在 __init__ 外面的变量是类变量,所有对象共用一份。
写在 __init__ 里面、带 self. 的是实例变量,每个对象各一份。
演示里 Counter 类有一个类变量 made,每造一个就加一。
造了三个之后 Counter.made 是 3,c1.made 也是 3——因为 c1 自己没有 made,它去类里找,
找到的就是那一份共用的。
判断标准很简单:这个东西是「每个对象各自的」还是「所有对象共享的」。
「一共造了多少个」「默认税率是多少」属于共享的;「这个货叫什么名字」「这个货有几个」属于各自的。
最经典的坑在这里:给 c1.made = 99 赋值,
这不是改类变量,是给 c1 造了一个新的实例变量。 于是 c1.made 是 99,Counter.made 还是 3,
c2.made 也还是 3。
「改了一个,全都变了」和「改了一个,别的没变」,两件事的写法看着几乎一样,区别只在有没有赋值。
这一点必须自己试一遍才记牢。
五、继承:在已有的类上加东西
class DiscountItem(Item): 意思是「我做了一个新类,它跟 Item 一样,但有下面这些不同」。
演示里 DiscountItem 只重写了 total(打 8 折)和 __str__(显示折扣),其余全部照用父类的。
isinstance(d, Item) 为 True,哪怕 d 是 DiscountItem。 这是继承最实用的一点:
写了个只认 Item 的函数,打折商品也能传进去。
教材里那句「子类继承父类的所有方法,只把要改的那个重写掉」,就是这个意思。
重写(override)不是覆盖,是「我这个类用我自己的版本」。
super() 是在子类里调父类的写法。
想「在父类基础上再加一点」,就写 super().total() * 0.8,而不是把父类的公式抄一遍。
继承不要滥用。判断标准:两者是不是「是一种」的关系。 打折商品是一种商品,所以继承合理;
一张订单不是一种商品,那该用组合(把商品放进订单里)。
六、一个类管一张表
教材 8-2 节的做法:一个类装一整张表,再加几个「算总数」「按名字汇总」的方法。
演示第六段那张表四行、合计 654.00 元,还按品名汇总出了三行。这些都是 Table 类的方法。
以后要用这张表,import 这个类就行,不用每次重写一遍汇总逻辑。
这一段是上一篇那个「一百条记录」的自然延续:一堆记录放哪儿?放列表里,谁都能乱改; 放类里,读写都从这个类过,出问题只有一个地方要查。 类和函数的分界线不在语法,在「这段逻辑会不会被用第二次」。 只用一次的函数,包成类是负担; 用五次的,包成类省五次。
七、动手 Demo:类:把数据和操作它的方法捆在一起
先看没有类的时候怎么写的,再看换成类之后的样子。
然后验证两件事:两个对象的数据互不影响、类变量是共用的。
最后加一个打折子类,看 isinstance 怎么判。
print('① 先看没有类的时候')
rec = {'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5}
total_of = lambda r: r['数量'] * r['单价']
fmt_of = lambda r: '%s × %d = %.2f 元' % (r['品名'], r['数量'], total_of(r))
print(' 用字典', rec)
print(' 算金额 %.2f' % total_of(rec))
print(' 格式化', fmt_of(rec))
print(' 但函数和数据是分开的:谁都能往 rec 里塞一个错的键,'
'total_of 也不知道自己该处理什么形状的字典。')
print('② 换成类')
class Item:
def __init__(self, name, qty, price):
self.name, self.qty, self.price = name, qty, price
def total(self):
return self.qty * self.price
def __str__(self):
return '%s × %d = %.2f 元' % (self.name, self.qty, self.total())
it = Item('六角螺栓 M8', 120, 1.5)
print(' it = Item(\'六角螺栓 M8\', 120, 1.5)')
print(' it.total() → %.2f' % it.total())
print(' print(it) → %s' % it)
print(' type(it) → %s' % type(it).__name__)
print(' 注意 total 后面有括号才是调用:it.total 是 %r' % it.total)
print('③ 每个对象各有一份数据')
a = Item('六角螺栓 M8', 120, 1.5)
b = Item('不锈钢垫片', 500, 0.4)
print(' a → %s' % a)
print(' b → %s' % b)
a.qty = 200
print(' 改了 a.qty 之后:')
print(' a → %s' % a)
print(' b → %s(没受影响)' % b)
print('④ 类变量是所有人共用的')
class Counter:
made = 0
def __init__(self):
Counter.made += 1
c1, c2, c3 = Counter(), Counter(), Counter()
print(' 造了三个,Counter.made = %d' % Counter.made)
print(' c1.made = %d(也是这个数)' % c1.made)
c1.made = 99
print(' 但给 c1.made 赋值之后:c1.made = %d,Counter.made = %d,c2.made = %d'
% (c1.made, Counter.made, c2.made))
print(' 赋值不是改类变量,是给 c1 造了一个新的实例变量。')
print('⑤ 继承:在已有的类上加东西')
class DiscountItem(Item):
def __init__(self, name, qty, price, rate=0.8):
super().__init__(name, qty, price)
self.rate = rate
def total(self):
return super().total() * self.rate
def __str__(self):
return '%s × %d = %.2f 元(打 %.0f 折)' % (self.name, self.qty, self.total(),
self.rate * 10)
d = DiscountItem('清仓螺栓', 100, 1.5)
print(' 原价类 ', Item('六角螺栓 M8', 120, 1.5))
print(' 打折类 ', d)
print(' 类型 ', type(d).__name__)
print(' 是不是 Item?', isinstance(d, Item))
print('⑥ 一个类管一张表')
class Table:
def __init__(self, rows):
self.rows = list(rows)
def total(self):
return sum(r.total() for r in self.rows)
def by_name(self):
out = {}
for r in self.rows:
out[r.name] = out.get(r.name, 0) + r.qty
return out
t = Table([Item('六角螺栓 M8', 120, 1.5), Item('膨胀螺栓 10mm', 80, 2.3),
Item('不锈钢垫片', 500, 0.4), Item('六角螺栓 M8', 60, 1.5)])
print(' 表里 %d 行,合计 %.2f 元' % (len(t.rows), t.total()))
for k, v in t.by_name().items():
print(' %-14s %d' % (k, v))
print(' 一个类装一整张表,再加几个算总数、按名字汇总的方法。')
print(' 以后要用这张表,import 这个类就行,不用每次重写一遍汇总逻辑。')
实跑输出:
--------------------------------------------------------------------
类:把数据和操作它的方法捆在一起
--------------------------------------------------------------------
① 先看没有类的时候
--------------------------------------------------------------------
用字典 {'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5}
算金额 180.00
格式化 六角螺栓 M8 × 120 = 180.00 元
能跑。
但函数和数据是分开的:谁都能往 rec 里塞一个错的键,total_of 也不知道自己该处理什么形状的字典。
字段一多、函数一多,就乱了。
② 换成类
--------------------------------------------------------------------
it = Item('六角螺栓 M8', 120, 1.5)
it.total() → 180.00
print(it) → 六角螺栓 M8 × 120 = 180.00 元
type(it) → Item
__init__ 是「造一个出来的时候自动跑」的那个函数,self 就是刚造出来的那个对象本身。
注意 total 后面有括号才是调用。
写 it.total 拿到的是方法本身,不会算出金额——这个错很常见,报错信息还不太好懂。
③ 每个对象各有一份数据
--------------------------------------------------------------------
a → 六角螺栓 M8 × 120 = 180.00 元
b → 不锈钢垫片 × 500 = 200.00 元
改了 a.qty 之后:
a → 六角螺栓 M8 × 200 = 300.00 元
b → 不锈钢垫片 × 500 = 200.00 元(没受影响)
同一个类造出来的两个对象,各存各的数据,共用同一套方法。
这就是类和字典最大的区别:方法跟着数据走。
④ 类变量是所有人共用的
--------------------------------------------------------------------
造了三个,Counter.made = 3
c1.made = 3(也是这个数)
写在 __init__ 外面的是类变量,所有对象共用一份。写 self.made = ... 才是给这个对象单独造一份。
想统计「一共造了多少个」用类变量,想给每个对象存自己的状态用实例变量。
混着写就会出现「改了一个,全都变了」这种怪事。
⑤ 继承:在已有的类上加东西
--------------------------------------------------------------------
原价类 六角螺栓 M8 × 120 = 180.00 元
打折类 清仓螺栓 × 100 = 120.00 元(打 8 折)
类型 DiscountItem
是不是 Item?True
子类继承父类的所有方法,只把要改的那个重写掉。上面它重写了 total 和 __str__,其余照用。
isinstance 判断「是不是某种东西」时,子类也算。
这是继承最实用的一点:写了个只认 Item 的函数,打折商品也能传进去。
⑥ 一个类管一张表
--------------------------------------------------------------------
表里 4 行,合计 654.00 元
不锈钢垫片 500
六角螺栓 M8 180
膨胀螺栓 10mm 80
一个类装一整张表,再加几个「算总数」「按名字汇总」的方法。
以后要用这张表,import 这个类就行,不用每次重写一遍汇总逻辑。
这就是教材 8-2 节说的「保存试算表数据的自定义类」。
类和函数的分界线不在语法,在「这段逻辑会不会被用第二次」。
第一段那两个 lambda 就是「没有类」的典型写法:函数操作字典,两边靠约定连在一起, 语言层面看不出关系。 第二段那个 it.total 是本篇最高频的错误。 少一对括号,拿到的是方法本身,打印出来是 bound method,不会算出金额。 第三段验证了「各存各的数据,共用同一套方法」。改了 a.qty,b 一点没受影响。 第四段那段赋值是重点:给 c1.made 赋值不是改类变量,是造了一个新的实例变量。 所以 c1.made 变 99,Counter.made 还是 3。这个坑必须自己跑一遍才记牢。 第五段 isinstance 返回 True,说明子类也算父类。 这就是「写了个只认 Item 的函数,打折商品也能传进去」的原理。 第六段那个 Table 类把「一堆记录」和「汇总逻辑」捆在一起, 这正是教材说的「保存试算表数据的自定义类」。
30 秒实验
- 把演示第二段的
it.total()改成it.total,打印出来看是什么,再试着加一,看报什么错。 - 在演示第四段末尾加一句
c2.made = 5,再看三个made各是多少。 - 给
Table加一个cheapest()方法,返回单价最低的那一行,提示:min(self.rows, key=lambda r: r.price)。
常见误区
- 「
it.total和it.total()差不多。」差一对括号。it.total拿到的是方法本身,it.total()才是算出来的数。 少写括号的报错信息不太好懂, 容易卡很久。 - 「给
c1.made = 99就是改类变量。」不是,是给c1造了一个实例变量。 类变量还是原值,别的对象也还是原值。 要改类变量得写Counter.made = 99。 - 「继承能复用代码,所以能继承就继承。」两者必须是「是一种」的关系。 订单不是一种商品,该用组合。 滥用继承会让类的关系越缠越乱。
自测三问(点开对答案)
1. `__init__` 和 `self` 分别是什么?为什么调用时不用传 `self`? —— `__init__` 是造对象时自动跑的那个函数,负责把参数存进对象。`self` 是刚造出来的那个对象本身。 调用 `it.total()` 时 Python 自动把 `it` 塞进 `self`,所以定义时要写、调用时不写。 2. 类变量和实例变量怎么区分?给对象的一个类变量赋值会发生什么? —— 写在 `__init__` 外面的是类变量(所有对象共用),写在里面带 `self.` 的是实例变量。 给对象赋值一个跟类变量同名的属性,**不是改类变量,是给这个对象造了一个新的实例变量**, 类变量和别的对象都不受影响。 3. `isinstance(打折商品, Item)` 为什么是 `True`?这有什么实际用处? —— 因为 `DiscountItem` 继承自 `Item`,子类的实例也算是父类的一种。 实际用处是:你写了一个只处理 `Item` 的函数,打折商品也能直接传进去,不用改函数。下一篇回到存数据:同一份内容用 JSON、XML、pickle 各存一遍,看它们各自适合什么场合。
- ☐ (本篇完,下一篇 8.2 XML、JSON 与 pickle)