为什么有时候__new__之后不会调用__init__方法?

15

首先声明,这不是为什么没有参数调用__new__时__init__不会被调用的重复问题。我已经努力构建了一些样例代码来使用__new____init__,但无法找到任何解释。

基本要求:

  • 有一个名为NotMine的基类,因为它来自另一个库(我会在最后透露,这里不重要)
  • 该类具有一个__init__方法,该方法又调用_parse方法
  • 我需要在子类中覆盖_parse方法
  • 创建哪个子类在调用时是未知的
  • 我知道有工厂设计模式,但我不能在这里使用它们(更多内容在结尾处)
  • 我尝试了谨慎使用super以避免Python日志记录:为什么__init__会被调用两次?中出现的问题
  • 我知道这也是“抽象基础方法”的机会,但那并没有帮助

总之,__init__应该在__new__之后被调用。对于为什么下面的一些示例不起作用的每个解释,我似乎都能指向其他有效的案例并排除该解释。

class NotMine(object):

    def __init__(self, *args, **kwargs):
        print "NotMine __init__"
        self._parse()

    def _parse(self):
        print "NotMine _parse"

class ABC(NotMine):
    def __new__(cls,name,*args, **kwargs):
        print "-"*80
        print "Entered through the front door ABC.__new__(%s,%s,*%s,**%s)"%(cls,name,args,kwargs)
        if name == 'AA':
            obj = super(NotMine,ABC).__new__(AA,*args,**kwargs)
            print "Exiting door number 1 with an instance of: %s"%type(obj)
            return obj 
        elif name == 'BB':
            obj = super(NotMine,ABC).__new__(BB,*args,**kwargs)
            print "Exiting door number 2 with an instance of: %s"%type(obj)
            return obj
        else:
            obj = super(NotMine,ABC).__new__(cls,*args,**kwargs)
            print "Exiting door number 3 with an instance of: %s"%type(obj)
            return obj

class AA(ABC):

    def _parse(self):
       print "AA _parse"

class BB(ABC):

    def __init__(self, *args, **kw):
        print "BB_init:*%s, **%s"%(args,kw)        
        super(BB,self).__init__(self,*args,**kw)

    def _parse(self):
        print "BB _parse"

class CCC(AA):

    def _parse(self):
        print "CCCC _parse"


print("########### Starting with ABC always calls __init__ ############")
ABC("AA")            # case 1
ABC("BB")            # case 2
ABC("NOT_AA_OR_BB")  # case 3

print("########### These also all call __init__ ############")
AA("AA")           # case 4
BB("BB")           # case 5
AA("NOT_AA_OR_BB") # case 6
BB("NOT_AA_OR_BB") # case 7
CCC("ANYTHING")    # case 8

print("########### WHY DO THESE NOT CALL __init__ ############")
AA("BB")  # case 9  
BB("AA")  # case 10
CCC("BB") # case 11
如果你执行该代码,你会发现每次调用__new__都会宣布它正在通过哪个“门”以及使用哪种类型进行退出。在同一个“门”上使用相同的“类型”对象可以在某些情况下调用__init__而在其他情况下则不会。我查看了“调用”类的MRO,但没有发现任何有见地的地方,因为我可以调用该类(或子类,例如CCC)并调用__init__

注: 我使用的NotMine库是Genshi MarkupTemplate,不使用工厂设计方法的原因是他们的TemplateLoader需要一个defaultClass来构造。而我要在解析之前才能知道,而这正是我在__new__中做的。genshi加载器和模板的许多酷炫的巫术使得这个值得努力去做。

我可以运行一个未修改的实例,并且只要将ABC(抽象的、类似于工厂的)类作为默认类传递,一切都正常工作。但这个无法解释的行为几乎肯定会成为以后的bug。

更新: Ignacio已经理解了最初的问题,如果返回的对象不是cls的“实例”,那么将不会调用__init__。我发现调用“构造器”(例如AA(args..))是错误的,因为它会再次调用__new__,将你带回到起点。您可以修改参数以走不同的路径。这意味着您调用ABC.__new__两次而不是无限次。一种有效的解决方案是将上面的class ABC更改为:

