解密 Python 实例、类方法和静态方法

解密 Python 实例、类方法和静态方法
0

原文:https://realpython.com/instance-class-and-static-methods-demystified/
译者:老齐,研途教育科技有限公司 CTO,Python 畅销书作者,出版书籍有《跟老齐学 Python:轻松入门》、《跟老齐学 Python:Django 实战》、《跟老齐学 Python:数据分析》、《Python 大学实用教程》(大学教材),并在苏州大学数科院为应用统计专业硕士生开设《机器学习实践》课程。

译者注: 本文适合于已经具有 Python 基础知识的读者阅读,对于初学者而言,应该已经学习过了《跟老齐学 Python:轻松入门》或者《Python 大学实用教程》中有关类的章节之后,再阅读本文会有更多收获。

在本文中,我将帮助你揭开类方法、静态方法和常规实例方法背后的奥秘。

如果对它们的差异有了直观的理解,那么你将能够编写面向对象的 Python 程序,以便更清楚地传达某种意图,并且从长远来看更易于维护。

实例、类方法和静态方法概述

我们首先编写一个类,其中包含实例、类方法和静态方法:

class MyClass:
    def method(self):
        return 'instance method called', self

    @classmethod
    def classmethod(cls):
        return 'class method called', cls

    @staticmethod
    def staticmethod():
        return 'static method called'

实例方法

MyClass 的第一个方法叫做“方法”,它是一个普通的实例方法,也是你将会在大多数情况下使用的最基本、最简单的方法。该方法接受一个参数 self,它在调用该方法时指向 MyClass 的一个实例(当然,实例方法可以接受的参数不止一个)。

通过 self 参数,实例方法可以自由地访问同一对象上的属性和其他方法。当涉及到修改对象的状态时,实例方法就非常给力。

实例方法不仅可以修改对象状态,还可以通过 self.__class__ 属性来访问类本身,这就意味着实例方法也可以修改类的状态。

类方法

比较 MyClass.method 与第二个方法 MyClass.classmethod 。我用一个 @classmethod 装饰符把这个方法标记为类方法。

类方法不接受 self 参数,而是接受一个 cls 参数。 cls 参数在调用方法时指向的是类而不是对象实例。

由于类方法只能访问此 cls 参数,因此它无法修改对象实例的状态——那需要访问 self 。但是,类方法仍然可以对应用于类的所有实例的类状态进行修改。

静态方法

第三个方法 MyClass.staticmethod 用一个装饰符 @staticmethod 标记为静态方法。

这个方法既不接受 self 参数,也不接受 cls 参数(但是,它可以接受任意数量的其他参数)。

因此静态方法既不能修改对象状态,也不能修改类状态。静态方法在它们可以访问的数据方面受到限制,它们主要用于对你的方法进行空间命名。

见证它们的作用!

我知道到目前为止,这个讨论是相当理论化的。我相信:对这些方法在实践中的差异有一个直观的理解。我们现在来复习一些具体的例子。

让我们来看看这些方法在被调用时是如何运行的。我们从创建类的实例开始,然后对其调用三个不同的方法。

MyClass 的每个方法,都返回一个元组,元组包含踪迹信息,以及该方法可以访问的类或对象的有关信息。

下面是调用实例方法时发生的情况:

>>> obj = MyClass()
>>> obj.method()
('instance method called', <MyClass instance at 0x101a2f4c8>)

这些代码确认了 method (实例方法)可以通过 self 参数访问对象实例(打印为 <MyClass instance> )。

此方法被调用时,Python 用实例对象 obj 替换了 self 参数。我们可以用句点“.”调用方法的方式( obj.method() ),手动传递实例对象来获得相同的结果:

>>> MyClass.method(obj)
('instance method called', <MyClass instance at 0x101a2f4c8>)

你能猜到在不创建实例的情况下调用该方法会怎么样吗?

顺便说一下,实例方法还可以通过 self.__class__ 属性访问类本身。这使得实例方法在访问受限时非常给力——它们可以修改对象实例和类本身的状态。

接下来让我们尝试一下类方法:

>>> obj.classmethod()
('class method called', <class MyClass at 0x101a2f4c8>)

