【问题标题】:Why isn't the __new__() method of my class returning an instance of the class?为什么我的类的 __new__() 方法不返回该类的实例?
【发布时间】:2020-12-05 07:11:53
【问题描述】:

我试图找出一种方法来创建预先确定的列表,以避免列表对象的限制,例如无法插入到列表末尾之外。所以这段代码按照我想要的方式构建列表,但是直到我开始实现 __str__() 方法时我才注意到这个 to string 方法实际上从未被调用过。

似乎是这个类正在创建list,而不是它被调用和定义的ArrayList。我的ArrayList 班级去哪儿了?所以,为了从课堂上调用__str__(),我必须暴力破解它,这确实违背了目的,而且我还想在混合中添加其他不会直接调用的方法,我敢肯定。

所以,我想知道是否可以采取一些措施来解决此问题,或者可能有更好的方法。我也不打算使用另一个现有的类,例如用于数组。在这一点上,这是教学法,我希望继续朝着同样的方向前进。这是我的代码:

import copy

class ArrayList(list):
    def __init__(self, *dim, initial = []):
        pass

    def __new__(cls, *dim, initial=[]):
        template = None
        for d in range(len(dim)-1, -1, -1):
            if template is None:
                template = [initial for _ in range(dim[d])]
            else:
                template = [copy.deepcopy(template) for _ in range(dim[d])]
        return template

    def __str__(self):
        print('using override')
        print(len(self))
        for i in range(len(self)):
            print(self[i])
        return

var1 = ArrayList(4,3,2,initial=0)
print(var1)
print(type(var1))   # <class 'list'>
print(var1 is ArrayList)    #False
print(ArrayList.__str__(var1))

# var2 = ArrayList(3,2,2,initial='a')
# var2[1][0][1] = 5
# print(var2)
# print("~"*20)
# var1[2][2] = 17
# print(*var1, sep='\n')
# print(len(var1))
# print("~"*20)
# print(ArrayList.__str__(var1))  #this works

