惊奇的发现:
class Cat(object):
def Eat(self):
pass
cat = Cat()
print cat.Eat is cat.Eat
# 打印结果: False
事发背景:
前段时间做了个游戏输入管理器,那输入管理器一般而言的设计自然是提供一个Register接口提供外部注册,当某种输入条件达成时,触发注册到里面的函数或方法。
同时又希望外部可以更省心得使用,也就是说允许只手动注册不手动注销,避免某些情况下对象本该被释放时忘了写注销,或某些原因没有调用到注销,导致对象一直被管理器引用没释放。
那理所应当得是使用Weakref弱引用注册进来的函数或方法,并且在它销毁时回调时自动触发注销。
但测试时发现,注册进对象的方法时,立即就触发了自动注销流程,也就是说这个方法对象刚传进去就销毁了。
# -*- coding: utf-8 -*-
import weakref
def Func():
pass
class Cat(object):
def Eat(self):
pass
def OnDestory(*args):
print "对象已gg", args
cat = Cat()
print "Step 1"
funcRef = weakref.ref(Func, OnDestory)
print "Step 2"
catRef = weakref.ref(cat.Eat, OnDestory)
print "Step 3"
print funcRef
print "Step 4"
print catRef
print "Step 5"
# ----------------------------------------------------------------
# 打印结果:
# Step 1
# Step 2
# 对象已gg (<weakref at 0000000002E58A48; dead>,)
# Step 3
# <weakref at 0000000002E044A8; to 'function' at 00000000028ACBA8 (Func)>
# Step 4
# <weakref at 0000000002E58A48; dead>
# Step 5
# 对象已gg (<weakref at 0000000002E044A8; dead>,)
过程类似上例,Func是函数,被模块引用,因此程序运行结束时才销毁,但方法cat.Eat被弱引用的同时就gg了,cat是全局对象,自然也是程序运行结束才销毁,对象还存在时,它的方法gg了?
为什么会这样呢?
原因在于习惯性得认为:属性与方法一样,是对象的一部分,换言之,都是被对象自己引用的,因此认为对象的方法因为被对象引用,所以只要对象存在,那么这个方法对象肯定也是存在的,因此直接对方法对象弱引用是可行的。
但实际上,恰恰相反,是方法引用了对象,而非对象引用了方法。
class Cat(object):
def Eat(self):
pass
cat = Cat()
method1 = cat.Eat
method2 = cat.Eat
method3 = cat.Eat
print("method1对象描述", method1)
print("method1对象id", id(method1))
print("method2对象描述", method2)
print("method2对象id", id(method2))
print("method3对象描述", method3)
print("method3对象id", id(method3))
# 打印结果
# method1对象描述 <bound method Cat.Eat of <__main__.Cat object at 0x0000014AB0337408>>
# method1对象id 1420289855688
# method2对象描述 <bound method Cat.Eat of <__main__.Cat object at 0x0000014AB0337408>>
# method2对象id 1420289856776
# method3对象描述 <bound method Cat.Eat of <__main__.Cat object at 0x0000014AB0337408>>
# method3对象id 1420294742344
从打印描述来看,method1,method2,method3都是<__main__.Cat object at 0x0000014AB0337408>的一个bound method,也就是说都是cat这个对象的方法。但它们的对象id却是不一样,说明它们只是内容相同,但实际是不同的方法对象。每次 对象.方法 调用,都会生成一个新的方法对象。
print(dir(method1))
# 打印结果
# ['__call__', '__class__', '__delattr__', '__dir__',
# '__doc__', '__eq__', '__format__', '__func__', '__ge__',
# '__get__', '__getattribute__', '__gt__', '__hash__',
# '__init__', '__init_subclass__', '__le__', '__lt__',
# '__ne__', '__new__', '__reduce__', '__reduce_ex__',
# '__repr__', '__self__', '__setattr__',
# '__sizeof__', '__str__', '__subclasshook__']
打印一下方法对象的属性,除了一些常规属性之外,还有__self__,__func__两个属性。
class Cat(object):
def Eat(self):
pass
cat = Cat()
method = cat.Eat
print(method.__self__ is cat)
print(method.__func__ is Cat.Eat)
# 打印结果:
# True
# True
很显而易见了,每次执行 对象.方法,都会生成一个新的方法对象,这个新的方法会引用对象,以及一个函数对象,这个函数对象也就是对象的类里用def定义的函数。
这也顺带解释了,类里使用了def定义的函数其实就跟普通的函数没什么两样,Cat类.Eat就是一个普通的函数对象,但是Cat对象.Eat是一个方法对象,方法调用与函数调用最明显的区别是,self参数是不需要传的。
Cat类.Eat定义时第一个参数是self,但Cat对象是Cat对象.Eat()直接调用,不需要传self,为什么不需要?因为Cat对象.Eat是方法,而不是函数,方法被调用时,它最终执行的也是Cat类.Eat,只是它帮你自动得把self参数传了。
class Cls(object):
def Func(self):
print(id(self))
print("Hello")
obj = Cls()
method = obj.Func
method()
method.__func__(method.__self__)
Cls.Func(obj)
# 打印结果
# 2428456467336
# Hello
# 2428456467336
# Hello
# 2428456467336
# Hello
后三行调用基本是一样的过程,method只是将对象和函数一起包了一层并免去了外部对self的传参,无论其中有多么花里胡哨的存储方式或调用跳转,最终还是得走调用一个函数并规规矩矩传参这么一个质朴的过程。
解决方案
问题已查明,尝试弱引用了一个没有被其他地方强引用的方法对象,因此输入管理器弱引用了个寂寞。
监听方法的生命周期解决方案是将传进来的函数和方法进行区分,函数直接弱引用,方法则是对该方法的对象,也就是这方法的__self__属性进行弱引用监听生命周期。
这解决了监听自动注销的难题,但注册新的问题又来了,想调用到这个方法对象,必须强引用它,但方法对象又强引用了__self__,还是间接强引用了对象,那监听生命周期自动注销直接无效。解决方案是不保存这个方法,创建一个伪方法对象,伪方法与方法的区别就是伪方法对对象是弱引用。
class InputMethod(object):
def __init__(self, obj, methodName):
self._obj = weakref.ref(obj)
self._methodName = methodName
def __call__(self, *args, **kwargs):
method = getattr(self._obj(), self._methodName, None)
if method is None:
print("Object {} lose method {}".format(type(self._obj()), self._methodName))
return
method(*args, **kwargs)
...
# 输入管理器里从传进来的method获取它的对象和方法名,构造一个伪方法对象并保存
# InputMethod(method.__self__, method.__name__)
...
伪方法对象也可以保存类函数的引用,即method._func_。但考虑开发时频繁有热更操作,便选择保存方法名,调用时使用getattr实时获取到最新版的方法。