Assume I want to create a class instance only if certain calculation gives positive number, otherwise, return None
.
I can create a staticmethod and just use if - else
inside... But it is not intuitive why would I not use common instance initialization to create objects. And if I want it to be the only "valid" way to create instances - I would need to raise an Exception inside __init__
method.
I wondered if I could do it differently using __new__
. So I made up something like this (simplified example):
class A:
n_contracts: int
def __new__(cls, cash: float, margin: float):
n_contracts = int(cash // margin)
if n_contracts <= 0:
return None
instance = super().__new__(cls)
instance.n_contracts = n_contracts
return instance
def __repr__(self):
return f"A(n_contracts={self.n_contracts})"
a = A(1000, 100)
print(a) # prints `A(n_contracts=10)`
try:
print(A.n_contracts)
except AttributeError as err:
print(err.name)
b = A(-1000, 100)
print(b) # prints `None`
A.n_contracts = 100000
print(a) # prints `A(n_contracts=10)`
c = A(1000, 10)
print(c) # prints `A(n_contracts=100)`
print(A.n_contracts) # prints `100000`
Seems like everything working correctly. Is there any downfalls? Because it feels... Uncommon. I'm afraid I just don't see any problem right now, but later it may be a pain to fix, so I want to clarify this to myself.
The usual thing for a __new__
method call in a Python class is to return a working instance of its class.
That is not mandatory, but it is what is strongly expected - and changing it to return None on failure would almost certainly lead to incorrect usage patterns of your class.
So, for uncommon instantiating patterns, even if you build then into __new__
it would be better to raise an Exception (any exception will do as far as it is a documented behavior - no need for a custom exception class) - - that would prevent one from thinking he has an instance of your class bound to a variable, when the value is actually a None
.
And still less surprising, (and therefore could be considered "more correct") would be to prevent direct instantiation of your class at all using the default __new__
method, and having a custom method (or other call in your project), which will create the instances of your class. The big semantic change there is that for a custom classmethod one has to actually check the project documentation to see what is being returned at each call, and it is free to return None
if it makes sense.
So, besides having a classmethod to instantiate your class, making __new__
fail by default would also be a nice design:
from typing import Self, Never
class A:
"""
A object that frublonates the eschaton
...
Do not instantiate this directly - call
A.new(...) for new instances
"""
def __new__(cls) -> Never:
raise RuntimeError(f"{cls.__name__} can not be instantiated directly. Use {cls.__name__}.new(...) ")
@classmethod
def new(cls, cash: float, margin: float) -> Self| None:
if not (my_condition):
return None
# The __new__ method in superclasses won't be using
#the custom parameters:
instance = super().__new__(cls)
# write the __init__ method as usual, handling
# the passed in attributes, so that inheritance will work
# correctly if you need to override it:
instance.__init__(cash, margin)
return instance
def __init__(self, cash: float, margin: float) -> None:
...