我有一个由外部库提供给我的课程。我已经创建了这个类的子类。我也有一个原始类的实例。
我现在想要将此实例转换为我的子类的实例,而不更改实例已有的任何属性(除了我的子类覆盖的那些属性)。
以下解决方案似乎有效。
# This class comes from an external library. I don't (want) to control
# it, and I want to be open to changes that get made to the class
# by the library provider.
class Programmer(object):
def __init__(self,name):
self._name = name
def greet(self):
print "Hi, my name is %s." % self._name
def hard_work(self):
print "The garbage collector will take care of everything."
# This is my subclass.
class C_Programmer(Programmer):
def __init__(self, *args, **kwargs):
super(C_Programmer,self).__init__(*args, **kwargs)
self.learn_C()
def learn_C(self):
self._knowledge = ["malloc","free","pointer arithmetic","curly braces"]
def hard_work(self):
print "I'll have to remember " + " and ".join(self._knowledge) + "."
# The questionable thing: Reclassing a programmer.
@classmethod
def teach_C(cls, programmer):
programmer.__class__ = cls # <-- do I really want to do this?
programmer.learn_C()
joel = C_Programmer("Joel")
joel.greet()
joel.hard_work()
#>Hi, my name is Joel.
#>I'll have to remember malloc and free and pointer arithmetic and curly braces.
jeff = Programmer("Jeff")
# We (or someone else) makes changes to the instance. The reclassing shouldn't
# overwrite these.
jeff._name = "Jeff A"
jeff.greet()
jeff.hard_work()
#>Hi, my name is Jeff A.
#>The garbage collector will take care of everything.
# Let magic happen.
C_Programmer.teach_C(jeff)
jeff.greet()
jeff.hard_work()
#>Hi, my name is Jeff A.
#>I'll have to remember malloc and free and pointer arithmetic and curly braces.
然而,我不相信这个解决方案不包含任何我没有想到的警告(对于三重否定而感到抱歉),特别是因为重新分配神奇的__class__
感觉不对。即使这样有效,我也无法理解应该有更多的pythonic方法来做到这一点。
有吗?
编辑:感谢大家的回答。这是我从他们那里得到的:
虽然通过分配__class__
重新分类实例的想法并不是一个广泛使用的习惯用法,但大多数答案(在撰写本文时为6个中的4个)认为它是一种有效的方法。一个anwswer(由ojrac说)乍一看“非常奇怪”,我同意(这是提出问题的原因)。只有一个答案(由Jason Baker提出;有两个正面评论和选票)积极阻止我这样做,不过这样做基于示例用例而不是一般的技术。
没有一个答案,无论是否为正,都会在此方法中发现实际的技术问题。一个小例外是jls提到提防旧式类,这可能是真的,并且C扩展。我认为新方式感知的C扩展应该与Python本身一样好(假设后者是真的),尽管如果你不同意,请保留答案。
至于pythonic是如何的问题,有一些积极的答案,但没有给出真正的理由。看一下Zen(import this
),我想这个案例中最重要的规则是“明确比隐含更好”。不过,我不确定这条规则是否反对以这种方式重新分类。
使用{has,get,set}attr
似乎更明确,因为我们明确地对对象进行了更改而不是使用魔法。
使用__class__ = newclass
似乎更明确,因为我们明确地说“这现在是类'newclass的对象',期望一种不同的行为”,而不是默默地改变属性,但让对象的用户相信它们是处理旧班的常规对象。
总结:从技术角度来看,该方法似乎没问题; pythonicity问题仍然没有答案,偏向于“是”。
我接受了Martin Geisler的回答,因为Mercurial插件示例非常强大(并且还因为它回答了我甚至还没有问过自己的问题)。但是,如果对pythonicity问题有任何争论,我仍然希望听到它们。谢谢你们所有人。
P.S。实际用例是一个UI数据控件对象,需要在运行时增加其他功能。但是,问题是非常笼统的。
答案 0 :(得分:19)
当扩展(插件)想要更改代表本地存储库的对象时,在Mercurial(分布式修订控制系统)中重新完成这样的实例。该对象称为repo
,最初是localrepo
实例。它依次传递给每个扩展,并且在需要时,扩展将定义一个新类,它是repo.__class__
的子类,将<{em}}的类更改为此新子类的repo
!
在代码中显示like this:
def reposetup(ui, repo):
# ...
class bookmark_repo(repo.__class__):
def rollback(self):
if os.path.exists(self.join('undo.bookmarks')):
util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
return super(bookmark_repo, self).rollback()
# ...
repo.__class__ = bookmark_repo
扩展(我从书签扩展中获取代码)定义了一个名为reposetup
的模块级函数。 Mercurial将在初始化扩展时调用此方法并传递ui
(用户界面)和repo
(存储库)参数。
该函数然后定义了类repo
恰好是的子类。由于扩展需要能够相互扩展,所以不就足以简单地继承localrepo
。因此,如果第一个扩展程序将repo.__class__
更改为foo_repo
,则下一个扩展名应将repo.__class__
更改为foo_repo
的子类,而不仅仅是localrepo
的子类。最后,该函数更改了instanceø的类,就像在代码中一样。
我希望此代码能够合法地使用此语言功能。我认为这是我见过它在野外使用的唯一地方。
答案 1 :(得分:11)
我不确定在这种情况下继承的使用是否最好(至少在“重新分类”方面)。看起来你是在正确的轨道,但听起来像组合或聚合将是最好的。这是我正在考虑的一个例子(在未经测试的伪esque代码中):
from copy import copy
# As long as none of these attributes are defined in the base class,
# this should be safe
class SkilledProgrammer(Programmer):
def __init__(self, *skillsets):
super(SkilledProgrammer, self).__init__()
self.skillsets = set(skillsets)
def teach(programmer, other_programmer):
"""If other_programmer has skillsets, append this programmer's
skillsets. Otherwise, create a new skillset that is a copy
of this programmer's"""
if hasattr(other_programmer, skillsets) and other_programmer.skillsets:
other_programmer.skillsets.union(programmer.skillsets)
else:
other_programmer.skillsets = copy(programmer.skillsets)
def has_skill(programmer, skill):
for skillset in programmer.skillsets:
if skill in skillset.skills
return True
return False
def has_skillset(programmer, skillset):
return skillset in programmer.skillsets
class SkillSet(object):
def __init__(self, *skills):
self.skills = set(skills)
C = SkillSet("malloc","free","pointer arithmetic","curly braces")
SQL = SkillSet("SELECT", "INSERT", "DELETE", "UPDATE")
Bob = SkilledProgrammer(C)
Jill = Programmer()
teach(Bob, Jill) #teaches Jill C
has_skill(Jill, "malloc") #should return True
has_skillset(Jill, SQL) #should return False
如果您不熟悉这些示例,则可能需要详细了解sets和arbitrary argument lists。
答案 2 :(得分:3)
这很好。我已经习惯了很多次这个习语。但要记住的一点是,这种想法不适合旧式类和各种C扩展。通常情况下这不是问题,但由于您使用的是外部库,因此您必须确保不要处理任何旧式类或C扩展。
答案 3 :(得分:3)
“状态模式允许对象在其内部状态发生变化时改变其行为。该对象似乎会改变它的类。” - 首创设计模式。 Gamma等人写的非常类似。在他们的设计模式书中。 (我把它放在我的其他地方,所以没有引用)。我认为这就是这种设计模式的重点。但是,如果我可以在运行时更改对象的类,大多数时候我不需要该模式(有时State Pattern不仅仅模拟类更改)。
此外,在运行时更改类并不总是有效:
class A(object):
def __init__(self, val):
self.val = val
def get_val(self):
return self.val
class B(A):
def __init__(self, val1, val2):
A.__init__(self, val1)
self.val2 = val2
def get_val(self):
return self.val + self.val2
a = A(3)
b = B(4, 6)
print a.get_val()
print b.get_val()
a.__class__ = B
print a.get_val() # oops!
除此之外,我考虑在运行时改变课程Pythonic并不时使用它。
答案 4 :(得分:1)
Heheh,有趣的例子。
乍一看,“重新分类”非常奇怪。那么'复制构造函数'方法呢?您可以使用类似反射的hasattr
,getattr
和setattr
执行此操作。此代码将从一个对象复制到另一个对象,除非它已经存在。如果您不想复制方法,可以将它们排除;请参阅评论if
。
class Foo(object):
def __init__(self):
self.cow = 2
self.moose = 6
class Bar(object):
def __init__(self):
self.cat = 2
self.cow = 11
def from_foo(foo):
bar = Bar()
attributes = dir(foo)
for attr in attributes:
if (hasattr(bar, attr)):
break
value = getattr(foo, attr)
# if hasattr(value, '__call__'):
# break # skip callables (i.e. functions)
setattr(bar, attr, value)
return bar
所有这些反映都不是很好,但有时候你需要一台丑陋的反射机来制作很酷的东西。 ;)
答案 5 :(得分:1)
这种技术对我来说似乎是合理的Pythonic。合成也是一个不错的选择,但是分配给__class__
是完全有效的(有关以稍微不同的方式使用它的配方,请参阅here。)
答案 6 :(得分:0)
在ojrac的回答中,break
突然出现for
- 循环,并且不再测试任何属性。我认为仅使用if
- 语句来决定一次一个地对每个属性做什么更有意义,并继续遍历所有属性的for
循环。否则,我喜欢ojrac的答案,因为我也认为__class__
分配给我很奇怪。 (我是Python的初学者,据我记得这是我在StackOverFlow上的第一篇文章。感谢所有的好消息!!)
所以我试着实现它。我注意到dir()没有列出所有属性。 http://jedidjah.ch/code/2013/9/8/wrong_dir_function/所以我添加了&#39; 类&#39;,&#39; doc &#39;,&#39; 模块强>&#39;和&#39; init &#39;如果他们已经不存在,那么要添加的内容列表(尽管他们可能已经存在),并且想知道dir是否还有更多错过的东西。我还注意到我(可能)分配到类&#39;说完之后很奇怪。
答案 7 :(得分:-2)
我会说这完全没问题,如果它适合你。