类的定义与实例化
Python 是一门彻头彻尾的面向对象语言。所谓类(Class),可以理解成一张图纸;所谓对象(Object),就是照着图纸造出来的零件。图纸画一次,零件可以造无数个,每个零件身上带着自己的数据(属性),也带着自己的行为…
Python 是一门彻头彻尾的面向对象语言。所谓类(Class),可以理解成一张图纸;所谓对象(Object),就是照着图纸造出来的零件。图纸画一次,零件可以造无数个,每个零件身上带着自己的数据(属性),也带着自己的行为(方法)。把相关的数据和操作打包在一起,代码的结构和复用性都会上一个台阶。
一、从一张空图纸开始
class 关键字用来画图纸:
class Goods: # 定义一个类
pass # 暂时什么都不装
item = Goods() # 照图纸造一个对象(实例化)
print(type(item)) # <class '__main__.Goods'>
用 type() 三参形式"现场造图纸"
类本身也是对象,所以类也可以被"造"出来——type() 的三参数形式就是干这个的:
# type(类名, (父类元组,), {名字字典})
def show(self):
print(f"商品:{self.name}")
# 运行时动态创建 Goods 类
Goods = type("Goods", (), {"name": "机械键盘", "show": show})
item = Goods()
print(item.name) # 机械键盘
item.show() # 商品:机械键盘
三个参数分别是什么:
- 参数一:类名(字符串)
- 参数二:要继承的父类组成的元组
- 参数三:装着类属性和方法的字典
什么时候用得上? ORM 框架里根据数据库配置批量生成模型类、根据接口描述自动拼装类,靠的都是这一手。
二、__init__:对象的出厂设置
__init__ 是构造方法:造对象的那一刻自动执行,专门负责给对象装上初始属性:
class Goods:
def __init__(this, name, price):
this.name = name
this.price = price
item = Goods("机械键盘", 399) # 参数会自动传给 __init__
print(item.name) # 机械键盘
print(item.price) # 399
注意: 第一个参数接收的是对象自己(习惯写作 self,但名字随便取——上面写成 this 一样能跑)。它是唯一一个调用时不用传的参数。
三、实例属性与类属性:一人一份 vs 全员共享
实例属性:跟着对象走
写在 self. 后面的属性,每个对象各存各的:
class Goods:
def __init__(self, name):
self.name = name # 实例属性
a = Goods("机械键盘")
b = Goods("显示器")
print(a.name) # 机械键盘
print(b.name) # 显示器
类属性:挂在图纸上
直接写在类体里的属性,所有对象共同可见:
class Goods:
category = "未分类" # 类属性
def __init__(self, name):
self.name = name
a = Goods("机械键盘")
b = Goods("显示器")
print(a.category) # 未分类
print(b.category) # 未分类
# 想改就通过类名改,全体对象立刻生效
Goods.category = "数码产品"
print(a.category) # 数码产品
访问规则一句话: 实例属性挂在 self 上,类属性用 类名 或 self 都能读到。
四、三种方法:实例方法、类方法、静态方法
实例方法:最常见的那种
定义在类里的普通函数,第一个参数固定是 self:
class Goods:
def __init__(self, name):
self.name = name
def show(self):
print(f"商品:{self.name}")
def advertise(self):
self.show() # 方法内部可以调其他方法
print(f"{self.name},速来抢购!")
item = Goods("机械键盘")
item.show() # 商品:机械键盘
item.advertise() # 商品:机械键盘 + 机械键盘,速来抢购!
类方法:@classmethod
第一个参数换成 cls,代表类本身,适合读写类属性:
class Goods:
created = 0 # 类属性:记录一共造过多少件商品
def __init__(self, name):
self.name = name
Goods.created += 1
@classmethod
def total_created(cls):
return cls.created
a = Goods("机械键盘")
b = Goods("显示器")
print(Goods.total_created()) # 2
静态方法:@staticmethod
既不要 self 也不要 cls,本质就是普通函数,只是逻辑上归属这个类,放在类里方便管理:
class FeeUtils:
@staticmethod
def tax(amount, rate):
return amount * rate
@staticmethod
def free_shipping(amount):
return amount >= 99
print(FeeUtils.tax(100, 0.13)) # 13.0
print(FeeUtils.free_shipping(59)) # False
三者对照:
| 方法类型 | 装饰器 | 第一个参数 | 能用实例属性吗 | 能用类属性吗 |
|---|---|---|---|---|
| 实例方法 | 无 | self |
✓ | ✓ |
| 类方法 | @classmethod |
cls |
✗ | ✓ |
| 静态方法 | @staticmethod |
无 | ✗ | ✗ |
五、继承:站在父类的肩膀上
子类直接获得父类的属性和方法,还可以在此基础上扩展或改写:
class Goods:
def __init__(self, name):
self.name = name
def summary(self):
print("这是一件商品")
class EBook(Goods): # EBook 继承 Goods
def summary(self): # 改写父类的同名方法
print(f"《{self.name}》电子书,下单即读")
class AudioBook(Goods):
def summary(self):
print(f"《{self.name}》有声书,随时收听")
book = EBook("Python 从入门到实践")
audio = AudioBook("三体")
book.summary() # 《Python 从入门到实践》电子书,下单即读
audio.summary() # 《三体》有声书,随时收听
用 super() 把活儿递给父类
class Goods:
def __init__(self, name):
self.name = name
print("Goods 初始化")
class EBook(Goods):
def __init__(self, name, file_format):
super().__init__(name) # 先让父类处理 name
self.file_format = file_format # 再补充自己的属性
print("EBook 初始化")
book = EBook("Python 从入门到实践", "EPUB")
print(book.name) # Python 从入门到实践
print(book.file_format) # EPUB
多继承:一个子类,多张图纸
class Downloadable:
def download(self):
print("支持下载到本地")
class Shareable:
def share(self):
print("支持赠送给好友")
class EBook(Downloadable, Shareable): # 同时继承两个类
pass
book = EBook()
book.download() # 支持下载到本地
book.share() # 支持赠送给好友
方法解析顺序(MRO)
多个父类出现同名方法时,Python 沿着 MRO(Method Resolution Order)这条固定路线找:
class Root:
def greet(self):
print("来自 Root")
class Left(Root):
def greet(self):
print("来自 Left")
class Right(Root):
def greet(self):
print("来自 Right")
class Joined(Left, Right): # MRO: Joined → Left → Right → Root
pass
j = Joined()
j.greet() # 来自 Left(先碰到 Left 的版本)
# 查看 MRO 路线
print(Joined.__mro__)
# (<class 'Joined'>, <class 'Left'>, <class 'Right'>, <class 'Root'>, <class 'object'>)
super() 在多继承里听 MRO 的
super() 调用的是 MRO 路线上的下一站,不一定是书写上的直接父类:
class Root:
def __init__(self):
print("Root 初始化")
class Left(Root):
def __init__(self):
print("Left 初始化")
super().__init__() # 下一站是 Right,不是 Root
class Right(Root):
def __init__(self):
print("Right 初始化")
super().__init__() # 下一站才是 Root
class Joined(Left, Right):
def __init__(self):
print("Joined 初始化")
super().__init__() # 下一站是 Left
Joined()
# 输出:
# Joined 初始化
# Left 初始化
# Right 初始化
# Root 初始化
提醒: 多继承功能虽强,层级一深就难读难调。实际开发里,"组合"(把功能类做成成员塞进来)通常比多继承更好维护。
六、下划线约定:Python 的"软私有"
Python 没有强制的 public/private,靠命名习惯来传达意图:
| 命名 | 含义 | 使用建议 |
|---|---|---|
name |
公开 | 随意访问 |
_name |
受保护(约定) | 最好别在外面直接碰 |
__name |
私有(触发名称改写) | 外部很难直接碰到 |
class Wallet:
def __init__(self, owner, amount):
self.owner = owner # 公开
self._amount = amount # 受保护
self.__pin = "666888" # 私有(实际存成 _Wallet__pin)
def income(self, n):
if n > 0:
self._amount += n
def amount(self):
return self._amount
wallet = Wallet("小周", 1000)
print(wallet.owner) # 小周
print(wallet._amount) # 1000(技术上能访问,但别这么干)
# print(wallet.__pin) # AttributeError!
print(wallet._Wallet__pin) # 666888(绕过名称改写强行访问)
记住: 这一切靠的是约定和名称改写,不是编译器强制。
七、对象自带的"身份证":内置属性
| 属性 | 含义 |
|---|---|
__class__ |
对象由哪个类造出来的 |
__bases__ |
类的所有直接父类(元组) |
__base__ |
类的第一个直接父类 |
__dict__ |
对象或类的属性字典 |
class Goods:
pass
class EBook(Goods):
category = "数字内容"
def __init__(self, name):
self.name = name
book = EBook("Python 从入门到实践")
# __class__:对象属于哪个类
print(book.__class__) # <class '__main__.EBook'>
print(book.__class__.__name__) # EBook
# __bases__ / __base__:父类信息
print(EBook.__bases__) # (<class '__main__.Goods'>,)
print(EBook.__base__) # <class '__main__.Goods'>
# __dict__:对象身上挂了哪些属性
print(book.__dict__) # {'name': 'Python 从入门到实践'}
# 类的 __dict__:包含类属性和方法
print(EBook.__dict__.keys()) # dict_keys([..., 'category', '__init__', ...])
八、类型判断与内容查看
type():对象由哪个类造的
book = EBook("Python 从入门到实践")
print(type(book)) # <class '__main__.EBook'>
print(type(EBook)) # <class 'type'>
print(type(123)) # <class 'int'>
print(type("hi")) # <class 'str'>
isinstance():是不是这个类(或其子类)的实例
book = EBook("Python 从入门到实践")
print(isinstance(book, EBook)) # True
print(isinstance(book, Goods)) # True(EBook 继承自 Goods)
print(isinstance(book, str)) # False
# 传元组:命中其中任意一个就算 True
print(isinstance(book, (EBook, int))) # True
print(isinstance(123, (str, int))) # True
与 type() 的差别: isinstance() 认继承关系,type() 只认"亲爹"。
issubclass():类和类之间的血缘
print(issubclass(EBook, Goods)) # True
print(issubclass(EBook, EBook)) # True(类算自己的子类)
print(issubclass(Goods, EBook)) # False
# 也支持元组
print(issubclass(EBook, (Goods, str))) # True
dir():把对象的成员全列出来
book = EBook("Python 从入门到实践")
print(dir(book)) # 列出对象的所有属性和方法(含继承来的)
# ['__class__', '__delattr__', ..., 'name', ...]
print(dir(EBook)) # 列出类的所有属性和方法
print(dir()) # 不传参数时,列出当前作用域的所有名字
vars():属性字典一览
book = EBook("Python 从入门到实践")
print(vars(book)) # {'name': 'Python 从入门到实践'}
print(vars(EBook)) # 类的 __dict__
print(book.__dict__) # 和 vars(book) 一回事
print(vars()) # 不传参数时,等同于 locals()
九、属性的动态操作四件套
getattr():取属性,取不到还能兜底
会沿继承链一路找(实例属性 → 类属性 → 父类属性):
class Goods:
brand = "自营"
def intro(self):
return "品质保证"
class EBook(Goods):
def __init__(self, name):
self.name = name
book = EBook("Python 从入门到实践")
print(getattr(book, "name")) # Python 从入门到实践(实例属性)
print(getattr(book, "brand")) # 自营(从父类继承的类属性)
print(getattr(book, "intro")()) # 品质保证(从父类继承的方法)
print(getattr(book, "pages", 300)) # 300(属性不存在,返回默认值)
# 等价于
book.name
book.brand
book.intro()
setattr():设置属性,没有就现场创建
如果继承链上定义了描述符(如 @property.setter)或 __setattr__,同样会按那些机制走:
book = EBook("Python 从入门到实践")
setattr(book, "pages", 300)
print(book.pages) # 300
# 等价于
book.pages = 300
# 属性名可以用字符串变量
key = "rating"
setattr(book, key, 4.8)
print(book.rating) # 4.8
hasattr():有没有这个属性
同样会查整条继承链:
book = EBook("Python 从入门到实践")
print(hasattr(book, "name")) # True(实例属性)
print(hasattr(book, "brand")) # True(继承自 Goods 的类属性)
print(hasattr(book, "intro")) # True(继承自 Goods 的方法)
print(hasattr(book, "pages")) # False(还没设置)
# 常见用法:访问前先探个路
if hasattr(book, "name"):
print(book.name)
delattr():删掉属性
book = EBook("Python 从入门到实践")
setattr(book, "pages", 300)
print(book.pages) # 300
delattr(book, "pages")
# print(book.pages) # AttributeError!
# 等价于
del book.pages
作业
作业一:多继承面试题
不运行代码,写出下面程序的完整打印顺序:
class Foundation(object):
def __init__(self):
print(">> Foundation")
print("<< Foundation")
class Alpha(Foundation):
def __init__(self):
print(">> Alpha")
super().__init__()
print("<< Alpha")
class Beta(Foundation):
def __init__(self):
print(">> Beta")
super().__init__()
print("<< Beta")
class Gamma(Alpha, Beta):
def __init__(self):
print(">> Gamma")
super().__init__()
print("<< Gamma")
g = Gamma()
作业二:综合预测题
不运行代码,逐行写出打印结果:
class Employee:
company = "Acme"
def __init__(self, name):
self.name = name
class Manager(Employee):
headcount = 0
def __init__(self, name, level):
super().__init__(name)
self.level = level
Manager.headcount += 1
def manage(self):
return f"{self.name} 正在安排工作"
m1 = Manager("Alice", 3)
m2 = Manager("Bob", 5)
print(type(m1))
print(type(Manager))
print(isinstance(m1, Employee))
print(isinstance(m1, (int, Manager)))
print(issubclass(Manager, object))
print(m1.__class__.__name__)
print(Manager.__base__.__name__)
print(hasattr(m1, "company"))
print(getattr(m1, "level"))
print(getattr(m2, "desk", "corner"))
setattr(m1, "desk", "standing")
print(m1.desk)
print("manage" in dir(m1))
print(vars(m2))
delattr(m1, "desk")
print(hasattr(m1, "desk"))
print(Manager.headcount)
作业三:实现歌单链表
请实现一个"歌单"类 Playlist,底层用单链表存储歌曲,支持以下操作:
需要实现的方法:
| 方法 | 说明 |
|---|---|
__init__(songs=None) |
初始化空歌单;songs 可以是列表、元组或集合,其中的歌曲会依次初始化为链表节点 |
for_each(callback) |
遍历歌单,对每首歌调用 callback(index, song) |
__str__() |
返回歌单的字符串表示,如 "晴天 -> 七里香 -> 夜曲" |
to_pylist() |
把歌单转成 Python 列表返回 |
add(song) |
在歌单末尾追加一首歌 |
add_first(song) |
在歌单开头插入一首歌 |
insert_at(index, song) |
在指定索引处插入歌曲,索引从 0 开始 |
remove(song) |
删除第一首等于 song 的歌,返回是否删除成功 |
pop_at(index) |
删除指定索引处的歌并返回歌名,索引越界返回 None |
index_of(song) |
查找等于 song 的歌,返回其索引,找不到返回 -1 |
song_at(index) |
获取指定索引处的歌名,索引越界返回 None |
size() |
返回歌单中的歌曲数量 |
is_empty() |
判断歌单是否为空 |
提示: 可以先定义一个 SongNode 类表示链表的节点(存放歌曲和指向下一首歌的引用)。
下一章:07 · 类型判定与抽象基类