【问题标题】:Forbid the usage of the default constructor from outside the class禁止在类外使用默认构造函数
【发布时间】:2019-02-07 05:15:30
【问题描述】:

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

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

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

【问题讨论】:

    标签: python class python-3.7 python-dataclasses


    【解决方案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))
    

    【讨论】:

      【解决方案2】:

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

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

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

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

      如果__new__() 没有返回cls 的实例,那么新实例的__init__() 方法将不会被调用。

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

      【讨论】:

      • create_from 中创建实例时,不是也调用__new__ 吗?在我看来,每当创建实例时都会引发此异常。
      • @chris 当然,我会在一分钟内解释。
      【解决方案3】:

      由于这不是对实例创建施加的标准限制,因此可能值得多写一两行来帮助其他开发人员了解正在发生的事情/为什么禁止这样做。本着“我们都是同意的成年人”的精神,__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_* 的情况下创建实例,但开发人员必须有意识地绕过你的障碍才能做到这一点。

      【讨论】:

      • 为避免调用者在不了解他们在做什么的情况下意外传递参数,请将其设为仅关键字:def __init__(self, a, *, _is_direct=True):
      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 2015-10-30
        • 2011-12-25
        • 2012-12-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多