在Python中重新分类实例
我有一个类,是从外部库里得到的。我为这个类创建了一个子类。同时,我也有一个这个原始类的实例。
现在我想把这个实例变成我子类的实例,但不想改变这个实例已经拥有的任何属性(除了那些我子类本身会覆盖的属性)。
下面这个方法似乎可以实现。
# 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__
让我觉得不太对劲。即使这个方法有效,我还是觉得应该有更符合 Python 风格的做法。
有没有呢?
编辑:感谢大家的回答。根据这些回答,我得到了以下几点:
虽然通过给
__class__
赋值来重新分类一个实例的想法并不常见,但大多数回答(在写这段时是6个回答中有4个)认为这是一个有效的方法。有一个回答(来自 ojrac)说这“乍一看有点奇怪”,我同意这个看法(这也是我提问的原因)。只有一个回答(来自 Jason Baker,得到了两个积极的评论和投票)明确劝我不要这样做,但主要是基于具体的使用案例,而不是对这个技术本身的看法。无论是正面的还是负面的回答,都没有发现这个方法的实际技术问题。唯一的例外是 jls 提到要小心旧式类,这可能是对的,还有 C 扩展。我想新的 C 扩展应该和这种方法一样好用,前提是 Python 本身也是这样。如果你有不同的看法,欢迎继续讨论。
至于这个方法在 Python 风格上是否合适,有一些积极的回答,但没有给出具体的理由。看了《Python 之禅》(import this
),我觉得在这种情况下最重要的原则是“显式优于隐式”。不过,我不太确定这个原则是支持还是反对这种重新分类的方法。
使用
{has,get,set}attr
似乎更显式,因为我们明确地对对象进行了修改,而不是使用魔法。使用
__class__ = newclass
似乎也更显式,因为我们明确地说“这个现在是 'newclass' 类的对象,期待不同的行为”,而不是默默地改变属性,让使用这个对象的人以为他们在处理一个旧类的普通对象。
总结一下:从技术角度来看,这个方法似乎没问题;至于 Python 风格的问题,答案仍然没有定论,但倾向于“是”。
我接受了 Martin Geisler 的回答,因为 Mercurial 插件的例子非常有说服力(而且它还回答了我自己都没问的问题)。不过,如果有人对 Python 风格的问题有不同的看法,我还是很想听听。谢谢大家的帮助。
附言:实际的使用案例是一个 UI 数据控制对象,需要在运行时增加额外的功能。不过,这个问题是想要很一般化的讨论。
8 个回答
这没问题。我用过这个方法很多次了。不过,有一点需要注意的是,这个想法和老式类以及一些C语言扩展不太兼容。通常这不会成为问题,但因为你在使用一个外部库,所以你得确保自己没有在处理任何老式类或C语言扩展。
我不太确定在这种情况下使用继承是否是最好的选择(至少在“重新分类”方面)。听起来你走在正确的道路上,但我觉得使用组合或聚合会更合适。下面是我想到的一个例子(这是未经测试的伪代码):
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
像这样重新分类实例的操作是在Mercurial(一个分布式版本控制系统)中进行的。当一些扩展(插件)想要改变表示本地仓库的对象时,就会用到这个方法。这个对象叫做repo
,最开始是一个localrepo
实例。这个对象会依次传递给每个扩展,当需要的时候,扩展会定义一个新的类,这个类是repo.__class__
的子类,并且会把repo
的类改成这个新的子类!
在代码中,它看起来是这样的:
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
的子类。最后,这个函数会像你在代码中做的那样,改变实例的类。
我希望这段代码能展示这个语言特性的一个合理用法。我觉得这是我见过的唯一一个在实际应用中使用它的地方。