向现有类实例添加方法,或如何“子类化”实例
我正在使用一个包,这个包给我提供了一个装满数据的对象,我不想手动把这些数据转成其他对象来用。我想做的是给这个对象添加一些额外的方法,方便我自己使用。
理想情况下,我希望能神奇地对这个对象进行子类化,但这似乎不太可能。虽然“猴子补丁”可能有效,但网上说这样做不太好,而且因为我代码的其他部分可能还会用到原来的类,所以这样做有点危险。
我尝试创建一个包装对象,但很多(可能全部)魔法方法(比如 __iter__
)会跳过 __getattribute__
的调用,所以这样不完整。直接写一堆转发函数(比如 def __iter__(self): return iter(object.__getattribute__(self, '_digraph'))
)感觉很笨重(而且我可能会忘记写其中一个)。
class ColliderGraph(object):
def __init__(self, digraph):
self._digraph = digraph
def __getattribute__(self, name):
cls_attrs = ['_digraph', 'whereis', 'copy'] # more to follow.
if name not in cls_attrs:
return object.__getattribute__(
object.__getattribute__(self, '_digraph'), name)
else:
return object.__getattribute__(self, name)
#return getattr(self._digraph, name)
def whereis(self, node):
"""find a node inside other nodes of the digraph"""
def copy(self):
return ColliderGraph(self._digraph.copy())
在其他地方,我以一种更有限的方式开始用一个奇怪的函数来补丁这个实例,像这样:
def _whereis_addon(digraph, node):
"""For patching onto specially-modifed digraph objects."""
# then elsewhere...
digraph.whereis = types.MethodType(_whereis_addon, digraph)
但是如果调用了 .copy()
,那么它就会失去这些升级(我想我也可以补丁这个...),而且这样添加一堆方法看起来也不太好,但或许可以做到。
有没有更好的解决办法呢?
1 个回答
首先,我觉得最靠谱的办法是对digraph
这个实例进行修改,添加你需要的方法,并且把__copy__
也包括在内,或者你可以继续使用你的封装,利用元类为魔法方法添加代理,就像在这个回答中提到的那样。
话说回来,我最近在玩一个“神奇”的子类实例的想法,觉得可以和你分享一下我的发现,因为你也在尝试同样的事情。以下是我写的代码:
def retype_instance(recvinst, sendtype, metaklass=type):
""" Turn recvinst into an instance of sendtype.
Given an instance (recvinst) of some class, turn that instance
into an instance of class `sendtype`, which inherits from
type(recvinst). The output instance will still retain all
the instance methods and attributes it started with, however.
For example:
Input:
type(recvinst) == Connection
sendtype == AioConnection
metaklass == CoroBuilder (metaclass used for creating AioConnection)
Output:
recvinst.__class__ == AioConnection
recvinst.__bases__ == bases_of_AioConnection +
Connection + bases_of_Connection
"""
# Bases of our new instance's class should be all the current
# bases, all of sendtype's bases, and the current type of the
# instance. The set->tuple conversion is done to remove duplicates
# (this is required for Python 3.x).
bases = tuple(set((type(recvinst),) + type(recvinst).__bases__ +
sendtype.__bases__))
# We change __class__ on the instance to a new type,
# which should match sendtype in every where, except it adds
# the bases of recvinst (and type(recvinst)) to its bases.
recvinst.__class__ = metaklass(sendtype.__name__, bases, {})
# This doesn't work because of http://bugs.python.org/issue672115
#sendtype.__bases__ = bases
#recv_inst.__class__ = sendtype
# Now copy the dict of sendtype to the new type.
dct = sendtype.__dict__
for objname in dct:
if not objname.startswith('__'):
setattr(type(recvinst), objname, dct[objname])
return recvinst
这个想法是重新定义实例的__class__
,把它改成我们选择的新类,并将原来的__class__
的值添加到inst.__bases__
中(连同新类型的__bases__
)。此外,我们还把新类型的__dict__
复制到实例中。这听起来可能有点疯狂,实际上也确实是,但我做了一些小测试,似乎(大部分)是有效的:
class MagicThread(object):
def magic_method(self):
print("This method is magic")
t = Thread()
m = retype_instance(t, MagicThread)
print m.__class__
print type(m)
print type(m).__mro__
print isinstance(m, Thread)
print dir(m)
m.magic_method()
print t.is_alive()
print t.name
print isinstance(m, MagicThread)
输出:
<class '__main__.MagicThread'>
<class '__main__.MagicThread'>
(<class '__main__.MagicThread'>, <class 'threading.Thread'>, <class 'threading._Verbose'>, <type 'object'>)
True
['_Thread__args', '_Thread__block', '_Thread__bootstrap', '_Thread__bootstrap_inner', '_Thread__daemonic', '_Thread__delete', '_Thread__exc_clear', '_Thread__exc_info', '_Thread__ident', '_Thread__initialized', '_Thread__kwargs', '_Thread__name', '_Thread__started', '_Thread__stderr', '_Thread__stop', '_Thread__stopped', '_Thread__target', '_Verbose__verbose', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_block', '_note', '_reset_internal_locks', '_set_daemon', '_set_ident', 'daemon', 'getName', 'ident', 'isAlive', 'isDaemon', 'is_alive', 'join', 'magic_method', 'name', 'run', 'setDaemon', 'setName', 'start']
This method is magic
False
Thread-1
False
所有的输出都正如我们所希望的那样,除了最后一行 - isinstance(m, MagicThread)
返回的是False
。这是因为我们并没有把__class__
真正赋值给我们定义的MagicMethod
类。相反,我们创建了一个独立的类,名字相同,方法和属性也一样。理想情况下,这可以通过在retype_instance
中真正重新定义MagicThread
的__bases__
来解决,但Python不允许这样做:
TypeError: __bases__ assignment: 'Thread' deallocator differs from 'object'
这似乎是一个Python中的bug,可以追溯到2003年。至今还没有修复,可能是因为动态重新定义实例的__bases__
是个奇怪且可能不好的主意!
现在,如果你不在乎能否使用isinstance(obj, ColliderGraph)
,那么上面的做法可能对你有用。或者它可能会以奇怪、意想不到的方式失败。我真的不推荐在任何生产代码中使用这个,但我觉得还是分享出来比较好。