Запретить использование конструктора dafault извне класса

Рассмотрим следующий класс данных. Я хотел бы предотвратить создание объектов с использованием __init__ метод directclty.

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 извне класса?

4 ответа

Решение

Попытка сделать конструктор приватным в 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))

Поскольку это не стандартное ограничение, накладываемое на создание экземпляра, возможно, стоит добавить еще одну или две строки, чтобы помочь другим разработчикам понять, что происходит / почему это запрещено. Сохраняя в духе "Мы все взрослые по согласию", скрытый параметр для вашего __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_*, но разработчик должен сознательно обойти ваш контрольно-пропускной пункт, чтобы сделать это.

__init__ method is not responsible for creating instances from a class. Вы должны переопределить __new__ method if you want to restrict the instantiation of your class. Но если вы переопределите __new__ method if will affect any form of instanciation as well which means that your classmethod больше не будет работать Because of that and since it's generally not Pythonic to delegate instance creation to another function, it's better to do this within the __new__ метод. Detailed reasons for that can be simply found in doc:

Called to create a new instance of class cls. __new__() is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument. Остальные аргументы передаются в выражение конструктора объекта (вызов класса). Возвращаемое значение __new__() should be the new object instance (usually an instance of cls).

Типичные реализации создают новый экземпляр класса, вызывая суперкласс __new__() метод с использованием super().__new__(cls[, ...]) с соответствующими аргументами, а затем, при необходимости, изменив созданный экземпляр перед его возвратом.

Если__new__() возвращает экземпляр clsзатем новый экземпляр __init__() метод будет вызываться как __init__(self[, ...])где self - это новый экземпляр, а остальные аргументы такие же, как были переданы __new__(),

Если __new__() does not return an instance of cls, then the new instance's __init__() method will not be invoked.

__new__() предназначен главным образом для того, чтобы подклассы неизменяемых типов (таких как int, str или tuple) могли настраивать создание экземпляров. Он также обычно переопределяется в пользовательских метаклассах для настройки создания классов.

Вместо того, чтобы создавать два конструктора, а затем запрещать один из них и принудительно использовать метод класса, почему бы просто не предоставить только тот конструктор, который вам нужен?

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

Это выглядит намного проще, чем оригинальный код, и выполняет то, что было запрошено.

Как объяснил ответ Dunes, это не то, что вы обычно хотели бы сделать. Но так как это возможно в любом случае, вот как:

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__ для всего остального, что весьма прискорбно. Но это один из способов ответить на вопрос в вашем посте.

Другие вопросы по тегам