Python中的__new__和__init__

82 投票
3 回答
39731 浏览
提问于 2025-04-17 06:10

我正在学习Python,到目前为止,我对__new____init__有以下几点理解:

  1. __new__是用来创建对象的。
  2. __init__是用来初始化对象的。
  3. __new____init__之前被调用,因为__new__会返回一个新的实例,而__init__是在之后被调用来初始化内部状态。
  4. __new__适合不可变对象,因为一旦赋值就不能改变。所以我们可以返回一个具有新状态的新实例。
  5. 我们可以同时使用__new____init__来处理可变对象,因为它们的内部状态是可以改变的。

但我现在有其他问题。

  1. 当我创建一个新实例,比如a = MyClass("hello","world"),这些参数是怎么传递的?我的意思是,我应该如何构建这个类,使用__init____new__,因为它们是不同的,并且都接受除了默认第一个参数之外的任意参数。
  2. self这个关键词的名字可以换成别的吗?我在想cls这个名字也能换成别的吗?因为它只是一个参数名。

我做了一些小实验,如下:

>>> class MyClass(tuple):
    def __new__(tuple):
        return [1,2,3]

然后我做了以下操作:

>>> a = MyClass()
>>> a
[1, 2, 3]

虽然我说我想返回tuple,但这段代码运行得很好,给我返回了[1,2,3]。我知道我们在调用__new__函数时传递了第一个参数,作为我们想要接收的类型。我们是在讨论New函数,对吧?我不知道其他语言的返回类型是否有不同于绑定类型的。

我还做了其他事情:

>>> issubclass(MyClass,list)
False
>>> issubclass(MyClass,tuple)
True
>>> isinstance(a,MyClass)
False
>>> isinstance(a,tuple)
False
>>> isinstance(a,list)
True

我没有做更多实验,因为后面的内容不太明朗,我决定停下来,去问StackOverflow。

我阅读的SO帖子:

  1. Python对象创建
  2. Python中__new____init__的用法?

3 个回答

-1

你的问题大概是这样的:

在Python中,__new____init__有什么区别?


假设我们有一个非常简单的类,叫做Rectangle(矩形)。

class Rectangle:
    def __init__(self, width:float, height:float, /):
        self._width  = width
        self._hieght = height
        # implicitly return None 

我们可以这样创建Rectangle类的实例:

rocky_the_rectangle = Rectangle(10.01, 8.91)

上面的代码创建了一个矩形,它的:

width = 10.01
height = 8.91

为了构造这个矩形,Python解释器做了类似下面的事情:

rocky_the_rectangle = Rectangle.__new__(10.01, 8.91)
if isinstance(rocky_the_rectangle, Rectangle):
    Rectangle.__init__(rocky_the_rectangle, 10.01, 8.91)

注意,__init__只能返回None

如果你把rocky_the_rectangle传给__init__,那么__init__会做以下事情:

  1. 修改原来的rocky_the_rectangle。它不会是rocky的一个副本。

  2. __init__的输出,或者说返回值,将是None


关于__new__,以下几点是正确的:

  • __new__可以返回除矩形类的新实例以外的其他东西。

  • __new__可以读取和写入矩形类的属性。矩形类在__init__内部并不是直接可见的。

以上就是大部分内容了。不过,如果你还是感到困惑,继续往下看。


下面,我们有一个__init__方法和一个__new__方法。

class Rectangle:
    def __new__(cls, *args, **kwargs, /):
        obj = super().__new__(cls)
        return obj

    def __init__(self, width:float, height:float, /):
        self._width = width
        self._height = height

当我们运行这段代码时,可能很难看清楚发生了什么。

调试代码有很多方法,但一个快速简单的方法是添加一些打印语句,这样可以帮助我们看到函数调用的顺序,以及输入参数是什么。

def see_whats_going_on(self_or_cls, *args, **kwargs):
    # Get the string version of the input arguments
    sargs = repr(str(arg))[] for arg in [self_or_cls, *args]

    # Example
    #     sargs = [
    #         "<__main__.Rectangle object at 0x7f7cf24be5d0>",
    #         "10.01",
    #         "8.91"
    #      ]    

    # Now, insert some commas and spaces in-between the args
    comma_seperated_values = ", ".join(sargs)
    csv = comma_seperated_values

    # Example
    #    csv is the following:
    #        "<__main__.Rectangle object at 0x7f7cf24be5d0>, 10.01, 8.91"

    # next, add some parentheses 
    stringed_function_inputs = "(" + csv + ")"

    return stringed_function_inputs 

让我们在代码中插入一些打印函数的调用:

class Rectangle():
    def __new__(cls, *args, **kwargs):
        see_whats_going_on(cls, *args)        
        obj = super().__new__(cls)
        return obj

    def __init__(self, width:float, height:float, /):
        args = (width, height)
        see_whats_going_on(cls, *args)
        self._width = width
        self._height = height

    @classmethod
    def make_rectangle(cls, *args):
        new_instance = cls.__new__(cls, *args)
        if isinstance(new_instance, cls):
            cls.__init__(new_instance, *args)
        return new_instance

现在我们可以这样创建Rectangle类的实例:

rocky_II = Rectangle.make_rectangle(10.01, 8.91)

__init__允许我们以原地修改的方式改变self的属性。

但是,__init__不允许通过将self传入一个返回修改后副本的函数来修改self

# NOT ALLOWED INSIDE OF __init__
new_self = paint_it_blue(old_self)

__init__方法无法用新的不同的东西替换self参数。

例如,有时人们想在self参数周围加一个包装器。你可能想要这样的效果:

import functools
import sys

