实例方法,类方法,和静态方法(Python),以及Python OOP令人迷惑的小细节

目录

一. 实例方法

1.1 与实例方法有关的小细节

1.1.1 从类处获取的方法属于“function”而不是“method”

1.1.2 从类处可以调用实例方法

1.2 关于那个self参数 

1.2.1 实例方法的第一个参数可以不是self

1.2.2 打猴子补丁不强制参数规范

1.2.3 实例方法自动传入self

1.3 实例方法的应用(略)

二. 类方法

2.1 类方法的定义方式

 2.2 通过猴子补丁定义类方法

2.3 类方法的应用

三. 静态方法

3.1 静态方法的定义方式

3.2 通过猴子补丁定义静态方法

3.3 静态方法的应用

四. 小实验:利用描述符自己模拟绑定方法(需要元编程知识)

 五. 其他东西


你或许想过,为什么Python类的方法们第一个参数“必须”是"self"呢?而且就算是用不到self也要传入它呢?这篇文章介绍实例方法,类方法和静态方法,还会谈一谈有关self的小细节。其实,对self反感的人并不在少数。不过这其实使得实现变得更简单优雅,最大的缺点是牺牲了用户接口

def say(self, text) 的签名形式与实际调用的 say(''爱发奶龙的小朋友们你们好啊~") 不符。

最后,为了展示Python“一切即对象”的统一性,我将使用描述符来让普通的实例属性拥有像实例方法那样自动传入self的行为。

一. 实例方法

实例方法类方法属于绑定方法,这是因为它们都自动绑定值。我们先谈谈实例方法。

1.1 与实例方法有关的小细节

1.1.1 从类处获取的方法属于“function”而不是“method”

我们都知道,类中定义的函数叫做方法,它的第一个参数“必须”是self。不过,你可能不知道的是,从类而不是实例处获取的方法仍被归为“function”:

class 愚蠢的人类:
    def 鬼叫(self, text):
        print(text)

我 = 愚蠢的人类()
print( type(我.鬼叫) )
print( type(愚蠢的人类.鬼叫) )

结果如下:

1.1.2 从类处可以调用实例方法

我们也知道,实例在调用方法时不需要传入自己作为self。不过,你可能不知道的是,从类处也可以调用“方法”,此时必须手动传入实例作为self:

class 愚蠢的人类:
    def 鬼叫(self, text):
        print(text)

我 = 愚蠢的人类()
我.鬼叫('GoGoGo出发喽!')
愚蠢的人类.鬼叫(我, '我们人类玩家怎么你了?')

结果如下:

1.2 关于那个self参数 

1.2.1 实例方法的第一个参数可以不是self

我的意思是说,self位置的那个参数可以取为其他名字。比如下面的猫咪类的self位是“耄耋”:

class 猫咪:
    def __init__(耄耋, 名字, 岁数, 主银):
        耄耋.名字 = 名字
        耄耋.岁数 = 岁数
        耄耋.主银 = 主银
    def 自我介绍(耄耋):
        主银msg = (
            f'我是野猫!没有人能制服我{耄耋.名字}!' 
            if 耄耋.主银 is None 
            else f'我有一个主银,他叫{耄耋.主银}'
        )
        print(f'我叫{耄耋.名字},{耄耋.岁数}岁,是小猫咪。\n{主银msg}')

圆头耄耋 = 猫咪('圆头猫爹', '5', None)
圆头耄耋.自我介绍()

可以看到,它能正常工作:

1.2.2 打猴子补丁不强制参数规范

正是因为self只是命名约定,给类打猴子补丁的时候,参数也不需要严格遵守命名规则。比如下面给猫咪类打一个“吃东西”方法的补丁,补丁的第一个参数不是self也不是“耄耋”:

def 吃东西(小猫, 饭): #(1)
    print(f'{小猫.名字}吃了{饭}。')

猫咪.吃饭 = 吃东西 #(2)

圆头耄耋 = 猫咪('圆头猫爹', '5', None)
坏猫 = 猫咪('比老公还重要的坏猫', '3', '小明剑魔?')

圆头耄耋.吃饭('猫粮') #(3)
猫咪.吃饭(坏猫, '鱼') #(4)

(1):定义一个函数“吃东西”,它假定“小猫”是猫咪类的实例。这里“名字”不会被VS Code高亮,除非你注解“小猫”的类型为“猫咪”类。

(2):我刻意使用不同的名字,把猫咪类的“吃饭”属性赋值成“吃东西”函数。

(3): 现在猫咪的实例可以使用“吃饭”方法,但是这里“吃饭”不会被VS Code高亮。毕竟打猴补丁算是一种元编程操作,太过于动态的话编辑器也反应不过来。

(4):同样的,在类处也可以调用“吃饭”。但是需要传入猫咪实例。

结果如下:

1.2.3 实例方法自动传入self

 下面看一个极端例子,好孩子不要学我!

class str(str): #(1)
    def __call__(*args, **kwargs):
        print(*args, **kwargs)

先辈 = str('哼哼哼啊啊啊啊啊啊啊') #(2)
先辈('我24岁,是学生', '好时代,来临力(喜)', sep='!\n', end='!!!') #(3)

(1):用str继承内置类型str,这是纯纯的“骚操作”。我们新增了__call__方法,让它的实例可以像函数一样调用。这里__call__接收任意参数,然后把它们原封不动地传给print。

(2):“先辈”是新str的实例,参数是……那个东西。注意:我们虽然没有编写__init__来处理构造参数,但是新str天然使用从经典str那里继承的__init__方法。因此这……很臭的参数会被保留。

(3):调用__call__方法。sep='!\n'表示各个参数之间以 !+换行 隔开;end='!!!'表示最后会打印三个叹号。你觉得输出是什么样的呢?

下面公布答案:

哇噢!怎么很臭的那个东西也被当成参数传给print了呢?这是因为实例方法会把签名中的第一个参数自动赋值为类的实例。所以,如果我们不使用self捕获它,它就会成为*args的一部分——被print执行。而且,因为新str继承经典str的__init__等方法,所以 print(新str的实例) 可以像打印正常字符串那样执行,从而很臭的参数被打印了出来。

想要让它不打印很臭的参数,可以捕获self但是不用:

#---省略部分---
    def __call__(self, *args, **kwargs):
        ...

先辈 = str('哼哼哼啊啊啊啊啊啊啊')
先辈('我24岁,是学生', '好时代,来临力(喜)', sep='!\n', end='!!!')

现在更符合逻辑一些,毕竟我们没想要让它打印自己:

1.3 实例方法的应用(略)

这个嘛,大家比我会的更多!我就不再多说啦。(* ̄▽ ̄)~*

二. 类方法

下面我们谈谈类方法,它使用@classmethod装饰器定义。类似于实例方法的自动传参,它也自动绑定第一个参数,只不过绑定的是类本身。

2.1 类方法的定义方式

我们直接看代码:

class 猫咪:
    def __init__(耄耋, 名字, 岁数, 主银):
        耄耋.名字 = 名字
        耄耋.岁数 = 岁数
        耄耋.主银 = 主银
    def 自我介绍(耄耋):
        主银msg = (
            f'我是野猫!没有人能制服我{耄耋.名字}!' 
            if 耄耋.主银 is None 
            else f'我有一个主银,他叫{耄耋.主银}'
        )
        print(f'我叫{耄耋.名字},{耄耋.岁数}岁,是小猫咪。\n{主银msg}')
    @classmethod #(1)
    def 构造猫猫(cls, 名字, 岁数, 主银): #(2)
        return cls(名字, 岁数, 主银)

壕矛 = 猫咪.构造猫猫('一只好猫', 2, '小明建模') #(3)
淮矛 = 壕矛.构造猫猫('一只坏猫', 3, None) #(4)
壕矛.自我介绍()
print('------------------')
淮矛.自我介绍()

(1):“构造猫猫”是类方法,因此使用@classmethod装饰器。

(2):类方法的第一个参数自动绑定为类本身,即“猫咪”类。就像实例命名为self那样,类本身推荐命名为cls。这里方法的功能很简单,就是创建一个新的猫咪实例。

(3):可以通过 某个类.类方法() 来调用类方法。

(4):也可以使用 某个实例.类方法() 来调用,这时不需要传入类。

结果:

 2.2 通过猴子补丁定义类方法

类方法也可以通过猴子补丁创建,同样不强制要求参数规范。

class 猫咪:
    def __init__(耄耋, 名字, 岁数, 主银):
        耄耋.名字 = 名字
        耄耋.岁数 = 岁数
        耄耋.主银 = 主银
    def 自我介绍(耄耋):
        主银msg = (
            f'我是野猫!没有人能制服我{耄耋.名字}!' 
            if 耄耋.主银 is None 
            else f'我有一个主银,他叫{耄耋.主银}'
        )
        print(f'我叫{耄耋.名字},{耄耋.岁数}岁,是小猫咪。\n{主银msg}')

@classmethod #(1)
def 造猫(猫咪, 名字, 岁数, 主银): #(2)
    return 猫咪(名字, 岁数, 主银)

猫咪.构造猫猫 = 造猫 #(3)
#(4)
壕矛 = 猫咪.构造猫猫('一只好猫', 2, '小明建模')
淮矛 = 壕矛.构造猫猫('一只坏猫', 3, None)
壕矛.自我介绍()
print('------------------')
淮矛.自我介绍()

(1):用来打补丁的函数需要使用@classmethod装饰器。

(2):“造猫”函数假定第一个参数是猫咪类。

(3):把猫咪类的“构造猫猫”方法赋值成“造猫”函数。

(4):剩下的代码都能正常运行。

2.3 类方法的应用

一般地,类方法最常用于定义工厂函数,即构造实例的其它方法。最经典的例子就是from_bytes()方法:利用字节表示形式构建对象。

再比如,下面是一个例子,读取嵌套映射数据。把其中的映射都变成Data实例,序列都变成列表并且继续深入(用到了递归调用),单一元素都保持不变。它有些复杂,改编自《流畅的Python》元编程那一部分:

from collections import abc

class Data:
    def __init__(self, mapping):
        self.__data = dict(mapping)
    @property
    def data(self):
        return self.__data
    def __getattr__(self, name):
        try:
            return getattr(self.data, name)
        except AttributeError:
            return Data.build(self.data[name])
    @classmethod
    def build(cls, obj):
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(i) for i in obj]
        else:
            return obj

