代码之家  ›  专栏  ›  技术社区  ›  abc

禁止在类外使用dafault构造函数

  •  2
  • abc  · 技术社区  · 6 年前

    考虑以下数据类。我想阻止使用 __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 从参数到生成的 __初始化__ ,因此这部分地解决了问题 c = C(1) 引发异常。
    而且,我也不喜欢把它当作解决办法。

    有没有一个直接的方法来禁用 初始化 方法从类外部调用?

    5 回复  |  直到 5 年前
        1
  •  1
  •   Dunes    6 年前

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

    您可以在标准库中看到这一理念的实际应用。与 inspect.Signature . 类构造函数获取 Parameter 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
  •  1
  •   Community Egal    4 年前

    这个 __init__ __new__ 方法来限制类的实例化。但如果你推翻了 __新建__ 方法if也会影响任何形式的实例化,这意味着 classmethod 再也不行了。因此,由于将实例创建委托给另一个函数通常不是Pythonic的,因此最好在 __新建__ 方法。具体原因可以在 doc :

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

    __新建\() 方法使用 super().__new__(cls[, ...]) 然后根据需要修改新创建的实例,然后再返回它。

    __new__() 返回的实例 cls ,然后是新实例 __init__() 方法的调用方式如下 __init__(self[, ...]) ,其中self是新实例,其余参数与传递给的参数相同 __新建\() .

    如果 __新建\() __初始化() 方法将不会被调用。

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

        3
  •  1
  •   chris    6 年前

    因为这不是对实例创建的标准限制,所以额外的一两行代码来帮助其他开发人员理解发生了什么/为什么禁止这样做可能是值得的。保持“我们都是同意的成年人”的精神,这是一个隐藏的参数 __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_* ,但开发人员必须有意识地绕过您的障碍才能做到这一点。

        4
  •  0
  •   Arne    5 年前

    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
    

    我可能不需要说handle代码看起来非常糟糕,而且它也使它无法使用 __post_init__ 对于其他事情,这是非常不幸的。但这是在你的帖子里回答问题的一种方式。