class ABC(NotMine):
  def __new__(cls,name,*args, **kwargs):
    print "-"*80
    print "Entered through the front door ABC.__new__(%s,%s,*%s,**%s)"%(cls,name,args,kwargs)
    if name == 'AA':
        obj = super(NotMine,ABC).__new__(AA,*args,**kwargs)
        print "Exiting door number 1 with an instance of: %s"%type(obj)
    elif name == 'BB':
        obj = super(NotMine,ABC).__new__(BB,*args,**kwargs)
        print "Exiting door number 2 with an instance of: %s"%type(obj)
    elif name == 'CCC':
        obj = super(NotMine,ABC).__new__(CCC,*args,**kwargs)
        print "Exiting door number 3 with an instance of: %s"%type(obj)
    else:
        obj = super(NotMine,ABC).__new__(cls,*args,**kwargs)
        print "Exiting door number 4 with an instance of: %s"%type(obj)
    ## Addition to decide who calls __init__  ##
    if isinstance(obj,cls):
        print "this IS an instance of %s So call your own dam __init__"%cls
        return obj
    print "this is NOT an instance of %s So __new__ will call __init__ for you"%cls
    obj.__init__(name,*args, **kwargs)
    return obj

print("########### now, these DO CALL __init__ ############")
AA("BB")  # case 9  
BB("AA")  # case 10
CCC("BB") # case 11

注意最后几行。如果一个类是“不同”的类,而不调用__init__对我来说没有意义,尤其是当“不同”的类仍然是调用__init__的类的子类时。我不喜欢上面的修改,但至少现在我更好地理解了规则。


Genshi是否使用元类?请参见https://dev59.com/WnVD5IYBdhLWcg3wE3Ro - Borealid
不,我的示例代码没有使用genshi作为基础。 - Phil Cooper
2个回答

19

来自文档:

如果 __new__() 不返回 cls 的实例,则新实例的 __init__() 方法将不会被调用。

这是为了允许 __new__() 返回一个不同类的新实例,该实例具有其自己的 __init__() 被调用。如果不是创建新的 cls,则需要检测并调用适当的构造函数。


你应该在__new__()实例化它,通过调用它的构造函数。 - Ignacio Vazquez-Abrams
那么你在clsname之间的检查是错误的。 - Ignacio Vazquez-Abrams
cls和name之间的检查并不是问题,构造函数似乎仍然有问题...请参见我上面的编辑。话虽如此,你确实准确地回答了我的顶部问题,让我重新回到了正轨。谢谢! - Phil Cooper
我不同意你的修复,有两个原因:1)CCC('CCC') 将无限递归; 2) 存在大量重复代码。更好的做法是基于参数选择新类,然后检查 cls 和新类是否相同。 - Ignacio Vazquez-Abrams
回复1:你说得完全正确。我留下了测试用例,现在我已经编辑了修复程序,感谢你。 回复2:“更好”是主观的。 “检查cls和新类是否相同”? obj可能是cls的子类的实例,或者super __new__可能返回其他内容。 无论如何,我的实际代码还做了其他事情(加载模块等),这使得上述修复程序“更好”...对于我来说...对于这种情况...我认为 - Phil Cooper
显示剩余2条评论

-1

这只是我的个人看法,但为什么不使用 Python 的鸭子类型来提供类似类的行为给 Genshi 呢?

我快速查看了 Genshi 源代码,我所看到的“class”参数在TemplateLoader上的唯一要求是它能够调用给定的参数。

我认为通过工厂函数返回实际创建的实例更容易模拟一个类。


是的,在源代码深处,它最终调用了默认类的构造函数,该类可以是任何可调用对象。我认为这将是一个可行的替代方案。未来可能会有所不同,并且“似乎”传递MarkupTemplate的子类是正确的面向对象的做法。但是又一次,对我来说,__new__ 应该表现得不同,那么我算什么呢 ;-) - Phil Cooper
这似乎没有回答所提出的问题。 - Bryan Oakley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接