目录
1.1.1 从类处获取的方法属于“function”而不是“method”
你或许想过,为什么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)(作者:彬彬侠)
未来属于终身学习者,大家加油呀。