Skip to content

类对象 ​

上一节我们学习了实例对象,每个实例都可以保存自己的数据。

但有些数据属于整个类,比如所有游戏角色的默认阵营、已经创建了多少个角色。这些数据不适合在每个实例中重复保存,而应该交给类统一管理。哪些数据应该由类共享?如果一个函数只和类有关、与具体实例无关,又应该放在哪里?

这一节换一个角度看类本身:类不只是创建对象的模板,类自己也是对象。本节还会继续区分类属性、类方法、静态方法和私有成员的使用边界。

类对象与实例对象 ​

类的继承基础

在 Python 里,自定义类最终都会继承自 object。

例如:

python
class Person:
    pass

虽然这里没有显式写出 object,但 Person 仍然继承自 object。

可以简单理解为:object 是 Python 类继承体系中最基础的类。

实例化过程

前面我们已经学过创建实例对象。

这里再把顺序串一下。

使用类创建对象,一般按下面的顺序进行:

  1. 先设计类
  2. 再通过 类名() 创建对象
  3. 创建对象时,自动执行初始化逻辑

当我们写:

python
zx = Person()

从当前阶段看,可以先把背后的过程理解成两步:

  1. 先在内存里给对象分配空间
  2. 再调用 __init__() 初始化对象

所以:

  • 创建出来的对象,叫实例对象
  • 创建对象这个动作,叫实例化
  • 每个对象各自保存的属性,叫实例属性
  • 定义在类中、由实例对象调用的方法,叫实例方法

类对象

这里要记住一句很重要的话:

Python 里一切皆对象。

所以:

  • class AAA: 定义出来的是类对象
  • obj1 = AAA() 创建出来的是实例对象

程序执行类定义后,会在内存中创建对应的类对象。一个类对象可以继续创建很多个实例对象。

类对象与实例对象的关系

类属性 ​

当多个实例需要共享同一份数据时,应该把它放在实例属性中,还是放在类属性中?除了实例属性,类本身还可以有自己的属性。

这种属性就叫类属性。

类属性是所有实例对象共享的。

它更适合保存“这一类事物共同拥有的信息”。

例如:

python
class Person:
    address = '地球'

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def hello(self):
        return f'{self.name} 正在打招呼'


print(Person.address)
zx = Person('正心', 18)
print(zx.address)
zx.address = '外太空'
print(zx.address)
print(Person.address)

这里要注意:

  1. Person.address 是类属性
  2. 实例对象也可以访问类属性
  3. 给 zx.address 赋值,会在实例对象上新增一个同名属性

查找 zx.address 时,Python 会先找实例属性;如果实例中没有,再去找类属性。

因此,zx.address = '外太空' 只是让实例属性遮住了同名类属性,并没有修改 Person.address。最后一次输出仍然是 地球。

类属性与实例属性的查找和遮蔽

类属性的适用范围

一般适合放:

  1. 所有对象共享的数据
  2. 与“这一类”有关,而不是与“某一个实例”有关的数据

比如:

  • 人类的居住地
  • 游戏角色的默认阵营
  • 某个类的计数器

类方法 ​

如果一个操作只与类有关,不需要某个具体实例的数据,就可以定义成类方法。

类方法可以直接访问类属性,也可以调用其他类方法。

下面给 Person 定义一个类方法,用来读取类属性:

python
class Person:
    address = '地球'

    @classmethod
    def show_address(cls):
        print(f'人类居住在{cls.address}')


Person.show_address()

这里要注意几个点:

  1. 类方法前面要写 @classmethod
  2. 第一个参数通常写成 cls
  3. cls 指的是当前这个类本身

和实例方法里的 self 类似:

  • self 指对象
  • cls 指类

类方法通常通过 类名. 调用。调用 Person.show_address() 时,Python 会自动把 Person 类传给 cls。

在类方法内部,可以用 cls. 访问类属性和类方法。

实例方法、类方法和静态方法的对比

静态方法 ​

有时候,一个函数与某个类有关系,但既不需要实例属性,也不需要类属性。此时可以把它放进类中,定义为静态方法。

但这个函数:

  1. 不需要访问实例属性
  2. 不需要访问类属性

这时就可以写成静态方法。例如,判断年龄是否有效只需要使用传入的参数,不需要访问某个实例或类属性:

python
class Person:
    @staticmethod
    def check_age(age):
        return 0 <= age <= 120


print(Person.check_age(18))
print(Person.check_age(150))

静态方法前面要写 @staticmethod。

它既不需要 self,也不需要 cls。

静态方法通常也通过 类名. 调用。可以先把它理解成一个与当前类有关、放在类中管理的普通函数。

私有成员 ​

如果某些数据和方法只应该由类的内部使用,就不能把它们都直接暴露给外部。在面向对象里,这类成员可以使用私有命名约定。

有些属性和方法,只想让类的内部自己用。

这时就可以使用私有成员。

Python 里,以双下划线 __ 开头、但不以双下划线结尾的属性或方法,通常只供类的内部使用。

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    def __get_age(self):
        return self.__age

    def introduce(self):
        print(f'我叫{self.name},今年{self.__get_age()}岁')


zx = Person('正心', 18)
zx.introduce()

外部可以正常调用公开方法 introduce(),再由它访问私有属性和私有方法。直接访问 zx.__age 或调用 zx.__get_age(),都会触发 AttributeError。

私有成员的适用范围

适合这种情况:

  1. 某些属性不希望被外部直接改
  2. 某些方法只是内部实现细节
  3. 不想让外部随便调用

名称改写边界

双下划线开头的名字会触发名称改写,但这不是绝对的安全机制。

基础阶段先记住一点就够了:

双下划线开头的成员应该留在类的内部使用,对外操作由公开方法提供。

私有成员的公开访问边界

小结 ​

本节重点记住五件事:

  1. 类本身也是对象
  2. 类属性属于类,实例对象共享
  3. 类方法用 @classmethod 定义,第一个参数通常是 cls
  4. 静态方法用 @staticmethod 定义
  5. 以双下划线开头、但不以双下划线结尾的属性和方法,通常表示私有成员

类成员体系架构与设计选型决策指南

练习 ​

  1. 定义一个 Hero 类,添加一个类属性,比如 camp = '蜀国'
  2. 定义一个类方法,打印这个类属性
  3. 再定义一个静态方法,输出一段固定提示文字
  4. 给类增加一个私有属性,观察外部直接访问时的表现