我们解释一下这里的__getattr__和build方法。

1. __getattr__()

  • 当访问一个不存在的属性时,__getattr__会被调用。
  • 它尝试从 self.data(即 __data)中获取该属性。
  • 如果属性不存在,会调用 Data.build 方法递归地构建新的 Data 对象。

2.build() 

  • build 是一个递归类方法,用于将嵌套的数据结构(如字典或列表)包装成 Data 对象。
  • 逻辑:如果 obj 是一个映射(如字典),递归地将其包装成 Data 对象。如果 obj 是一个可变序列(如列表),递归地将其每个元素包装成 Data 对象。如果是其他类型的对象,直接返回原始对象。

三. 静态方法

静态方法不是绑定方法,使用@staticmethod装饰器定义。它不自动绑定参数。可以说,它就是“写在类主体中的普通函数”。

3.1 静态方法的定义方式

下面我们为老朋友猫咪类新添一个“喵喵叫”方法,它不需要任何和猫咪实例有关的数据。因此很适合当作静态方法:

from random import randint

class 猫咪:
    def __init__(耄耋, 名字, 岁数, 主银):
        耄耋.名字 = 名字
        耄耋.岁数 = 岁数
        耄耋.主银 = 主银
    @staticmethod #(1)
    def 喵喵叫(): #(2)
        print('喵'*randint(1,5) + '~') #(3)

