实例,类和静态方法-概述
让我们开始编写一个(Python 3)类,其中包含所有三种方法类型的简单示例:
代码语言:javascript复制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
该class MyClass(object):
语法的新样式类,而不是使用简单的声明。除此之外,您还不错。
实例方法
MyClass
调用的第一个方法method
是常规实例方法。这是您大多数时候会使用的基本,简洁的方法类型。您可以看到该方法self
带有一个参数,它指向MyClass
该方法被调用的时间的实例(但是当然实例方法可以接受多个参数)。
通过该self
参数,实例方法可以自由访问同一对象上的属性和其他方法。在修改对象状态时,这赋予了他们很多功能。
实例方法不仅可以修改对象状态,而且还可以通过self.__class__
属性访问类本身。这意味着实例方法也可以修改类状态。
类方法
让我们将其与第二种方法进行比较MyClass.classmethod
。我用@classmethod
装饰器标记了此方法,以将其标记为类方法。
self
类方法不接受参数,而是在调用方法时使用cls
指向类的参数,而不是对象实例。
因为类方法只能访问此cls
参数,所以它不能修改对象实例状态。那将需要访问self
。但是,类方法仍然可以修改适用于该类所有实例的类状态。
静态方法
第三种方法MyClass.staticmethod
用@staticmethod
装饰器标记,以将其标记为静态方法。
这种类型的方法既不带参数self
也不带cls
参数(但是可以自由接受任意数量的其他参数)。
因此,静态方法无法修改对象状态或类状态。静态方法在可以访问哪些数据方面受到限制-它们主要是为方法命名空间的一种方法。
让我们看看他们的行动!
我知道到目前为止,这种讨论还只是理论上的。而且,我相信您必须对这些方法类型在实践中的差异有一个直观的了解。现在,我们将讨论一些具体示例。
让我们看一下这些方法在调用时的行为。我们将从创建该类的实例开始,然后在其上调用三个不同的方法。
MyClass
的设置方式是,每个方法的实现都返回一个元组,其中包含供我们跟踪发生了什么的信息以及该方法可以访问的类或对象的哪些部分。
当我们调用实例方法时,将发生以下情况:
代码语言:javascript复制>>> 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__
。这使实例方法在访问限制方面功能强大-它们可以修改对象实例和类本身的状态。
接下来让我们尝试类方法:
代码语言:javascript复制>>> 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
和得到同样的结果。重要的是它们在该方法的参数列表中排在第一位。
现在该调用静态方法了:
>>>
代码语言:javascript复制>>> obj.staticmethod()
'static method called'
您是否看到我们如何调用staticmethod()
对象并能够成功完成调用?当一些开发人员得知可以在对象实例上调用静态方法时,他们会感到惊讶。
在幕后,Python只是通过使用点语法调用静态方法时不传入self
或cls
参数来简单地强制执行访问限制。
这证实了静态方法既不能访问对象实例状态也不能访问类状态。它们像常规函数一样工作,但属于类(和每个实例的)名称空间。
现在,让我们看看尝试在类本身上调用这些方法时发生的情况-无需事先创建对象实例:
代码语言:javascript复制>>> 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})'
代码语言:javascript复制>>> 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
如果您在现实世界中接触过任何披萨,就会知道有很多美味的选择:
代码语言:javascript复制Pizza(['mozzarella', 'tomatoes'])
Pizza(['mozzarella', 'tomatoes', 'ham', 'mushrooms'])
Pizza(['mozzarella'] * 4)
几个世纪前,意大利人弄清了他们的比萨饼分类法,因此这些美味的比萨饼都有自己的名字。我们会很好地利用这一优势,并为我们Pizza
班级的用户提供一个更好的界面来创建他们渴望的披萨对象。
一个很好的方法是将类方法用作我们可以创建的各种披萨的工厂函数:
代码语言:javascript复制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'])
请注意,我如何cls
在margherita
和prosciutto
工厂方法中使用参数,而不是Pizza
直接调用构造函数。
这是一个技巧,您可以用来遵循“ 不要重蹈覆辙”(DRY)的原则。如果我们决定在某个时候重命名该类,则无需记住在所有类方法工厂函数中都更新构造函数名称。
现在,我们可以用这些工厂方法做什么?让我们尝试一下:
代码语言:javascript复制>>> Pizza.margherita()
Pizza(['mozzarella', 'tomatoes'])
>>> Pizza.prosciutto()
Pizza(['mozzarella', 'tomatoes', 'ham'])
如您所见,我们可以使用工厂函数来创建Pizza
按照所需方式配置的新对象。它们__init__
内部都使用相同的构造函数,并且只是提供了一种用于记住所有各种成分的捷径。
查看类方法使用情况的另一种方法是,它们允许您为类定义替代构造函数。
Python __init__
每个类只允许一个方法。使用类方法,可以根据需要添加尽可能多的替代构造函数。这样可以使您的类的接口自记录(一定程度上)并简化其使用。
何时使用静态方法
在这里想出一个很好的例子要困难一些。但是告诉你,我将继续把比萨的类比越来越薄……(好吃!)
这是我想出的:
代码语言:javascript复制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()
静态方法。
让我们尝试一下!
代码语言:javascript复制import math
>>> p = Pizza(4, ['mozzarella', 'tomatoes'])
>>> p
Pizza(4, ['mozzarella', 'tomatoes'])
>>> p.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669
当然,这只是一个简单的例子,但是它将很好地帮助解释静态方法提供的一些好处。
如我们所知,静态方法无法访问类或实例状态,因为它们不带有cls
或self
参数。这是一个很大的限制-但是这也表明一个特定的方法与周围的其他事物无关,这是一个很好的信号。
在上面的示例中,很明显circle_area()
不能以任何方式修改类或类实例。(当然,您始终可以使用全局变量来解决这个问题,但这不是重点。)
现在,为什么有用?
将方法标记为静态方法不仅暗示方法不会修改类或实例状态,而且该限制也由Python运行时强制实施。
诸如此类的技术使您可以清晰地交流您的类体系结构的各个部分,以便自然而然地指导新开发工作在这些既定范围内进行。当然,克服这些限制将很容易。但是在实践中,它们通常有助于避免意外修改而违反原始设计。
换句话说,使用静态方法和类方法是传达开发人员意图的方法,同时强制执行该意图,以免引起大多数人的误解和破坏设计的错误。
谨慎地应用并且在有意义的情况下,以这种方式编写一些方法可以提供维护优势,并减少其他开发人员错误使用您的类的可能性。
在编写测试代码时,静态方法也有好处。
因为该circle_area()
方法与类的其余部分完全独立,所以测试起来要容易得多。
在单元测试中测试方法之前,我们不必担心建立完整的类实例。我们可以像测试常规函数一样开火。同样,这使将来的维护更加容易。
重要要点
- 实例方法需要一个类实例,并且可以通过访问该实例
self
。 - 类方法不需要类实例。他们无法访问实例(
self
),但是可以通过访问类本身cls
。 - 静态方法无权访问
cls
或self
。它们像常规函数一样工作,但属于类的名称空间。 - 静态方法和类方法进行通信,并(在一定程度上)强制开发人员进行有关类设计的意图。这可以带来维护优势。