Python的实例、类和静态方法被揭开神秘面纱

发布于:2021-02-01 13:56:20

0

68

0

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用户:@staticmethod@classmethod修饰符从Python 2.4开始就可以使用了,这个示例将起作用照原样。您可以选择使用class MyClass(object):语法声明从object继承的新样式类,而不是使用普通的class MyClass:声明。除此之外,您还可以继续使用

实例方法

MyClass调用的第一个方法method是常规实例方法。这是您大多数时候会使用的基本,简洁的方法类型。您可以看到该方法self带有一个参数,它指向MyClass该方法何时被调用的实例(但当然实例方法可以接受多个参数)。

通过该self参数,实例方法可以自由访问同一对象上的属性和其他方法。在修改对象的状态时,这赋予了他们很多功能。

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

类方法

让我们将其与第二种方法进行比较MyClass.classmethod。我用@classmethod装饰器标记了此方法,以将其标记为类方法。

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

因为类方法只能访问此cls参数,所以它不能修改对象实例状态。那将需要访问self。但是,类方法仍然可以修改适用于该类所有实例的类状态。

静态方法

第三种方法,MyClass.staticmethod被标记为一个@staticmethod装饰器,以将其标记为一个静态方法。

这种类型的方法既不接受self也不接受cls参数(当然它可以自由接受任意数量的其他参数)。

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

让我们看看他们的行动!

我知道到目前为止,这一讨论还只是理论上的。而且,我相信您必须对这些方法类型在实践中的差异有一个直观的了解。现在,我们将讨论一些具体示例。

让我们看一下这些方法在调用时的行为。我们将从创建该类的实例开始,然后在其上调用三个不同的方法。

MyClass 的设置方式是,每个方法的实现都返回一个元组,其中包含供我们跟踪发生了什么的信息以及该方法可以访问的类或对象的哪些部分。

当我们调用实例方法时,会发生以下情况:

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

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

调用该方法时,Python会将self参数替换为实例对象obj。我们可以忽略点调用语法(obj.method())的语法糖,并手动传递实例对象以获得相同的结果:

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

您能猜出如果不先创建实例就尝试调用该方法会发生什么情况吗?

顺便说一句,实例方法还可以通过属性访问类本身self.__class__。这使实例方法在访问限制方面功能强大-它们可以修改对象实例和类本身的状态。

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

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

调用classmethod()显示了它无权访问该<MyClass instance>对象,而只能访问<class MyClass>代表该类本身的对象(Python中的所有内容都是一个对象,甚至是类本身)。

请注意,当我们调用时,Python如何自动将类作为第一个参数传递给函数MyClass.classmethod()。通过点语法在Python中调用方法会触发此行为。self实例方法上的参数以相同的方式工作。

请注意,命名这些参数self而cls仅仅是一个惯例。你可以很容易地为它们命名the_object和the_class和得到同样的结果。重要的是它们在该方法的参数列表中排在第一位。

现在该调用静态方法了:

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

您是否看到我们如何调用staticmethod()对象并能够成功完成调用?当一些开发人员得知可以在对象实例上调用静态方法时,他们会感到惊讶。

在幕后,Python只是通过使用点语法调用静态方法时不传递self或cls参数来简单地强制执行访问限制。

这证实了静态方法既不能访问对象实例状态也不能访问类状态。它们像常规函数一样工作,但属于类(和每个实例的)名称空间。

现在,让我们看看尝试在类本身上调用这些方法时发生的情况-无需事先创建对象实例:

>>> 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'])

注意:此代码示例以及本教程中的后续代码示例使用Python 3.6 f-strings构造由返回的字符串__repr__。在Python 2和3.6之前的Python 3版本上,您将使用不同的字符串格式表达式,例如:

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

美味比萨饼工厂与@classmethod

如果你在现实世界中接触过比萨饼,你会知道有许多美味的变化可用:

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

意大利人几个世纪前就想出了他们的比萨饼分类法,所以这些美味的比萨饼都有自己的名字。我们最好利用这个优势,为Pizza类的用户提供一个更好的界面来创建他们想要的pizza对象。

一个好的、干净的方法是使用类方法作为工厂函数来创建不同种类的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__方法。使用类方法可以根据需要添加任意多的可选构造函数。这可以使类的接口自文档化(在一定程度上),并简化它们的使用。

什么时候使用静态方法

这里要想出一个好的例子有点困难。但是告诉你,我会继续把比萨饼拉得越来越薄…(好吃!)这是我想到的:

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, ['mozzarella', 'tomatoes'])
>>> p.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669

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

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

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

现在,为什么这很有用?

将一个方法标记为静态方法不仅仅意味着一个方法不会修改类或实例状态-Python运行时也会强制执行此限制。

类似的技术允许您清楚地交流类体系结构的各个部分,以便新的开发工作自然地在这些体系结构中进行设置边界。当然,挑战这些限制是很容易的。但在实践中,它们通常有助于避免意外的修改违背原始设计。

换言之,使用静态方法和类方法是传达开发人员意图的方法,同时强制执行该意图,以避免大多数会破坏设计的思维失误和错误。

谨慎地应用用这种方式编写某些方法可以提供维护好处,减少其他开发人员错误使用您的类的可能性,这是有道理的。

在编写测试代码时,静态方法也有好处。

因为circle_area()方法完全独立于类的其他部分,所以它非常重要更容易测试。

在单元测试中测试方法之前,我们不必担心设置一个完整的类实例。我们可以像测试常规函数一样发射出去。同样,这使以后的维护更容易。

关键要点

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

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

  • 静态方法无权访问cls或self。它们像常规函数一样工作,但属于类的名称空间。

  • 静态方法和类方法进行通信,并(在一定程度上)强制开发人员进行有关类设计的意图。这可以带来维护优势。