8.4 方法
类的方法,其基本结构与第7章中学过的函数近似,就普通的方法而言,仿照函数编写即可。然而类里面还会有一些不普通的方法,比如本节将要介绍的“类方法”和“静态方法”。这些方法都是为了让程序更简洁、紧凑而创立的。如果不使用这些方法,也能编写程序,但是用了它们,则锦上添花。
8.4.1 比较方法和函数
函数和方法有很多相似处,比如都是使用 def
关键词来定义,除了某些特殊方法外( __init__()
初始化方法 ),普通方法和函数一样,都使用 return 语句作为结束(也可以说,所有方法都以 return
语句结束,但是 __init__()
中的是 return None
)。
相同之处容易掌握,区别要特别关注。
函数是由函数名引用的一个独立对象(第一类对象),通过函数名称可以调用这个对象,它不依赖于其他东西。
代码语言:javascript复制>>> def func(x): return x 7
...
>>> func
<function func at 0x7ff148d5f430>
>>> type(func)
<class 'function'>
>>> func(4)
11
在调用函数的时候,如果函数有参数,必须很明确地(或者说是“显式地”)给每个参数提供对象或引用——一个参数也不能少。
而方法,必须要依赖于对象。因为它写在了类里面,如果要调用它,就要使用某个对象。前面已经学习过的知识是使用类的实例对象调用它,即通过实例名称:
代码语言:javascript复制>>> class Foo:
... def my_method(self, x):
... return x ** 2
...
>>> f = Foo()
>>> f.my_method(9)
81
在类 Foo
中定义了方法 my_method()
,此方法有两个参数(形参)。根据8.3.3节可知,第一个参数 self
总引用类的实例,且通过实例调用方法的时候,不需要显式地为它传入实参。
此外,对于类中的方法,也可以通过类名称调用:
代码语言:javascript复制>>> Foo.my_method(f, 9) # (1)
81
此时,必须要显式地为 self
提供实例参数。
尽管方法必须通过实例名称或者类名称调用,但每个方法在 Python 中也是一个对象,比如:
代码语言:javascript复制>>> f.my_method
<bound method Foo.my_method of <__main__.Foo object at 0x7ff14892a730>>
像这样的对象在 Python 中叫做绑定方法对象,即当前调用的方法绑定在了一个实例上。如果:
代码语言:javascript复制>>> Foo.my_method
<function Foo.my_method at 0x7ff148d5f550>
显然 Foo.my_method
与普通函数无异(如前面编写的函数 func()
)——其实就是一个函数,注释(1)中的调用方式与函数形式完全一样。那么,这个方法是否可以称为“非绑定方法”——尚未与实例绑定。在 Python 3 中没有这个名词了,因为它本质是函数,只是“函数名称”有点特别罢了。
8.4.2 类方法
Python 的内置函数 classmethod()
的作用就是在类中以装饰器语法糖的形式定义类方法(Class Method)。请读者阅读下面的代码示例:
#coding:utf-8
'''
filename: clssmethod.py
'''
class Message:
msg = "Python is a smart language." # (2)
def get_msg(self):
print("the self is:", self)
print("attrs of class(Message.msg):", Message.msg) # (3)
@classmethod # (4)
def get_cls_msg(cls):
print("the cls is:", cls) # (5)
print("attrs of class(cls.msg):", cls.msg)
if __name__ == "__main__":
mess = Message()
mess.get_msg()
print("-" * 20)
mess.get_cls_msg()
先执行程序,再对照结果解释:
代码语言:javascript复制% python classmethod.py
the self is: <__main__.Message object at 0x7ff8ddb32d00>
attrs of class(Message.msg): Python is a smart language.
--------------------
the cls is: <class '__main__.Message'> # (6)
attrs of class(cls.msg): Python is a smart language.
类 Message
中定义了类属性 msg
(如注释(2)所示),然后在普通方法中调用这个类属性,如注释(3)所示,此处使用的是 Message.msg
,没有使用 self.msg
。如果将 Message.msg
改为 self.msg
,程序的输出效果是一样。
但是,不提倡使用 self.msg
。其原因要从8.3.2节图8-3-1所示的实例属性搜索顺序说起。如果该实例没有 msg
属性,则会读取 Message.__dict__['msg']
的值。注意前提条件:“实例没有 msg
属性”。在简单的程序中,我们能够很容易判断实例是否已经有 msg
属性,但在复杂情况下,不能明确地控制实例属性时,在注释(3)的语句中使用 self.msg
就会有较大风险(比如实例有与 msg
同名的属性,但其值不是注释(2)中的类属性的值)。“稳定压倒一切”,这是编程的基本原则。所以,注释(3)中使用 Message.msg
要好于 self.msg
。
故事情节必须要进一步“反转”,读者才能感觉有意思——但是,Message.msg
这种写法并不好。假如某天开发者一激动,觉得 class Message
中所用的类的名称不妥,修改成为了其他名称,但把注释(3)处给忘了——这是常见的(开发者历尽千辛万苦终于发现是此原因导致了 Bug,常常会非常激动,“解决了一个严重的潜在问题”)。那么,程序就会报错。像这种把类名称“写死”的方式,在编程中会称为硬编码(Hard Code)。如何避免硬编码?继续看下文。
注释(4)用装饰器装饰了一个名为 get_cls_msg()
的方法,这个方法的参数使用了 cls
——这也是惯例,使用其它参数名称亦可,不过还是遵守惯例较好。这个方法——被装饰器 @classmethod
装饰的方法——中如果调用类属性,不需要“硬编码”,改为 cls.msg
的样式。那么,方法中的 cls
是什么呢?
注释(5)打印了 cls
,其结果显示在注释(6),即 cls
引用了对象 <class '__main__.Message'>
——类 Message
(类也是对象)。所以,从效果上看,cls.msg
和Message.msg
是一样的,但 cls.msg
显然避免了将类名称“写死”的硬编码。能够令 cls
引用当前类对象的就是注释(4)的装饰器语法糖。
在 Python 中,通过装饰器 @classmethod
装饰的方法称为类方法。类方法的参数有且至少有一个,且要置于参数列表的首位,通常命名为 cls
,它引用的就是当前所在的类对象。
在上述程序中,类 Message
里面的普通方法 get_msg()
通常是通过实例名称调用,如 mess.get_msg()
,像这样的方法称为实例方法(Instance Method)。
怎么在实际问题中应用类方法?从如下示例中体悟。
在定义一个类时,只能有一个初始化方法 __init__()
。在某些情况下,会有捉襟见肘之感。比如,有一个名为 Person
的类,可以根据姓名( name
)和年龄( age
)实例化。如果要求还能用姓名和出生日期( birthday
)实例化(这个要求是很正常的,因为“年龄”与“出生日期”其实具有一定等价性),应该如何写初始化方法?多写一个吗?肯定不能有重名的方法。
运用类方法就能解决此问题。
代码语言:javascript复制#coding:utf-8
'''
filename: agebirth.py
'''
import datetime
class Person:
def __init__(self, name, age):
self.name = name
self.age = age # 用年龄初始化
@classmethod
def by_birth(cls, name, birth_year):
this_year = datetime.date.today().year
age = this_year - birth_year
return cls(name, age) # (7)
def get_info(self):
return "{0}'s age is {1}".format(self.name, str(self.age))
if __name__ == "__main__":
newton = Person('Newton', 26) # (8)
print(newton.get_info())
hertz = Person.by_birth("Hertz", 1857) # (9)
print(hertz.get_info())
注释(8)实例化 Person
类时,默认首先调用初始化方法 __init__()
,并且将参数传给初始化方法。但是,如果用出生年份作为注释(8)的参数,比如 Person('Hertz', 1857)
显然是不对的。为了能用年份创建实例,又不破坏已经定义的初始化方法 __init__()
,于是使用类方法装饰器,定义了类方法 by_birth()
。在这个方法中,计算了 age
之后,以注释(7)中的 cls(name, age)
创建实例对象。此处不需要使用 Person
类名称,而是使用 cls
代表当前类名称。注释(9)则直接通过类名称调用类方法创建实例。
特别要注意,注释(9)通过类名称调用类方法,本来在类中所定义的类方法有三个参数,第一个是cls
,它引用的就是当前类对象。那么在注释(9)中调用这个方法的时候,不再显式地在参数列表中传入类对象,Person.by_birth()
就表示类 Person
作为第一个参数传给了 cls
。
8.4.3 静态方法
先看这样一个问题。
写一个关于猫的类,就正常的猫而言,都有两个耳朵和四条腿,这可以作为其共有的属性,即类属性。不同的猫,颜色可能不同,所以这个属性应该是实例属性。另外,正常的猫都会叫,为此可以定义一个实例方法 speak()
实现“猫叫”。但是,如果这样,则没有从方法上体现“不管什么猫,在一般人的听觉中,叫声都一样(假设如此)”的特点。为了解决这种类型的问题,Python 中引入了静态方法(Static Method)的编写形式——所谓“静态”,即不因实例而变化,类比于8.3.2节的“静态属性”。
Python 语言的内置函数 staticmethod()
为编写静态方法提供了简洁的形式,类似8.4.2节的类方法,所有用 @staticmethod
装饰的方法即为静态方法。
#coding:utf-8
'''
filename: catspeak.py
'''
class Cat:
ears = 2
legs = 4
def __init__(self, color):
self.color = color
@staticmethod
def speak(): # (10)
print("Meow, Meow")
if __name__ == "__main__":
black_cat = Cat("black")
white_cat = Cat("white")
black_cat.speak()
white_cat.speak()
if black_cat.speak is white_cat.speak and black_cat.speak is Cat.speak:
print('black_cat.speak, white_cat.speak, Cat.speak are the same objects.')
程序执行结果如下:
代码语言:javascript复制% python catspeak.py
Meow, Meow
Meow, Meow
black_cat.speak, white_cat.speak, Cat.speak are the same objects.
注释(10)所定义的方法,既没有以 self
也没有以 cls
作为第一个参数,所以这个方法不是实例方法,也不是类方法。如果不用 @staticmethod
装饰 speak()
方法,在类里面不许可用这种形式定义方法。用 @staticmethod
装饰后,就构成了静态方法。
从执行结果可以得知,以 black_cat.speak
、white_cat.speak
、Cat.speak
三种不同方式调用同一个静态方法,该方法是同一个对象——所有猫叫声都一样。
在下面的示例中,综合应用了类方法和静态方法,请读者注意体会它们的应用时机。
代码语言:javascript复制#coding:utf-8
'''
filename: judgescore.py
'''
class Score:
def __init__(self, scores):
self.scores = scores
@classmethod
def from_csv(cls, score_csv_str):
scores = list(map(int, score_csv_str.split(',')))
return cls(scores) if cls.validate(scores) else cls(False)
@staticmethod
def validate(scores):
for g in scores:
if g < 0 or g > 100:
return False
return True
if __name__ == '__main__':
# Try out some valid scores
class_scores_valid = Score.from_csv('90, 80, 85, 94, 70')
print('Got scores:', class_scores_valid.scores)
# Should fail with invalid scores
class_scores_invalid = Score.from_csv('92, -15, 99, 101, 77, 65, 100')
print(class_scores_invalid.scores)
程序执行结果:
代码语言:javascript复制% python judgescore.py
Got scores: [90, 80, 85, 94, 70]
False
在 Score
类中,三个方法的作用依次是:
- 初始化方法
__init__()
只实现了实例属性的赋值; - 类方法
from_csv()
用于创建实例,并且对字符串参数进行转换和判断,如果有不符合要求(小于零或大于一百)的整数,则认为输入数据不合规(返回False
) - 静态方法
validate()
用于判断数据是否合规。
在类方法 from_csv()
中以 cls.validate()
的形式调用了当前类中的静态方法,显然此静态方法不需要与实例绑定。
至此,学习了类中的三种方法:
- 普通的实例方法:最常用的,第一个参数
self
是实例,用实例名称调用。 - 类方法:第一个参数
cls
是当前的类,必须用@classmethod
装饰。 - 静态方法:不需要引用实例或类的参数,必须用
@staticmethod
装饰。