禁止从类外部使用默认构造函数

时间:2018-09-01 11:57:36

标签: python class python-3.7 python-dataclasses

请考虑以下数据类。我想防止使用__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

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

是否有直接方法可以禁止从类外部调用 init 方法?

5 个答案:

答案 0 :(得分:1)

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

  

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

     

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

     

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

     

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

     

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

答案 1 :(得分: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_*的情况下创建实例当然仍然是可能,但是开发人员必须有意识地绕过障碍进行操作。

答案 2 :(得分:1)

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

您可以在标准库中看到这一理念。使用inspect.Signature。类构造函数采用Parameter的列表,创建起来相当复杂。这不是要求用户创建签名实例的标准方法。而是提供了一个名为signature的函数,该函数以Callable作为参数,并完成了从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))

答案 3 :(得分:0)

不是创建两个构造函数,然后禁止其中之一,并强制使用类方法,为什么不简单地仅提供所需的构造函数呢?

class C:
    def __init__(self, a: int):
        # do something
        self.a = a

这看起来比原始代码简单得多,并且可以执行请求的操作。

答案 4 :(得分:0)

正如Dunes' answer所解释的,这不是您通常想要做的。但是由于仍然有可能,因此是这样:

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__用于其他任何事情,这是非常不幸的。但这是回答帖子中问题的一种方法。