Python面向对象编程04-类与实例的方法

0

类方法和静态方法

实例方法

第一节中,便已经介绍过实例方法。实例方法需要有一个实例才能调用,而这个实例是通过第一个通常名为 self 的参数表示的,因此借助该参数可以在定义方法时就去操作一个抽象的实例对象。例如,假设有一个这样的类和实例的定义:

class C:
    def method(self, arg):
        print(self, arg)

c = C()

那么可以使用这样的方式通过实例调用该方法,实例对象将会自动作为第一个位置参数传入:

c.method(arg=...)

许多编程语言在方法内部,对实例的引用是通过 this 关键字完成的;而 Python 则与之不同,实例的引用是通过显式的参数传递的。这样做的好处在于,一是通过显式定义来强调默认定义的方法是实例方法,二是也可以把方法当做第一个参数是实例的函数一样调用:

C.method(c, arg=...)

例如,多个字符串连接默认是通过字符串的 .join() 方法实现,这里调用该方法的字符串实例将被插入被连接的字符串之间:

'||'.join(['a', 'b', 'c'])
# 'a||b||c'

如果觉得用被插入的字符串去调用该方法比较奇怪,也可以通过 str 类调用该方法,并将被插入的字符串作为第一个位置参数主动传入:

str.join('||', ['a', 'b', 'c'])

因此,实例方法的实质就是一个函数,只不过它的主要用途是处理实例。Python 允许实例方法可以直接由实例调用,使得调用的表示更加简洁。

类方法

与实例方法相对应的是类方法。类方法是类本身具有的方法,它可以在不需要实例的情况下使用类本身来调用。

Python 中类方法和实例方法相似,但是它需要传入一个名为 cls 的参数,用来代表这个类本身(同样,这个名字不必是 cls ,可以是 typLei 等)。但是为了与实例方法区分,类方法在定义时需要使用 @classmethod 装饰器。

因此,一个完整的类方法的定义为:

@classmethod
def method_name(cls, ...):
    ...

类方法有几种比较常见的应用场景。首先是构造前交互,有些时候需要在实例化前做一些准备,例如一个代表数据库的类,可以通过类方法读取配置文件并连接到数据库中,这样随后对实例的操作可以实时反映到对数据库的更新中。由于读取配置文件这种操作需要在得到任意一个实例之前完成,因此可以借助类方法实现。

类方法还可以用于以别的方式构建实例。例如,datetime 是一个用于处理日期与时间的标准库,其中 date 是一个用于表示日期的类。如果要通过该类生成一个具体的日期,在初始化时需要传入代表年、月、日的参数:

>>> from datetime import date >>> date(2023, 3, 19) datetime.date(2023, 3, 19)

但有些时候,拿到的信息可能未必包含年月日,它可能是具有特定格式的表示时间的字符串,还有可能是浮点型的时间戳。此时,可以使用 date 类提供的类方法使用这几种信息构造一个日期实例:

>>> date.fromisoformat('2023-03-19') # python3.7+ datetime.date(2023, 3, 19) >>> date.fromtimestamp(time.time()) datetime.date(2023, 3, 19)

仿照这种思路,可以为之前自定义的 Point 类编写一个类方法来通过格式字符串构造实例:

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    @classmethod
    def from_format(cls, format):
        x, y = format[1:-1].split(',')
        return cls(int(x), int(y))  # same as Point(...)

注意,这里的参数 cls 在调用时会替换为 Point ,因此调用 cls(...) 等价于类的实例化 Point(...)

使用这个类方法可以返回一个实例,就像正常方式得到的实例一样可以调用各种方法:

p = Point.from_format('(10,40)')
p.distance_to_origin()  # returns 41.23

从技术上说,也可以用一个普通函数来构造实例。但使用类方法的好处在于,一是使用者容易明白构造出的实例是属于哪个类的,二是当类被重命名后类方法无需跟着修改,降低了因为疏忽导致错误的可能性。

这里顺便说一下,既然实例可以调用类属性,那么实例也可以调用类方法,这等价于用它的类调用类方法。不过一般不推荐利用实例调用类方法,因为这样做可能有些奇怪。

