Запретить использование конструктора по умолчанию извне класса - PullRequest
0 голосов
/ 01 сентября 2018

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

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 голосов
/ 11 марта 2019

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

0 голосов
/ 01 сентября 2018

Попытка сделать конструктор частным в 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))
0 голосов
/ 01 сентября 2018

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

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

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

0 голосов
/ 01 сентября 2018

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

0 голосов
/ 01 сентября 2018

Метод __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) могли настраивать создание экземпляров. Он также обычно переопределяется в пользовательских метаклассах для настройки создания класса.

...