在Python中重新分类实例

59 投票
8 回答
10629 浏览
提问于 2025-04-15 12:13

我有一个类,是从外部库里得到的。我为这个类创建了一个子类。同时,我也有一个这个原始类的实例。

现在我想把这个实例变成我子类的实例,但不想改变这个实例已经拥有的任何属性(除了那些我子类本身会覆盖的属性)。

下面这个方法似乎可以实现。

# 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 个回答

3

这没问题。我用过这个方法很多次了。不过,有一点需要注意的是,这个想法和老式类以及一些C语言扩展不太兼容。通常这不会成为问题,但因为你在使用一个外部库,所以你得确保自己没有在处理任何老式类或C语言扩展。

11

我不太确定在这种情况下使用继承是否是最好的选择(至少在“重新分类”方面)。听起来你走在正确的道路上,但我觉得使用组合或聚合会更合适。下面是我想到的一个例子(这是未经测试的伪代码):

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

如果你对集合可变参数列表不太熟悉,可能需要多了解一下这些内容,以便理解这个例子。

20

像这样重新分类实例的操作是在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的子类。最后,这个函数会像你在代码中做的那样,改变实例的类。

我希望这段代码能展示这个语言特性的一个合理用法。我觉得这是我见过的唯一一个在实际应用中使用它的地方。

撰写回答