开源图书《Python完全自学教程》8.4方法

2022-07-06 16:12:30 浏览数 (1)

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)。请读者阅读下面的代码示例:

代码语言:javascript复制
#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.msgMessage.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 装饰的方法即为静态方法。

代码语言:javascript复制
#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.speakwhite_cat.speakCat.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 装饰。

0 人点赞