在Python中重新分类实例

时间:2009-06-13 14:17:42

标签: subclass 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__感觉不对。即使这样有效,我也无法理解应该有更多的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数据控件对象,需要在运行时增加其他功能。但是,问题是非常笼统的。

8 个答案:

答案 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

如果您不熟悉这些示例,则可能需要详细了解setsarbitrary 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,有趣的例子。

乍一看,“重新分类”非常奇怪。那么'复制构造函数'方法呢?您可以使用类似反射的hasattrgetattrsetattr执行此操作。此代码将从一个对象复制到另一个对象,除非它已经存在。如果您不想复制方法,可以将它们排除;请参阅评论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)

我会说这完全没问题,如果它适合你。