【问题讨论】:

  • “返回的对象应该是类的类型” - 如你应该让它成为那个类型,通常通过调用super.__new__(yourclass, ...)。它不会接受你返回的任何东西并以某种方式改变它的类型。 (从技术上讲,您也可以从__new__ 返回任何类型,而不是您应该在这里这样做)。
  • 您为什么使用__new__() 方法而不是__init__()?请注意,在您的情况下,__new__() 返回的任何内容都会分配给var1
  • 这看起来更像是__init__ 的工作,而不是__new____new__ 通常用于(逻辑上或物理上)不可变对象。
  • 如果我可以直接分配给“self”,而不仅仅是 self 的属性,我会使用 init。那么,有没有办法在 init 中真正做到这一点?如何从列表对象创建方数组并仅返回方数组?我不希望返回类似于“self.yourArray”之类的东西,但就像“self”一样,我认为该类定义了对象类型。
  • 我将 'return' 更改为 ``` return list.__new__(cls, template) ''' 并且 "str" 现在可以工作并且类型反映为 " ArrayList”,非常感谢。它仍然在“var1 is ArrayList”上显示错误,但“is”应该是“isinstance”,这也有效!非常感谢!

标签: python python-3.x list class


【解决方案1】:

您可以使用collections.UserList。它用于子类列表。这将允许您使用相同的代码在__init__() 中分配给self.data,并避免__new__() 的问题:

from collections import UserList
import copy

class ArrayList(UserList):
    def __init__(self, *dim, initial = []):
        self.data = None
        for d in range(len(dim)-1, -1, -1):
            if self.data is None:
                self.data = [initial for _ in range(dim[d])]
            else:
                self.data = [copy.deepcopy(self.data) for _ in range(dim[d])]

    def __str__(self):
        return '\n'.join(str(s) for s in self)

var1 = ArrayList(4,3,2,initial=0)
print(var1)
print(type(var1))   # <class 'list'>
print(isinstance(var1, ArrayList))

打印:

[[0, 0], [0, 0], [0, 0]]
[[0, 0], [0, 0], [0, 0]]
[[0, 0], [0, 0], [0, 0]]
[[0, 0], [0, 0], [0, 0]]
<class '__main__.ArrayList'>
True

请注意,文档建议:

UserList 的子类应该提供一个构造函数,它可以 不带参数或只带一个参数调用...

...如果派生类不希望遵守此要求,则需要重写该类支持的所有特殊方法

你在这里没有做什么(尽管这对你的目的可能无关紧要)。

此外,如果您还没有考虑过,使用 Numpy 可能会更容易:

import numpy as np
np.zeros([4, 3, 2])

【讨论】:

  • 感谢您的回答。我先看了,但我想知道“self.data”是否是一个特殊属性,用于我如何考虑使用“self”或者它只是另一个属性?我喜欢这个解决方案,它可能更适合实际项目。还是仅适用于“UserList”的东西?无论如何,谢谢你的这种方法。
  • @JeffreyFlynt self.dataUserList 类的记录属性。 The instance’s contents are kept in a regular list, which is accessible via the data attribute of UserList instances.
【解决方案2】:

我认为你没有正确使用__new__:这个神奇方法的工作应该是处理某个类的新实例的创建(这就是它不应该返回另一个对象的原因type),而__init__ 应该处理它的初始化。

正确的做法是完全忽略__new__ 方法(我鼓励您继续这样做,直到您明确发现自己处于无法使用__init__ 解决问题的情况),并且而是使用后者。

在您的示例中,这只是意味着将代码从 __new__ 移动到 __init__,并在 self 上工作,而不是在您创建的新对象上工作。

但是您可能对如何使用它们感到困惑,因为几乎没有其他错误可能会导致困难:

  1. 让我们从简单的东西开始:在您的评论块中,您调用var1 is ArrayListis 操作符检查对象的类型。您应该将其视为更强大的 == 运算符(区别在于后者允许通过重载自定义行为,前者只是检查两个操作数是否是 same 对象 - 相同 id)。
  2. 我不清楚ArrayListlist 继承是否有用:也许这就是您在更高级/更复杂的程序中会做的事情,但实际上它不会给您带来任何东西(即使我要说的是非常反pythonic,但它仍然是真的):大多数list-like 类型实际上并不从它继承,它们只是在内部存储列表(或其他更复杂的集合)并显示类似于lists。例如,查看 Python 文档中的 __getitem____setitem__,这很可能是您需要的。
  3. 最后,这是一个更微妙的错误,由于一个非常愚蠢的 (IMO) python“功能”,你会得到无处不在的错误,即:你的构造函数中的 initial 变量被考虑 static by python (即使静态变量不存在,诀窍是它实际上只创建一次,并且在创建时您不会对其进行深层复制,因此每当您修改一个单个项目,繁荣,所有其他项目都将采用该值)。

为了实用起见,我将举几个例子来说明如何实现你想要的。

首先,“最好”的方式(在某种意义上,当您变得更有经验时应该使用的方式):

import numpy
var1 = numpy.ndarray(4, 3, 2)
var1[1,0,1] = 5
# equivalent to
var1[1][0][1] = 5
# to initialize the whole array to some value
var1[:,:,:] = 0

DIY 方式(仅用于教育目的):

class Array:
    def __init__(self, *dims, default=0):
        if len(dims) == 0:
            raise TypeError("Cannot have 0 dimentional arrays")
        elif len(dims) == 1:
            self._array = [default for i in range(dims[0])]
        else:
            self._array = [Array(*dims[1:], default=default) for i in range(dims[0])]
    def __getitem__(self, key):
        return self._array[key]
    def __setitem__(self, key, value):
        self._array[key] = value
    def __repr__(self):
        return '[' + ', '.join(repr(e) for e in self._array) + ']'

您当然不想创建多个数组,但是有很多方法可以改进这一点。只是玩弄它以很好地理解它。

编辑

让我们取消一些要点:

I/ 静态变量:即 90% 的时间,一个非常微妙的错误,违反常识,很难调试

假设我想创建一个自定义列表类,但为了简单起见,假设我只能获取项目和追加,并且在初始化时(可选地)传递实际列表:

class MyList:
  def __init__(self, mylist=[]):
    self.mylist = mylist
  def __getitem__(self, key):
    return self.mylist[key]
  def append(self, value):
    self.mylist.append(value)

好吧,这段代码错误!以下是可能发生的情况:

a = MyList()
b = MyList()
a.append(3)
a[0] == 3 # true
b[0] == 3 # true?? wtf?

其实a.mylistb.mylist是同一个对象!我并不是说它是平等的,我的意思是它们确实共享相同的内存部分,改变一个会改变另一个。这是因为只有一个列表被创建过,那就是MyList 类被定义时,甚至在a 被创建之前!

正确的代码应该是这样的:

class MyList:
  def __init__(self, mylist=None):
    if mylist == None: self.mylist = []
    else: self.mylist = mylist
  # the rest of the code

II/ 重载__new__魔术方法而不实际创建对象

初学者不理解__new____init__之间的区别是很常见的,这是完全正常的:这是因为python在幕后做了很多事情,而对于python来说,它们之间有很大的不同两个,但对我们来说,它们都是在创建对象之前用相同的参数调用的,所以很混乱。让我们潜入!

__new__ 确实从无到有构建了一个新对象,这意味着您必须分配内存并返回新创建的对象。你不想那样做,除非你知道自己在做什么并且有这样做的很好的动机,对吧?

另一方面,

__init__ 负责初始化新创建的对象,这就是为什么它在__new__ 之后立即调用,并且不必返回任何内容:新对象已经创建。这是您想要将填充添加到新列表或任何您需要的地方。另一种魔术方法只有创建对象的目的(意思是,获取内存,说python“嘿,我存在”等)。

因此,在您写的评论中,新创建的对象是 None 对象。这很可能是因为您重载了__new__,但没有返回任何内容。我的建议是暂时忘记使用__new__,您将要做的大部分事情都不需要它。

【讨论】:

  • 我用过一点numpy,但列表不一定是纯数字的。参数中的“初始”变量仅在__new__中使用,也是“静态的”。所以,不仅我认为这无关紧要。你提出的第二点是从“列表”继承有什么好处,也许不多,但我打算添加方法来“填充”一个列表,这样一个有五个元素的列表就可以有一个元素[100]插入插槽 100,并用我想要的填充。这些都不一定是数字,我希望列表中的所有其他方法都能正常工作,再加上一些方法来改进......
  • 另外,我无法让该代码在 __ init __ 方法中工作,因为我必须将结果分配给“self”。当我这样做时,由于我不知道的原因,它返回了一个 NoneObject,IIRC。似乎您可以分配给“自我”,但它在类代码之外丢失了。现在,另一个人建议使用具有“self.data”并且有效的特殊类。到目前为止,这对我来说有点神秘,但我正在咬牙切齿。
  • 好吧,如果你只想填充列表,你应该直接为列表类创建一个类方法
  • 而且,顺便说一句,方法变量是静态的与方法本身是静态的完全不同的行为:关键是,只要函数声明中有静态数据,你很可能有一个错误。如果你愿意,我可以编辑我的帖子,为你提供一个可能出现的奇怪行为的例子
  • 哦,我也刚刚注意到for d in range(len(dims)-1, -1, -1)。您可以简单地将其更改为for dimention in reversed(dims),而不是访问dims[d],直接使用dimension
猜你喜欢
  • 1970-01-01
  • 2012-02-23
  • 1970-01-01
  • 1970-01-01
  • 2020-09-20
  • 2012-07-24
  • 1970-01-01
  • 2012-04-29
  • 1970-01-01
相关资源
最近更新 更多