静态方法

除了类方法和实例方法外,还有一个特殊的方法称为静态方法。静态方法不是类或实例所特有的,它可以被任意的类或实例调用。静态方法不涉及对类和实例的操作,因此被称为静态方法。

静态方法需要使用 @staticmethod 装饰器,并且不需要添加像 selfcls 这种特殊的参数,因此不参与对任意属性的修改。

一个静态方法的定义为:

@staticmethod
def method_name(...):
    ...

不难看出,静态方法脱离了类和实例,表现得更像一个普通的函数。


实际上,Python 中的方法和函数除了是否位于类的定义中外,并没有本质的区别。Python 中的类可以看作一个独立的命名空间。在 class 关键字引导的代码块中,可以包含各种语句,例如常见的 for 循环和 with 语句:

class Type:
    s = 0
    for i in range(10):
        s += i ** 2
    with open('settings.txt', 'r') as f:
        settings = dict([
            kv.split('=') for kv in f.read().split('\n')
        ])
    if settings.get('value'):
        @property
        def value(self):
            return self.settings['value']

注意,这里使用 if 语句动态地定义了一个方法,这是完全可以的。除此之外,在类内定义另一个类也是很常见的情况,这个嵌套类一般用于提供基本的元信息。

在类之中这个独立的命名空间内,可以直接访问其中定义的变量(或者说属性)。而在类外部,访问这些变量需要通过类名来标识其所位于的命名空间:

print(Type.s)  # 285
print(Type().value)  # 1

从这个角度上说,类和模块并没有太大的差别。实际上,当模块导入后,它就是一个普通的 Python 对象,只不过是 <class 'module'> 类型的对象。所以在 Python 中使用方法时,实际上就是使用一个函数,Python 解释器只是额外提供了一个语法糖,将调用方法的实例自动填写到函数的 self 参数上,仅此而已。

特殊方法

第一节介绍过实例方法时,同时介绍了一个特殊的实例方法 .__init()__ ,它会在类初始化时自动调用,以此完成一些初始化工作。之前介绍时同时提及了类还有很多类似的特殊方法,它们都以双下划线开头结尾,有时也被称为双下方法。

Python 的特点是一切皆对象,任何 Python 的变量都是一个对象。而 Python 对这些变量的操作,例如构造、初始化、删除、获取属性,甚至比较、索引、和迭代,都是通过调用一些特殊的方法实现的,这些方法称为 Python 的特殊方法(special method)或魔法方法(magic method),它们的名称以双下划线开始,以双下划线结束,代表着对象的一种特殊的操作。一般情况下,使用类方法或实例方法就可以完成类的功能;但在必要时,可以使用或者重写这些特殊方法,来为自己的类提供更加规范的接口。

在后续篇章中,对 Python 面向对象的介绍几乎都围绕这些特殊方法。通过对这些特殊方法的介绍,可以从根本上了解 Python 的运行机制,从而编写出更加优雅、更 Pythonic 的代码。

本节先介绍类的三个特殊方法:.__init__().__new__().__del__() ,它们参与着实例的生命周期。

初始化方法

初始化方法 .__init()__ 是第一个介绍的特殊方法,可能也是最常用的特殊方法。每当生成一个实例后,它便会调用该方法,以此完成一些初始化工作。

需要注意的是,.__init__() 方法不能有任何非 None 的返回值,否则就会引起 TypeError ,错误内容将提示该方法应该返回 None

构造方法

.__new__() 被称为构造方法,因为它负责创建一个新的实例。而 .__init__() 负责创建后的初始化,从中也可以明白它们的用途以及调用的先后顺序。

.__new__() 负责创建类的实例,它的第一个位置通常名为 cls ,代表生成实例的类,其余参数应该是 .__init__() 方法的其余参数。虽然看起来很像一个类方法,但它却是一个静态方法,并且无需使用 @staticmethod 装饰器。这也是特殊方法和普通方法的根本区别:Python 解释器已经规定了如何使用特殊方法,因此没必要额外声明其它东西。

