首页/Python 与数据实践/类的定义与实例化
Python 与数据实践 · 第 06 章

类的定义与实例化

Python 是一门彻头彻尾的面向对象语言。所谓类(Class),可以理解成一张图纸;所谓对象(Object),就是照着图纸造出来的零件。图纸画一次,零件可以造无数个,每个零件身上带着自己的数据(属性),也带着自己的行为…

约 24 分钟9,737 字26 个代码示例

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 · 类型判定与抽象基类