首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开源图书《Python完全自学教程》8.4方法

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

作者头像
老齐
发布2022-07-06 16:12:30
2450
发布2022-07-06 16:12:30
举报
文章被收录于专栏:老齐教室老齐教室

8.4 方法

类的方法,其基本结构与第7章中学过的函数近似,就普通的方法而言,仿照函数编写即可。然而类里面还会有一些不普通的方法,比如本节将要介绍的“类方法”和“静态方法”。这些方法都是为了让程序更简洁、紧凑而创立的。如果不使用这些方法,也能编写程序,但是用了它们,则锦上添花。

8.4.1 比较方法和函数

函数和方法有很多相似处,比如都是使用 def 关键词来定义,除了某些特殊方法外( __init__() 初始化方法 ),普通方法和函数一样,都使用 return 语句作为结束(也可以说,所有方法都以 return 语句结束,但是 __init__() 中的是 return None )。

相同之处容易掌握,区别要特别关注。

函数是由函数名引用的一个独立对象(第一类对象),通过函数名称可以调用这个对象,它不依赖于其他东西。

>>> def func(x): return x+7
...
>>> func
<function func at 0x7ff148d5f430>
>>> type(func)
<class 'function'>
>>> func(4)
11

在调用函数的时候,如果函数有参数,必须很明确地(或者说是“显式地”)给每个参数提供对象或引用——一个参数也不能少。

而方法,必须要依赖于对象。因为它写在了类里面,如果要调用它,就要使用某个对象。前面已经学习过的知识是使用类的实例对象调用它,即通过实例名称:

>>> class Foo:
...     def my_method(self, x):
...         return x ** 2
...
>>> f = Foo()
>>> f.my_method(9)
81

在类 Foo 中定义了方法 my_method() ,此方法有两个参数(形参)。根据8.3.3节可知,第一个参数 self 总引用类的实例,且通过实例调用方法的时候,不需要显式地为它传入实参。

此外,对于类中的方法,也可以通过类名称调用:

>>> Foo.my_method(f, 9)    # (1)
81

此时,必须要显式地为 self 提供实例参数。

尽管方法必须通过实例名称或者类名称调用,但每个方法在 Python 中也是一个对象,比如:

>>> f.my_method
<bound method Foo.my_method of <__main__.Foo object at 0x7ff14892a730>>

像这样的对象在 Python 中叫做绑定方法对象,即当前调用的方法绑定在了一个实例上。如果:

>>> 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()

先执行程序,再对照结果解释:

% 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 )实例化(这个要求是很正常的,因为“年龄”与“出生日期”其实具有一定等价性),应该如何写初始化方法?多写一个吗?肯定不能有重名的方法。

运用类方法就能解决此问题。

#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.')

程序执行结果如下:

% 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 三种不同方式调用同一个静态方法,该方法是同一个对象——所有猫叫声都一样。

在下面的示例中,综合应用了类方法和静态方法,请读者注意体会它们的应用时机。

#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)

程序执行结果:

% 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 装饰。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-06-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老齐教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 8.4 方法
    • 8.4.1 比较方法和函数
      • 8.4.2 类方法
        • 8.4.3 静态方法
        相关产品与服务
        日志服务
        日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档