调用 classmethod() 向我们展示了它不能访问 <MyClass instance> 对象,而是只能访问代表类本身的 <class MyClass> 对象(在 Python中,一切皆对象。即使是类本身也不列外)。

请注意,当我们调用 MyClass.classmethod() 时,Python 如何自动将类作为第一个参数传递给函数,通过句点“.”的方式自动实现这种行为,这与实例方法上的 self 参数的工作方式相同。

将这些参数命名为 selfcls 只是一种约定。你可以将它们命名为 the_objectthe_class ,也能得到相同的结果。重要的是,它们在方法的参数列表中处于第一个位置。

现在到了调用静态方法的时候:

>>> obj.staticmethod()
'static method called'

你看到我们如何成功地在对象上调用 staticmethod() 了吗?某些开发者看到通过实例调用静态方法时会感到惊讶。

在后台,当使用句点“.”的方式调用静态方法时,Python 只是通过不传入 selfcls 参数来执行访问限制。

这就确保静态方法既不能访问对象实例状态,也不能访问类状态。静态方法像普通函数一样工作,但它属于类(和每个实例)的命名空间。

现在,让我们看一看:如果没有预先创建对象实例,就在类本身上调用静态方法时,会发生什么情况:

>>> MyClass.classmethod()
('class method called', <class MyClass at 0x101a2f4c8>)

>>> MyClass.staticmethod()
'static method called'

>>> MyClass.method()
TypeError: unbound method method() must
    be called with MyClass instance as first
    argument (got nothing instead)

我们可以顺利地调用 classmethod()staticmethod() ,但调用实例方法 method() 时,显示 TypeError 异常。

这是意料之中的。这次我们没有创建实例,而是尝试直接通过在类本身调用实例方法。这意味着 Python 无法填充 self 参数,因此调用失败。

由上可见这三种方法之间的区别,但我不会就此罢休,在接下来的两部分中,我将介绍两个稍微实际一些的示例,以便大家了解何时使用这些特殊的方法类型。

以这个简单的 Pizza 类为例:

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __repr__(self):
        return f'Pizza({self.ingredients!r})'

>>> Pizza(['cheese', 'tomatoes'])
Pizza(['cheese', 'tomatoes'])

注意:本文中的代码示例使用 Python3.6 的 f 字符串来实现 __repr__ 方法中返回的字符串。在 Python2 和 3.6 之前的 Python3 版本中,你将使用不同的格式化字符串表达式,例如:

def __repr__(self):
        return 'Pizza(%r)' % self.ingredients

带有 @classmethod 的披萨工厂

如果你在现实世界中接触过披萨,你就会知道有很多不同的美味:

Pizza(['mozzarella', 'tomatoes'])
Pizza(['mozzarella', 'tomatoes', 'ham', 'mushrooms'])
Pizza(['mozzarella'] * 4)

几个世纪前,意大利人就弄清了比萨饼的分类,所以这些不同口味的比萨饼都有自己的名字。我们最好利用这一点,为 Pizza 类的用户提供更好的接口,以便于创建他们所渴望的 Pizza 对象。

一个好用的、整洁的方法是使用类方法作为工厂函数来存放我们可以创建的不同类型的比萨饼:

译者注: 在原文中,作者将类方法称为“工厂函数”。所谓工厂函数,是一个比喻说法。比如在现在的代码中,可以通过类方法 margherita 创建关于类 Pizza 的实例。这就好比 margherita 是一个“生产比萨的工厂”,故名之曰:工厂函数。

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __repr__(self):
        return f'Pizza({self.ingredients!r})'

    @classmethod
    def margherita(cls):
        return cls(['mozzarella', 'tomatoes'])

    @classmethod
    def prosciutto(cls):
        return cls(['mozzarella', 'tomatoes', 'ham'])

请注意看我是如何在 margheritaprosciutto 方法中使用 cls 参数,而不是直接调用 Pizza 类生成实例对象。

这是一个小窍门,你可以遵循“不要自我重复(DRY)”的原则。如果我们决定在某个时候重命名这个类,我们就不必在所有的类方法中更新类名称。

现在,能用这些方法做什么?让我们试试看:

>>> Pizza.margherita()
Pizza(['mozzarella', 'tomatoes'])

