Python:是方法引用了对象,而非对象引用了方法

惊奇的发现:

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

从打印描述来看,method1method2method3都是<__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实时获取到最新版的方法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值