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
装饰器将其标记为静态方法。
这种类型的方法不接受self
或cls
参数,但可以接受任意数量的其他参数。
因此,静态方法不能修改对象状态或类状态,仅能访问特定的数据,主要用于声明属于某个命名空间的方法。
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
参数的工作方式也是如此。
注意,self
和cls
这些参数的命名只是一个约定。你可以将其命名为the_object
和the_class
,结果相同,只要这些参数位于相关方法中参数列表的第一个位置即可。
现在来调用静态方法:
>>> obj.staticmethod()
'static method called'
注意到没有,在对象上可以调用staticmethod()
。有些开发人员在得知可以在对象实例上调用静态方法时会感到惊讶。
从实现上来说,Python在使用点语法调用静态方法时不会传入self
或cls
参数,从而限制了静态方法访问的内容。
这意味着静态方法既不能访问对象实例状态,也不能访问类的状态。静态方法与普通函数一样,但属于类(和每个实例)的名称空间。
现在不创建对象实例,看看在类本身上调用静态方法时会发生什么:
>>> 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 实例方法、类方法和静态方法 使用@classmethod
的Pizza
工厂类
如果你在现实世界中吃过比萨,那么就会知道比萨有很多种口味可供选择:
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'])
注意我们在margherita
和prosciutto
工厂方法中使用了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
当然这仍然是一个简单的例子,不过有助于说明静态方法的好处。
之前已经介绍了,静态方法不能访问类或实例的状态,因为静态方法不会接受cls
或self
参数。这是一个很大的局限性,但也很好地表明了静态方法与类的其他所有内容都无关。
在上面的例子中,很明显circle_area()
不能以任何方式修改类或类实例。(当然,你可以用全局变量来解决这个问题,不过这不是重点。)
那么这种功能有什么用呢?
将方法标记为静态方法不仅是一种提示,告诉大家这个方法不会修改类或实例状态,而且从上面可以看到,Python运行时也会实际落实这些限制。
通过这样的技术可以清晰地识别出类架构的各个部分,因而新的开发工作能够很自然地分配到对应的部分中。虽然不遵守这种限制也没什么大问题,但在实践中常常能避免与原始设计相悖的意外修改。
换句话说,使用静态方法和类方法不仅能传达开发人员的意图,还能够强制贯彻设计思路,避免许多心不在焉的错误以及会破坏设计的bug。
因此请谨慎地按需使用静态方法,添加静态方法对代码维护有好处,能避免其他开发人员误用你的类。
静态方法也有助于编写测试代码。由于circle_area()
方法与类的其余部分完全独立,因此测试起来更加容易。
在单元测试中测试静态方法时不需要建立完整的类实例,可以像测试普通函数那样直接测试静态方法。这不仅简化了维护,而且在面向对象和面向过程的编程风格之间建立了联系。
关键要点
-
实例方法需要一个类实例,可以通过
self
访问实例。 -
类方法不需要类实例,不能访问实例(
self
),但可以通过cls
访问类本身。 -
静态方法不能访问
cls
或self
,其作用和普通函数相同,但属于类的名称空间。 -
静态方法和类方法能(在一定程度上)展示和贯彻开发人员对类的设计意图,有助于代码维护。