【问题标题】:Reclassing an instance in Python在 Python 中重新分类实例
【发布时间】:2010-11-02 17:01:14
【问题描述】:

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

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

以下解决方案似乎有效。

# 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 提出;有两个正面的 cmets 和投票)积极劝阻我不要这样做,但是这样做是基于示例用例而不是一般技术。

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

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

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

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

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

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

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

【问题讨论】:

  • 很好的例子。您能否解释一下在哪种情况下您觉得这种模式很有用?我喜欢这个主意,但我不确定它是否有用:)
  • @NicDumZ:谢谢你的提问;它在编辑中得到了回答。
  • +1 用于示例代码和对问题的广泛工作。很高兴看到。
  • 为了记录,我不一定不鼓励使用重新分类。然而,它给了我(显然你在开始这个话题时)足够糟糕的感觉,我建议避免它,除非有令人信服的理由。请记住,Mercurial 在设计时就考虑到了这种类型的东西。你使用的任何库都可能不是。
  • 谢谢杰森;我就是这样回答你的。我会记住这一点。到目前为止,自尝试以来我还没有遇到任何问题,但我会小心的。

标签: subclass python


【解决方案1】:

当扩展(插件)想要更改代表本地存储库的对象时,在Mercurial(分布式修订控制系统)中完成这样的重新分类实例。该对象称为repo,最初是localrepo 实例。它依次传递给每个扩展,并且在需要时,扩展将定义一个新类,它是repo.__class__ 的子类,并将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ø 的类,就像您在代码中所做的那样。

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

【讨论】:

    【解决方案2】:

    我不确定在这种情况下最好使用继承(至少在“重新分类”方面)。看起来你走在正确的轨道上,但听起来组合或聚合最适合这个。这是我正在考虑的一个示例(在未经测试的伪类代码中):

    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 以获得此示例,您可能需要阅读有关它们的更多信息。

    【讨论】:

    • +1 在处理由外部 API 提供的对象时,包装总是更好。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

      “状态模式允许对象在其内部状态发生变化时改变其行为。对象似乎会改变它的类。” - 头部优先设计模式。写 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 并不时使用它。

      【讨论】:

        【解决方案5】:

        呵呵,有趣的例子。

        乍一看,“重新分类”很奇怪。那么“复制构造函数”方法呢?您可以使用类似反射的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
        

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

        【讨论】:

          【解决方案6】:

          这种技术对我来说似乎相当 Pythonic。组合也是一个不错的选择,但分配给__class__ 是完全有效的(请参阅here 以了解以稍微不同的方式使用它的配方)。

          【讨论】:

            【解决方案7】:

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

            所以我尝试实现它。我注意到 dir() 没有列出所有属性。 http://jedidjah.ch/code/2013/9/8/wrong_dir_function/ 所以我添加了 'class'、'doc'、'module' 和 'init'如果它们不存在,则要添加的内容列表(尽管它们可能都已经存在),并想知道是否还有更多的东西 dir 错过了。我还注意到,在说这很奇怪之后,我(可能)分配给了“class”。

            【讨论】:

              【解决方案8】:

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2013-05-13
                • 2017-05-15
                • 2019-04-02
                • 2020-10-14
                • 2021-07-17
                • 1970-01-01
                • 1970-01-01
                • 2019-05-07
                相关资源
                最近更新 更多