POP猫 = 猫咪('POP猫', '我不知道QAQ', '我也不知道QAQ')

POP猫.喵喵叫() #(4)
猫咪.喵喵叫() #(5)

(1):“喵喵叫”是静态方法,使用@staticmethod装饰器。

(2):静态方法不绑定参数。这里不需要额外参数,所以不声明任何形参。

(3):随机生成1到5个喵,最后加上波浪线。

(4):静态方法可以从实例调用。

(5):也可以从类调用,没有明显不同。

结果:

3.2 通过猴子补丁定义静态方法

啊……这个该怎么做,相信大家心里已经有数了:

from random import randint

class 猫咪:
    def __init__(耄耋, 名字, 岁数, 主银):
        耄耋.名字 = 名字
        耄耋.岁数 = 岁数
        耄耋.主银 = 主银

@staticmethod
def 鬼叫():
    print('喵'*randint(1,5) + '~')

猫咪.喵喵叫 = 鬼叫

POP猫 = 猫咪('POP猫', '我不知道QAQ', '我也不知道QAQ')

POP猫.喵喵叫()
猫咪.喵喵叫()

结果:

3.3 静态方法的应用

静态方法(@staticmethod)在 Python 中的主要作用是定义与类相关但不依赖于实例或类本身的逻辑。它们是类的一部分,但不需要访问实例(self)或类(cls)的属性和方法。