它的返回值是新的对象实例(通常是 cls 的实例)。例如以下类 Card ,它使用 .__new__() 方法,在每次生成一个实例时,便给予它唯一的一个 .id_ 属性:

class Card:
    id_ = 0
    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        cls.id_ += 1
        instance.id_ = cls.id_
        return instance

c01 = Card()
c02 = Card()
c03 = Card()
c03.id_  # 3

注意,虽然它表现得很像一个类方法,但不能像之前介绍的类方法一样通过 cls(...) 来创建实例,因为这样做实质上就是在调用该方法,会陷入无限递归。一般来说对象的创建是底层 C 语言的任务,而 Python 中只需要使用 CPython 提供的接口即可,例如使用 super().__new__(cls, ...) 调用 object 类提供的基本 .__new__() 方法,然后在返回之前根据需要修改新创建的实例即可。

由于涉及到对类属性的修改,因此使用 .__init__() 方法不太好实现(当然不是不能实现,在后续会介绍)。同时使用 .__new__() 方法也方便了继承,子类可以正常编写 .__init__() 方法而不影响 .id_ 的分配。

通过之前的介绍可以明白构造方法和初始化方法的执行顺序:.__new__() 方法会优先 .__init__() 初始化方法在生成实例前调用。执行了 .__new__() 并不一定会进入 .__init__() ;只有 .__new__() 返回了当前类的实例(即便是父类的实例也不行),才会进入 .__init__() 并为该实例做一定初始化,否则没有东西可以用于初始化。

利用这种特性,可以在 Python 中创建单例。单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。单例提供了创建与访问其唯一对象的方式。使用 .__new__() 方法可以自由控制创建的对象,从而比较方便地实现单例:

class Singleton:
    _has_instance = False
    __single_instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._has_instance:
            cls._has_instance = True
            cls.__single_instance = super().__new__(cls)
        return cls.__single_instance
   
one_task = Singleton()
another_task = Singleton(123, name='another')
one_task is another_task is Singleton('abs')  # True

可以看到,不管该类被实例化多少次,如何实例化,得到的都是唯一的实例。单例提供了一种共享数据的方式,并且方便使用 is 判断。但考虑到 Python 能使用全局变量完成相同的工作,因此一般来说在 Python 中没必要使用单例。

析构方法

与构造方法相反,析构方法 .__del__() 用于在销毁一个类实例时调用,它对应的是 Python 中的 del 语句。

当一个对象即将被销毁时,它便会调用 .__del__() 方法,通常该方法用于释放实例用到的一些额外的资源(如关闭已经打开的文件)。

.__del__() 是实例相关的方法,因此第一个参数应该为 self 。注意区别于 property@.deleter 装饰器,它装饰的方法在删除某个属性时调用,而不是整个实例。由于垃圾回收是后台处理的,且 del 是一个语句,因此 .__del__() 的返回值并没有任何意义。并且在该方法内引发任何异常也是没有作用的,它既不能被所处的 try 块捕获,也不会中断程序的运行。

注意,del 语句在调用时,并没有直接调用实例的 .__del__() 方法,这是因为如果变量仅仅是对实例的一个引用,那么 del 语句只会销毁这个引用的对象。只有当所有的引用都被销毁了,那么 del 语句才会调用 .__del__() 方法来清除这个实例。因此,通过在 .__del__() 中为对象添加一个引用可以暂时推迟对象的删除,但这是很不建议的做法。

通过 .__del__() 方法,可以研究 Python 的垃圾回收机制。例如,以下是在命令行中的一个小测试:

>>> GCTest() <__main__.GCTest object at 0x00000282CE8907B8> >>> t = GCTest() >>> del t object deleted >>> 'now delete "_"' object deleted 'now delete "_"'

第一个实例被自动保存在 _ 变量中,只有这个引用改变了,它才会被删除。

参考资料/延伸阅读

https://docs.python.org/3/reference/datamodel.html#basic-customization
Python3 语言参考——数据模型部分,该部分介绍了绝大多数常用的特殊方法、特殊属性与 Python 的运行机制,是深入理解 Python 最好的文档。

京ICP备2021034974号
contact me by hello@frozencandles.fun