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

Python 实例方法、类方法和静态方法,将深入探寻Python中的类方法静态方法和普通实例方法

在对这些方法之间的差异有直观的理解后,就能以面向对象的形式编写Python代码了,从而更清楚地传达代码的意图,而且从长远来看代码更易维护。

首先来编写一个类,其中包含这三种方法的简单示例(Python 3版):

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'

Python 2用户需要注意:从Python 2.4开始才可以使用@staticmethod@classmethod装饰器,因此此后的版本才能运行这个示例。另外,还需要使用class MyClass(object)这种语法来声明这是继承自object的新式类,而不是使用普通的class MyClass语法。除了这些之外就没有其他问题了。

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

MyClass上的第一种方法名为method,这是一个普通的实例方法。代码中一般出现的都是这种简单基础的实例方法。method方法需要一个参数self,在调用时指向MyClass的一个实例。当然,实例方法可以接受多个参数。

实例方法通过self参数在同一个对象上自由访问该对象的其他属性和方法,因此特别适合修改对象的状态。
实例方法不仅可以修改对象状态,也可以通过self.__class__属性访问类本身。这意味着实例方法也可以修改类的状态。

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

与第一种方法相比,第二种方法MyClass.classmethod使用了@classmethod装饰器,将其标记为类方法

类方法并不接受self参数,而是在调用方法时使用cls参数指向类(不是对象实例)。

由于类方法只能访问这个cls参数,因此无法修改对象实例的状态,这需要用到self。但类方法可以修改应用于类所有实例的类状态。

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

第三种方法MyClass.staticmethod使用@staticmethod装饰器将其标记为静态方法

这种类型的方法不接受selfcls参数,但可以接受任意数量的其他参数。

因此,静态方法不能修改对象状态或类状态,仅能访问特定的数据,主要用于声明属于某个命名空间的方法。

Python 实例方法、类方法和静态方法 在实践中探寻

到目前为止都是非常理论化的讨论,而重要的是在实践中直观地理解这些方法之间的区别,因此这里来介绍一些具体的例子。

让我们来看看调用这些方法时其各自的行为。首先创建一个类的实例,然后调用三种不同的方法。

MyClass中进行了一些设置,其中每个方法的实现都会返回一个元组,包含当前方法的说明信息和该方法可访问的类或对象的内容。

以下是调用实例方法时的情况:

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

从中可以确认,名为method的实例方法可以通过self参数访问对象实例(输出为<MyClass instance>)。

调用该方法时,Python用实例对象obj替换self变量。如果不用obj.method()这种点号调用语法糖,手动传递实例对象也会获得相同的结果:

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

顺便说一下,在实例方法中也可以通过self.__class__属性访问类本身。这使得实例方法在访问方面几乎没什么限制,可以自由修改对象实例和类本身的状态。
接下来尝试一下类方法

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

调用classmethod()的结果表明其不能访问<MyClass instance>对象,只能访问<class MyClass>对象,这个对象用来表示类本身(Python中一切皆为对象,类本身也是对象)。

注意,在调用MyClass.classmethod()时,Python自动将类作为第一个参数传递给该函数。在Python中用点语法(dot syntax)调用该方法就会触发这个行为。实例方法的self参数的工作方式也是如此。

注意,selfcls这些参数的命名只是一个约定。你可以将其命名为the_objectthe_class,结果相同,只要这些参数位于相关方法中参数列表的第一个位置即可。

现在来调用静态方法

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

注意到没有,在对象上可以调用staticmethod()。有些开发人员在得知可以在对象实例上调用静态方法时会感到惊讶。

从实现上来说,Python在使用点语法调用静态方法时不会传入selfcls参数,从而限制了静态方法访问的内容。

这意味着静态方法既不能访问对象实例状态,也不能访问类的状态。静态方法与普通函数一样,但属于类(和每个实例)的名称空间。

现在不创建对象实例,看看在类本身上调用静态方法时会发生什么:

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

>>> 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

这是预料之中的。由于没有创建对象实例,而是直接在类蓝图(blueprint)上调用实例方法,意味着Python无法填充self参数,因此调用实例方法method会失败并抛出TypeError异常。

通过这些实验,你应该更清楚这三种方法类型之间的区别了。别担心,现在还不会结束这个话题。在接下来的两节中,还将用两个更接近实际的例子来使用这些特殊方法。
下面以前面的例子为基础,创建一个简单的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'])

Python 实例方法、类方法和静态方法 使用@classmethodPizza工厂类

如果你在现实世界中吃过比萨,那么就会知道比萨有很多种口味可供选择:

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

几个世纪以前,意大利人就对比萨进行了分类,所以这些美味的比萨饼都有自己的名字。下面根据这个特性为Pizza类提供更好的接口,让用户能创建所需的比萨对象。

使用类方法作为工厂函数能够简单方便地创建不同种类的比萨:

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 实例方法、类方法和静态方法 什么时候使用静态方法

为这个主题提供一个好例子有点难,所以继续使用前面的比萨例子,把比萨烤得越来越薄……(要流口水了!)
下面是我想到的:

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__以接受额外的radius参数。

其次,添加了一个area()实例方法用于计算并返回比萨的面积。虽然这里更适合使用@property装饰器,不过对于这个简单的示例来说,那么做的话就有些大动干戈了。

area()并没有直接计算面积,而是调用circle_area()静态方法,后者使用众所周知的圆面积公式来计算。

下面来试试吧!

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

当然这仍然是一个简单的例子,不过有助于说明静态方法的好处。

之前已经介绍了,静态方法不能访问类或实例的状态,因为静态方法不会接受clsself参数。这是一个很大的局限性,但也很好地表明了静态方法与类的其他所有内容都无关。

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

那么这种功能有什么用呢?

将方法标记为静态方法不仅是一种提示,告诉大家这个方法不会修改类或实例状态,而且从上面可以看到,Python运行时也会实际落实这些限制。

通过这样的技术可以清晰地识别出类架构的各个部分,因而新的开发工作能够很自然地分配到对应的部分中。虽然不遵守这种限制也没什么大问题,但在实践中常常能避免与原始设计相悖的意外修改。

换句话说,使用静态方法和类方法不仅能传达开发人员的意图,还能够强制贯彻设计思路,避免许多心不在焉的错误以及会破坏设计的bug。

因此请谨慎地按需使用静态方法,添加静态方法对代码维护有好处,能避免其他开发人员误用你的类。

静态方法也有助于编写测试代码。由于circle_area()方法与类的其余部分完全独立,因此测试起来更加容易。
在单元测试中测试静态方法时不需要建立完整的类实例,可以像测试普通函数那样直接测试静态方法。这不仅简化了维护,而且在面向对象和面向过程的编程风格之间建立了联系。

关键要点

  • 实例方法需要一个类实例,可以通过self访问实例。

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

  • 静态方法不能访问clsself,其作用和普通函数相同,但属于类的名称空间。

  • 静态方法和类方法能(在一定程度上)展示和贯彻开发人员对类的设计意图,有助于代码维护。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程