>>> Pizza.prosciutto()
Pizza(['mozzarella', 'tomatoes', 'ham'])

如你所见,我们可以使用工厂函数来创建新的 Pizza 对象,这些对象是按照我们希望的方式配置的。它们都在内部使用相同的 __init__ 初始化方法,并且仅仅提供了一个快捷方式来记住所有不同的成分。

查看类方法的这种用途的另一种途径是:它们允许为你的类定义替代性的初始化方法。

Python 只允许每个类使用一个 __init__ 方法。使用类方法,有可能根据需要添加尽可能多的替代性的初始化方法。这可以(在一定程度上)使类的接口自文档化,并简化它们的使用。

译者注: 对于上述内容的进一步理解,可以参考《Python大学实用教程》6.4.2节中的例题6-4-1。

什么时候使用静态方法

在这里想出一个好的例子有点困难,但告诉你吧,我会继续把“比萨饼”抻得越来越薄…

我想到的是:

import math

class Pizza:
    def __init__(self, radius, ingredients):
        self.radius = radius
        self.ingredients = ingredients

    def __repr__(self):
        return (f'Pizza({self.radius!r}, '
                f'{self.ingredients!r})')

    def area(self):
        return self.circle_area(self.radius)

    @staticmethod
    def circle_area(r):
        return r ** 2 * math.pi

我在这里做了哪些更改?首先,我修改了初始化方法和 __repr__ 方法,以便接受另外一个关于半径的参数。

译者注: 原文作者没有区分类中的“初始化方法”( __init__ )和“构造方法”( __new__ )。原文中将 __init__ 称为constructor,这种说法欠妥当。对此,在《Python大学实用教程》的“6.9构造方法”一节中有详细说明。

我还添加了一个 area() 实例方法来计算并返回比萨饼的面积(这对于 @property 也是一个很好的候选项——但是,这只是一个虚构的示例)。

我没有直接在 area() 中计算面积,而是使用众所周知的圆面积公式,将其分解为独立的 circle_area() 静态方法。

我们试试吧!

>>> p = Pizza(4, ['mozzarella', 'tomatoes'])
>>> p
Pizza(4, ['mozzarella', 'tomatoes'])
>>> p.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669

当然,这是一个有点简陋的例子,但是它可以帮助解释静态方法所带来的一些好处。

正如我们所了解到的,静态方法不能访问类或实例状态,因为它们不接受 clsself 参数。这是一个很大的限制,但这也是一个很好的信号,表明一个特定的方法独立于它周围的一切。

在上面的例子中, circle_area() 显然不能以任何方式修改类或类实例。(当然,你总是可以用全局变量来解决这个问题,但这不是重点。)

静态方法为什么有用呢?

将一个方法标记为静态方法,不仅意味着这个方法不会修改类或实例状态,而且这种强制性要求在执行 Python 程序时还会强化。

这样的技术允许你明确地对类的各个部分进行调用,从而自然地引导新的开发工作在规定的范围内展开。当然,挑战这些限制是很容易的,但在实践中,它们常常有助于避免与原始设计相反的意外修改。

换言之,使用静态方法和类方法是传达开发者意图的方法,同时又能充分执行这种意图,以避免由于疏忽而造成的大多数的错误和 bug,因为这些 bug 可能会破坏原有的设计。

如果不将静态方法滥用,而是应用得恰到好处,那么所编写的程序则具有良好的可维护性,并降低了其他开发者犯错误的可能性。

静态方法对编写测试代码也有用处。

因为 circle_area() 方法完全独立于类的其他部分,所以测试起来容易得多。

在单元测试中测试这种方法之前,我们不必担心创建一个完整的实例,可以像测试常规函数一样不停地测试。同样,这使得以后的维护更加容易。

重要收获

实例方法需要实例,并且可以通过 self 访问该实例。

类方法不需要类实例,不能访问实例 ( self ) ,但是可以通过 cls 访问类本身。

静态方法无权访问 clsself ,可以像普通函数一样工作,但属于类的命名空间。

静态方法和类方法便于调用,并(在一定程度上)限制了开发者对类的设计意图,这有益于代码的维护。

谢谢分享!

最近正在学习 Python,谢谢分享啊