为什么Fraction使用__new__而不是__init__?

9

我正在尝试创建一个新的不可变类型,类似于内置的Fraction,但不是从它派生而来。 Fraction类是这样创建的:创建Fraction类

# We're immutable, so use __new__ not __init__
def __new__(cls, numerator=0, denominator=None):
    ...
    self = super(Fraction, cls).__new__(cls)
    self._numerator = ...
    self._denominator = ...
    return self

但我不明白这与其他有何不同。
def __init__(self, numerator=0, denominator=None):
    ...
    self._numerator = ...
    self._denominator = ...

使用相同值创建2个Fraction对象不会创建指向同一对象/内存位置的2个标签(实际上评论中指出这不是常见情况)

尽管源代码中有注释,但它们实际上并不是不可变的:

f1 = Fraction(5)

f2 = Fraction(5)

id(f1), id(f2)
Out[35]: (276745136, 276745616)

f1._numerator = 6

f1
Out[41]: Fraction(6, 1)

f2
Out[42]: Fraction(5, 1)

id(f1)
Out[59]: 276745136

那么这种方式的目的是什么?
文档中提到:
__new__()主要用于允许不可变类型(例如int、str或tuple)的子类自定义实例创建。它也通常在自定义元类中被覆盖,以便自定义类创建。
所以,如果我不是子类化内置类型,但我正在从头开始创建一个不可变类型(继承自object),我是否仍然需要使用它?

2
具有相同值的内置对象并不总是相同的实例。小整数是特殊情况。如果您尝试使用int(1000)而不是int(5)进行上述测试,则会发现i1 is i2为false。 - John Kugelman
3
如果需要的话,可以使“new”返回一个已存在的对象,而不是创建一个新对象。这样,如果您尝试两次构建它,您将得到相同的具有相同ID的对象。这并不一定是有益的,并且大多数内置类都不会这么做。 - khelwood
@khelwood 好的,我以为它们都会这样做。这样做有什么好处或不好处?我认为创建多个具有相同不可变值的相同等效对象没有任何好处。难道它们不只是浪费内存和内存访问时间吗? - endolith
2
@endolith 因为要实现这个功能,你必须保留所有可能需要返回的对象的引用。假设你跟踪每个实例化的对象以防需要实例化相同的对象 - 那么这些对象都不能被垃圾回收,并且每次新的实例化都需要更长的时间,因为你必须搜索缓存以查看它是否包含你想要的对象。 - khelwood
@khelwood 哦,我明白了,这就是为什么内置的 int 类型对于小的常见整数会这样做,但不会对大的不常见整数这样做。 - endolith
1个回答

5
如果你正在创建真正的不可变类型,那么应该使用__new__,因为传递到__init__中的< strong >self对象在逻辑上已经是不可变的,所以将其成员赋值给它已经太晚了。对于编写子类的人来说,这更加严格,因为添加成员将被禁止。
由于不可变性实际上并不是一种固有属性,而是一种技巧,通常通过挂接__setattr__来强制执行,因此人们确实编写了使用__init__进行初始化然后通过设置某些成员使自身成为不可变的不可变类型。但是,在这种情况下,逻辑可能会变得非常错综复杂,并且__setattr__可能会充斥着额外规则。
更有意义的做法是拥有某种可变类型,并从中继承不可变类型,并在子类中包含一个只 raise异常的版本__setattr__。这使得使用__new__的逻辑变得明显。由于它可以使可变的超类并修改它,然后将其作为继承类型返回,因此这样做不会让人感到困惑。
如果Fraction意图是不可变的,则实现者可能会错过这一步,或者后来考虑得更好,但忘记删除他们的注释。
>>> class Pair(object):
...     def __init__(self, key, value):
...         self.key = key
...         self.value = value
...
>>> class ImPair(Pair):
...     def __new__(cls, key, value):
...         self = Pair(key, value)
...         self.__class__ = cls
...     def __setattr__(self, name, value):
...         raise AttributeError(name)
...    
>>> x = Pair(2,3)
>>> x.key
2
>>> x.key = 9
>>> x.key
9
>>> x = ImPair(2,3)
>>> x.key
2
>>> x.key = 9
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __setattr__
AttributeError: key
>>>

似乎在语言之外编写如此简单的东西是不符合Python风格的。显式优于隐式... - Jon Jay Obermark
C或Python的其他实现语言中编写数据类型并没有什么隐含的意义。这样做最大的原因可能是性能(纯Python版本可能非常慢)。 - Ethan Furman
1
而且“c”实现仍将具有映射到__setattr__的函数。 实现方式将使该对象可变或不可变。 因此,您最初的评论对我来说毫无意义。 无论您用什么语言编写它,都没有更明确的方法将某些内容定义为Python解释器中的不可变对象。 - Jon Jay Obermark
我并不是说技巧是不好的。当它们增加了意外的优雅和强大时,它们是有用的。在编译器级别上将不可变性的单独实现作为特殊情况分离出来,正如大多数语言所处理的那样,会使Python无价值或者不那么强大。 - Jon Jay Obermark
@JonJayObermark,你在ImPair.__new__()中不需要return self吗?我的理解是__new__()需要返回一个构造好的对象,对吧? - Ray
显示剩余2条评论

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