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