class decorator:

    def __new__(cls, kallable):
        instance = super().__new__(cls)
        instance = functools.update_wrapper(instance, kallable)
        return instance

    def __init__(self, kallable):
        self._kallable = kallable
        self._file     = sys.stdout

    def __call__(self, *args, **kwargs):
        print("__init__(" + ", ".join(str(x) for x in [self, *args]) + ")", file=self._file)
        return self._kallable(*args, **kwargs)

@decorator
def pow(base:float, exp:int):
    """
         +------------------------------------------+
         |                EXAMPLES                  |
         +------------------------------------------+
         | BASE | EXPONENT |       OUTPUT           |
         +------+----------+------------------------+
         |    2 |        5 | 2^5      |          32 |
         |  2.5 |        7 | 2.5^7    | 610.3515625 |
         |   10 |        3 | 10^3     |        1000 |
         |  0.1 |        5 | 0.1^5    |     0.00001 |
         |    7 |        0 | 7^0      |           1 |
         +------+----------+----------+-------------+
    """
    base = float(base)
    # convert `exp` to string to avoid flooring, or truncating, floats
    exp  = int(str(exp))
    if exp > 0:
        return base * pow(base, exp-1)
    else: # exp == 2
        return 1

result1 = pow(2, 5)
result2 = pow(8.1, 0)
print("pow(2, 5) == "  , result1)
print("pow(8.1, 0) == ", result2)

print("What is the documentation string? The doc-string is... ", pow.__doc__)

__init__内部使用functools.update_wrapper是没有用的。如果你尝试写以下代码...

class f:
    def __init__(outter, inner):
        # `outter` is usually named `self`, but you are...
        #     ... allowed to use other names for it.  
        outter = functools.update_wrapper(outter, inner) 

...那么外部的内容会被忽略。你不能用一个不同的self替换名为self的参数。

__new__允许我们用一个包装器替换self

class decorator:

    def __new__(cls, kallable):
        instance = super().__new__(cls)
        instance = functools.update_wrapper(instance, kallable)
        return instance

__new__内部不使用functools.update_wrapper,原始可调用对象中的文档字符串将被清除、忽略、不继承或被遮蔽。

2

看起来没有人讨论过在实例化时参数是怎么处理的。

让我们来创建一个简单的类,定义一下newinit

censured = ["Cicciogamer"]

class Foo(object):
    def __new__(cls, name):
        if name in censured:
            print("you shouldn't do this")
        return super().__new__(cls)

    def __init__(self, var):
        self.var = var

当你调用一个类对象来获取一个实例时,Python会隐式地调用

Foo.__call__(*args, **kwargs)

所以使用上面的类,你可能会遇到不想要的双重参数传递:

foo = Foo("Cicciogamer")
>>> "you shouldn't do this"
foo.var
>>> "Cicciogamer"
    

为了控制这些参数的行为,按照我所知道的,你必须重写call方法,这个方法是针对类对象而不是实例的。

你可以使用 metaclasses(元类),但这可能有点复杂。

class MetaFoo:
    def __call__(cls, name, var):
        foo_object = cls.__new__(name)
        cls.__init__(foo_object, var)
        return foo_object

class Foo(metaclass=MetaFoo):
    ...


foo = Foo("Cicciogamer", 505)
>>> "you shouldn't do this"
foo.var
>>> 505

或者我觉得你可以简单地这样做:

class Foo:
    ...

    @classmethod
    def new(cls, name, var):
        foo_object = cls.__new__(name)
        cls.__init__(foo_object, var)
        return foo_object

    __call__ = new

如果有更好的方法来实现这个,或者我哪里搞错了,请告诉我。

69

我应该如何使用 __init____new__ 来构建类,因为它们是不同的,并且都接受除了默认的第一个参数之外的任意参数。

你很少需要担心 __new__。通常,你只需要定义 __init__,然后让默认的 __new__ 将构造函数的参数传递给它。

self 这个关键词的名字可以换成其他的名字吗?我在想 cls 这个名字是否也可以换,因为它只是一个参数名?

这两个其实都是参数名,在语言中没有特别的含义。不过在 Python 社区里,它们的用法是一个非常强的约定;大多数 Python 程序员在这些情况下不会改变 selfcls 的名字,如果有人这样做,他们会感到困惑。

注意,你使用 def __new__(tuple) 会在构造函数内部重新绑定名字 tuple。在实际实现 __new__ 时,你应该这样做:

def __new__(cls, *args, **kwargs):
    # do allocation to get an object, say, obj
    return obj

虽然我说我想返回 tuple,但这段代码运行得很好,并且返回了 [1,2,3]

MyClass() 将会得到 __new__ 返回的值。Python 中没有隐式的类型检查;返回正确的类型是程序员的责任(“我们都是成年人”)。能够返回与请求类型不同的类型在实现工厂模式时很有用:你可以返回请求类型的子类。

这也解释了你观察到的 issubclass/isinstance 行为:子类关系来自于你使用 class MyClass(tuple),而 isinstance 反映了你从 __new__ 返回了“错误”的类型。

作为参考,可以查看 Python 语言参考中 __new__ 的要求

编辑:好的,这里有一个 __new__ 可能有用的例子。类 Eel 记录了进程中有多少条鳗鱼存活,并且如果超过某个最大值就拒绝分配。

class Eel(object):
    MAX_EELS = 20
    n_eels = 0

    def __new__(cls, *args, **kwargs):
        if cls.n_eels == cls.MAX_EELS:
            raise HovercraftFull()

        obj = super(Eel, cls).__new__(cls)
        cls.n_eels += 1
        return obj

    def __init__(self, voltage):
        self.voltage = voltage

    def __del__(self):
        type(self).n_eels -= 1

    def electric(self):
        """Is this an electric eel?"""
        return self.voltage > 0

请注意,还有更聪明的方法来实现这种行为。

撰写回答