4

考虑以下数据类。我想防止使用__init__方法直接创建对象。

from __future__ import annotations
from dataclasses import dataclass, field

@dataclass
class C:
    a: int

    @classmethod
    def create_from_f1(cls, a: int) -> C:
        # do something
        return cls(a)
    @classmethod
    def create_from_f2(cls, a: int, b: int) -> C:
        # do something
        return cls(a+b)

    # more constructors follow


c0 = C.create_from_f1(1) # ok

c1 = C()  # should raise an exception
c2 = C(1) # should raise an exception

例如,我想强制使用我定义的其他构造函数,如果直接将对象创建为c = C(..).

到目前为止,我已经尝试过如下。

@dataclass
class C:
    a : int = field(init=False)

    @classmethod
    def create_from(cls, a: int) -> C:
        # do something
        c = cls()
        c.a = a
        return c

with init=Falsein fieldI 防止a成为生成的参数__init__,因此这部分解决了c = C(1)引发异常的问题。
另外,我不喜欢它作为解决方案。

有没有直接的方法来禁止从类外部调用init方法?

4

4 回答 4

1

__init__方法不负责从类创建实例。__new__如果要限制类的实例化,则应覆盖该方法。但是,如果您覆盖该__new__方法 if 也会影响任何形式的实例化,这意味着您classmethod将不再工作。正因为如此,并且由于将实例创建委托给另一个函数通常不是 Pythonic,所以最好在__new__ 方法中执行此操作。详细原因可以在doc中找到:

调用以创建类的新实例cls. __new__()是一个静态方法(特殊情况,因此您无需声明它),它将请求实例的类作为其第一个参数。其余参数是传递给对象构造函数表达式(对类的调用)的参数。的返回值__new__()应该是新的对象实例(通常是 cls 的实例)。

典型的实现通过使用适当的参数调用超类的__new__()方法来创建类的新实例super().__new__(cls[, ...]),然后在返回之前根据需要修改新创建的实例。

如果 __new__()返回 的实例cls,则将调用新实例的__init__()方法__init__(self[, ...]),其中 self 是新实例,其余参数与传递给 的参数相同__new__()

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

__new__()主要是为了允许不可变类型(如 int、str 或 tuple)的子类自定义实例创建。它也通常在自定义元类中被覆盖,以自定义类创建。

于 2018-09-01T12:17:59.487 回答
1

由于这不是对实例创建施加的标准限制,因此可能值得一两行来帮助其他开发人员了解正在发生的事情/为什么禁止这样做。本着“我们都是同意的成年人”的精神,您的一个隐藏参数__init__可能是易于理解和易于实施之间的良好平衡:

class Foo:

    @classmethod
    def create_from_f1(cls, a):
        return cls(a, _is_direct=False)

    @classmethod
    def create_from_f2(cls, a, b):
        return cls(a+b, _is_direct=False)

    def __init__(self, a, _is_direct=True):
        # don't initialize me directly
        if _is_direct:
            raise TypeError("create with Foo.create_from_*")

        self.a = a

当然仍然可以在不经过的情况下创建实例create_from_*,但是开发人员必须有意识地绕过您的障碍才能做到这一点。

于 2018-09-01T12:30:42.707 回答
1

尝试在 Python 中将构造函数设为私有并不是一件非常 Python 的事情。Python 的哲学之一是“我们都是同意的成年人”。也就是说,您不会尝试隐藏该__init__方法,但您确实记录了用户可能想要使用其中一个便利构造函数。但如果用户认为他们真的知道自己在做什么,那么欢迎他们尝试。

您可以在标准库中看到这一理念的实际应用。与inspect.Signature. 类构造函数需要一个列表Parameter,创建起来相当复杂。这不是用户创建 Signature 实例的标准方式。而是signature提供了一个被调用的函数,它接受一个可调用作为参数,并完成从 CPython 中的各种不同函数类型创建参数实例并将它们编组到Signature对象中的所有后续工作。

那就是做类似的事情:

@dataclass
class C:
    """
    The class C represents blah. Instances of C should be created using the C.create_from_<x> 
    family of functions.
    """

    a: int
    b: str
    c: float

    @classmethod
    def create_from_int(cls, x: int):
        return cls(foo(x), bar(x), baz(x))
于 2018-09-01T14:25:35.950 回答
0

正如沙丘的回答所解释的,这不是你通常想做的事情。但既然它是可能的,这里是如何:

dataclasses import dataclass

@dataclass
class C:
    a: int

    def __post_init__(self):
        # __init__ will call this method automatically
        raise TypeError("Don't create instances of this class by hand!")

    @classmethod
    def create_from_f1(cls, a: int):
        # disable the post_init by hand ...
        tmp = cls.__post_init__
        cls.__post_init__ = lambda *args, **kwargs: None
        ret = cls(a)
        cls.__post_init__ = tmp
        # ... and restore it once we are done
        return ret

print(C.create_from_f1(1))  # works
print(C(1))                 # raises a descriptive TypeError

我可能不需要说句柄代码看起来绝对令人发指,而且它也使它无法__post_init__用于其他任何事情,这是非常不幸的。但这是回答您帖子中问题的一种方法。

于 2019-03-11T13:58:39.573 回答