58

我有一个由外部库提供给我的类。我创建了这个类的一个子类。我也有一个原始类的实例。

我现在想将此实例转换为我的子类的一个实例,而不更改该实例已有的任何属性(无论如何我的子类覆盖的属性除外)。

以下解决方案似乎有效。

# 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 个)认为它是一种有效的方法。一个答案(由 ojrac 提供)说它“乍一看很奇怪”,我同意这一点(这是提出这个问题的原因)。只有一个答案(来自 Jason Baker;有两个正面评论和投票)积极劝阻我不要这样做,但是这样做是基于示例用例而不是一般技术。

  • 没有一个答案,无论是否积极,都无法在这种方法中找到实际的技术问题。一个小例外是 jls,他提到要提防旧式类(这很可能是真的)和 C 扩展。我认为这种新风格的类感知 C 扩展应该与 Python 本身一样好(假设后者是正确的),尽管如果你不同意,请继续给出答案。

至于这是多么pythonic的问题,有一些肯定的答案,但没有给出真正的理由。看看 Zen ( import this),我想在这种情况下最重要的规则是“显式优于隐式”。不过,我不确定该规则是否支持或反对以这种方式重新分类。

  • 使用{has,get,set}attr似乎更明确,因为我们明确地对对象进行更改而不是使用魔法。

  • 使用__class__ = newclass似乎更明确,因为我们明确地说“这现在是类 'newclass' 的对象,期待不同的行为”,而不是默默地改变属性,但让对象的用户相信他们正在处理旧类的常规对象。

总结:从技术角度来看,方法似乎还可以;pythonicity问题仍然没有答案,偏向于“是”。

我接受了 Martin Geisler 的回答,因为 Mercurial 插件示例是一个非常强大的示例(也因为它回答了一个我什至还没有问过自己的问题)。但是,如果对 pythonicity 问题有任何争论,我仍然想听听。感谢所有到目前为止。

PS 实际用例是一个 UI 数据控件对象,它需要在运行时增加额外的功能。但是,这个问题是非常笼统的。

4

8 回答 8

19

当扩展(插件)想要更改代表本地存储库的对象时,在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。最后,该函数更改了 instanceø 的类,就像您在代码中所做的那样。

我希望这段代码可以显示该语言功能的合法使用。我想这是我唯一见过它在野外使用的地方。

于 2009-06-13T18:47:45.290 回答
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

如果您不熟悉集合任意参数列表,您可能需要阅读更多关于它们的信息才能获得此示例。

于 2009-06-13T15:31:03.763 回答
3

这可以。这个成语我用过很多次了。不过要记住的一件事是,这个想法不适用于旧式类和各种 C 扩展。通常这不是问题,但由于您使用的是外部库,您只需要确保您没有处理任何旧式类或 C 扩展。

于 2009-06-13T18:13:46.760 回答
2

“状态模式允许对象在其内部状态发生变化时改变其行为。对象似乎会改变它的类。” - 头部优先设计模式。写 Gamma 等人的东西非常相似。在他们的设计模式书中。(我在其他地方有,所以没有报价)。我认为这就是这种设计模式的全部意义所在。但是如果我可以在运行时更改对象的类,大多数时候我不需要模式(在某些情况下,状态模式不仅仅模拟类更改)。

此外,在运行时更改类并不总是有效:

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 并不时使用它。

于 2010-07-19T20:31:20.480 回答
1

呵呵,有趣的例子。

乍一看,“重新分类”很奇怪。'复制构造函数'方法呢?您可以使用 Reflection-like 和 来做到hasattrgetattr一点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

所有这些反射并不漂亮,但有时你需要一个丑陋的反射机器来制作很酷的东西。;)

于 2009-06-13T14:43:34.173 回答
1

这种技术对我来说似乎相当Pythonic。组合也是一个不错的选择,但是分配到__class__是完全有效的(参见此处以获取以稍微不同的方式使用它的配方)。

于 2009-06-13T15:53:16.957 回答
0

在 ojrac 的回答中, -loopbreak中断for并且不再测试任何属性。我认为只使用if- 语句一次决定如何处理每个属性,然后继续遍历for所有属性的 - 循环更有意义。否则,我喜欢 ojrac 的回答,因为我也认为分配给__class__很奇怪。(我是 Python 的初学者,据我所知,这是我在 StackOverFlow 上的第一篇文章。感谢您提供的所有重要信息!!)

所以我试图实现它。我注意到 dir() 没有列出所有属性。 http://jedidjah.ch/code/2013/9/8/wrong_dir_function/ 所以我添加了“”、“文档”、“模块”和“初始化”到要添加的内容列表中,如果它们还没有的话, (尽管它们可能都已经存在了),并且想知道是否还有更多的东西 dir 错过了。我还注意到,在说这很奇怪之后,我(可能)分配给了“班级”。

于 2014-05-15T17:10:40.237 回答
-2

我会说这很好,如果它适合你。

于 2009-06-13T15:21:50.307 回答