例如,Python中构建翻译表的方法maketrans是str类的静态方法,在调用时使用str.maketrans()这种写法。(我之前写"实现Atbash加密"时,在文中说它是类方法,这是极度不负责任且自私的,我谢罪!QAQ)

静态方法其实就是普通函数,但是封装在类的命名空间里,让整体更加紧凑。比如,“喵喵叫”完全可以写成普通函数,但是不和“猫咪”联系起来,它的意义就少了很多:谁在叫?是作者吗?作者为什么要喵喵叫?为什么喵喵叫要写在这里?”但是,如果跟猫咪类放在一起,意义就很明确了。

好孩子不要学我!:我经常口胡把静态方法说成是“类方法”,我的意思其实是“封装在类里的普通方法”,但是可能导致表意不明。请大家一定要仔细辨析术语!

四. 小实验:利用描述符自己模拟绑定方法(需要元编程知识)

这一部分是我在作死,演示Python的动态功能多么强大。我注明了理解它可能需要元编程的知识,如果你觉得自己还看不明白它的工作原理,那么请直接忽略它!前面的内容才是核心。

下面先定义一个猫咪类,它的“吃两种东西”实例属性是一个匿名函数:

class 猫咪:
    def __init__(self, 名字: str):
        self.名字 = 名字
        self.吃两种东西 = lambda self, 食物1, 食物2 : print(f"{self.名字} 吃了 {食物1} 和 {食物2}")

小猫 = 猫咪('二狗')
小猫.吃两种东西(小猫, '猫粮', '鱼')

现在,这个属性不能自动绑定实例本身,所以要传入一个实例"小猫":

下面使用描述符,完成自动传入self的工作:

from types import MethodType

class 实例方法: #(1)
    def __set_name__(self, owner, name):
        self.name = name
    def __set__(self, instance, value):
        instance.__dict__[self.name] = value
    def __get__(self, instance, owner): #(2)
        if instance is None:
            return self
        else:
            return MethodType(instance.__dict__[self.name], instance)

class 猫咪:
    吃两种东西 = 实例方法() #(3)
    def __init__(self, 名字: str):
        self.名字 = 名字
        self.吃两种东西 = lambda self, 食物1, 食物2 : print(f"{self.名字} 吃了 {食物1} 和 {食物2}")

小猫 = 猫咪('二狗')
小猫.吃两种东西('猫粮', '鱼') #(4)

(1):“实例方法”是描述符,前两个方法都是标准实现。

(2):__get__方法是模拟的关键。如果instance是None,说明是在类处获取属性,那么返回描述符本身。这会导致无法从类处调用这个属性,因为返回的是描述符而不是函数。否则,使用types模块的MethodType来帮忙把instance绑定成函数的第一个参数,并返回改造后的函数。也可以使用functools.partial(),但是后者的type()显示是"<class 'functools.partial'>",前者则是"<class 'method'>"。因此虽然效果相同,但是前者更好。

(3):把类属性“吃两种东西”指定为“实例方法”的实例,应用描述符。

(4):现在,不再需要自己手动传入实例,描述符帮我们自动传入了这个参数。

结果:

好了,我的作死到此为止。

 五. 其他东西

我好像写的有点多了……(。ŏ_ŏ)

再推荐一些关于OOP的资料吧,可能没有我写的这么阴阳怪气和篇幅长,但它们是很棒的资源:

深入理解Python中的类方法、类实例方法和静态方法(作者:lww爱学习)

一文带你搞懂python中什么是实例方法,什么是类方法,什么是静态方法!!!(作者:No later)

深度解析:Python中的类方法、实例方法与静态方法(作者:一休哥助手)

 【Python】静态方法 (@staticmethod) 和类方法 (@classmethod)(作者:彬彬侠)

未来属于终身学习者,大家加油呀。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值