Python中的__new__和__init__
我正在学习Python,到目前为止,我对__new__
和__init__
有以下几点理解:
__new__
是用来创建对象的。__init__
是用来初始化对象的。__new__
在__init__
之前被调用,因为__new__
会返回一个新的实例,而__init__
是在之后被调用来初始化内部状态。__new__
适合不可变对象,因为一旦赋值就不能改变。所以我们可以返回一个具有新状态的新实例。- 我们可以同时使用
__new__
和__init__
来处理可变对象,因为它们的内部状态是可以改变的。
但我现在有其他问题。
- 当我创建一个新实例,比如
a = MyClass("hello","world")
,这些参数是怎么传递的?我的意思是,我应该如何构建这个类,使用__init__
和__new__
,因为它们是不同的,并且都接受除了默认第一个参数之外的任意参数。 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帖子:
3 个回答
你的问题大概是这样的:
在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__
会做以下事情:
修改原来的
rocky_the_rectangle
。它不会是rocky的一个副本。
__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
,原始可调用对象中的文档字符串将被清除、忽略、不继承或被遮蔽。
看起来没有人讨论过在实例化时参数是怎么处理的。
让我们来创建一个简单的类,定义一下new和init。
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
如果有更好的方法来实现这个,或者我哪里搞错了,请告诉我。
我应该如何使用
__init__
和__new__
来构建类,因为它们是不同的,并且都接受除了默认的第一个参数之外的任意参数。
你很少需要担心 __new__
。通常,你只需要定义 __init__
,然后让默认的 __new__
将构造函数的参数传递给它。
self
这个关键词的名字可以换成其他的名字吗?我在想cls
这个名字是否也可以换,因为它只是一个参数名?
这两个其实都是参数名,在语言中没有特别的含义。不过在 Python 社区里,它们的用法是一个非常强的约定;大多数 Python 程序员在这些情况下不会改变 self
和 cls
的名字,如果有人这样做,他们会感到困惑。
注意,你使用 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
请注意,还有更聪明的方法来实现这种行为。