#πŸ”’ code review, post_init for all classes

131 messages Β· Page 1 of 1 (latest)

wild wharf
#

the idea is based on dataclass' post_init, but the purpose is different.
this is mainly for typing purposes, when subclassing an existing class.
normally when subclassing a class while overriding its init dunder, you need to match the signature of the parent class, if it has overloads it becomes a painful process.
this is especially annoying when you dont actually need the init arguments.
for example:

class a:
  @overload
  def __init__(self, obj: int) -> None
  
  def __init__(self, obj: str) -> None

  def __init__(self, obj):
    self.obj = obj

class b:
  @overload
  def __init__(self, obj: int) -> None
  
  def __init__(self, obj: str) -> None

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

    self.obj_id = id(obj)  # or whatever

so, i've created a post_init dunder for this, using a metaclass:

zinc patrolBOT
#

@wild wharf

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

wild wharf
#
class post_init_support_metaclass(type):
    def __call__(cls: type[T], *args: Any, **kwargs: Any) -> T:
        self = super().__call__(*args, **kwargs)

        if isinstance(self, cls):
            self.__post_init__(*args, **kwargs)

        return self


class post_init_support(metaclass=post_init_support_metaclass):
    __slots__ = ()

    def __post_init__(self, *args: Any, **kwargs: Any) -> None:
        pass


class a():
    def __init__(self, obj: int) -> None:
        self.obj = obj

    def method(self) -> None:
        pass


class b(a, post_init_support):
    def __post_init__(self, *args, **kwargs):
        super().__post_init__(*args, **kwargs)

        self.obj *= 3


class c(b):
    def __post_init__(self, *args, **kwargs):
        super().__post_init__(*args, **kwargs)

        self.obj *= 4


class d(b):
    def __post_init__(self, *args, **kwargs):
        super().__post_init__(*args, **kwargs)

        self.obj += 7


class e(c, d):
    def __post_init__(self, *args, **kwargs):
        super().__post_init__(*args, **kwargs)

        self.obj += 2


print(e(3).obj)

the result is 66:
(((3 * 3) + 7) * 4) + 2
i'm still having a conflict as whether to forward the arguments or not

#

a simpler signature would be having only self, and also make sense to some extent, although it might still be useful, and arguments are forwarded from new to init so this is quite normal

hallow forum
hallow forum
#

that means that the __post_init__ signature will inevitably diverge from __init__

wild wharf
#

post_init returns None, and gets the same arguments init is getting

#

if you follow init signature post_init signature will bbe the same

#

if you dont the typechecker would scream

hallow forum
wild wharf
#

i see, well, that is not the intended usage

#

its not typed checked

#

although u've got a point

#

maybe i should remove the args and kwargs

#

valid i guess

#

although, typecheckers could typecheck post_init in the future

#

and prevent that signature of yours

hallow forum
wild wharf
#

fair point

#

i'll remove args and kwargs then

#

i think:

class post_init_support_metaclass(type):
    def __call__(cls: type[T], *args: Any, **kwargs: Any) -> T:
        self = super().__call__(*args, **kwargs)

        if isinstance(self, cls):
            self.__post_init__()

        return self


class post_init_support(metaclass=post_init_support_metaclass):
    __slots__ = ()

    def __post_init__(self) -> None:
        pass


class a():
    def __init__(self, obj: int) -> None:
        self.obj = obj

    def method(self) -> None:
        pass


class b(a, post_init_support):
    def __post_init__(self):
        super().__post_init__()

        self.obj *= 3


class c(b):
    def __post_init__(self):
        super().__post_init__()

        self.obj *= 4


class d(b):
    def __post_init__(self):
        super().__post_init__()

        self.obj += 7


class e(c, d):
    def __post_init__(self):
        super().__post_init__()

        self.obj += 2


print(e(3).obj)
#

keeping the original option with the args in the message from the start, for archiving purposes i guess

hallow forum
wild wharf
#

a doesn't have a post_init

#

post_init purposes is used mainly for subclasses

#

to be fair i could make the metaclass in b

#

editted

hallow forum
# wild wharf a doesn't have a post_init
class A(metaclass=post_init_support):
    def __init__(self, obj: int) -> None:
        self.obj = obj

    def method(self) -> None:
        pass

class B(A):
    def __post_init__(self):
        # super().__post_init__()  # allegedly not necessary
        self.obj *= 6

class C(A):
    def __post_init__(self):
        super().__post_init__()
        self.obj *= 7

class D(B, C):
    pass

print(D(10).obj)
``` `C`'s `__post_init__` is not executed, and 60 is printed
wild wharf
#

there is also another edge case

#

if call returns an object that is not an instance of the class

hallow forum
#

I would probably do this akin to Enum and ABC: ```py
class PostInit(metaclass=post_init_support):
def post_init(self) -> None:
pass

wild wharf
#

like an abstract class?

wild wharf
#

isinstance(self, cls), i think this is the first time i'm doing this lol

hallow forum
#

wrong link

zinc patrolBOT
#

Lib/abc.py lines 184 to 188

class ABC(metaclass=ABCMeta):
    """Helper class that provides a standard way to create an ABC using
    inheritance.
    """
    __slots__ = ()```
wild wharf
#

i see, so you say to copy that structure

hallow forum
wild wharf
#

this is smart

#

should definitely do it

#

all the rest is the same

#

wait, i dont need new anymore

hallow forum
#
class post_init_support:
    __slots__ = ()
    
    def __new__(cls, *args, **kwargs):
        new = super().__new__(cls, *args, **kwargs)
        new.__post_init__()
        return new

    def __post_init__(self) -> None:
        pass
wild wharf
wild wharf
#

but problem is, you assume there's new

#

this is still problematic, maybe using call is better

#

call is calling both new and init

#

or just new i guess

#

actually there's always new correct, just that it wouldn't get to init perhaps

hallow forum
#

Actually, you're right. __call__ is calling __new__ and then __init__ on the produced object. So __post_init__ cannot be called in __new___ if you want to support __new__ customization

#

So maybe a metaclass is the right way to go

wild wharf
#

you could maybe override init though...

#

instead of new

#

every object has an init no

#

but then it'd ruin the init signature i think

#

not working:


class post_init_support():
    __slots__ = ()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.__post_init__(*args, **kwargs)

    def __post_init__(self, *args, **kwargs) -> None:
        pass
#

nah can't get this to work

wild wharf
#

i dont know how to typehint this call correctly

#

since it might return an instance that is not T

#

maybe i should do type[T1] -> T1 | T2?

#

then the type would get bound to T1 anyway

#

no clue

#

the only way is type[Any] -> Any

#

but that is crap

#

maybe type[T] -> T | Any

#

@misty dagger

#

type[T] -> T^

#

type[T] -> T | Any

misty dagger
#

but these seem like different comparisons

wild wharf
misty dagger
#

e(3).obj vs e((3).obj)

wild wharf
#
class a:
  def __new__(cls):
    return 42

print(a())
wild wharf
misty dagger
wild wharf
#

yea it'd print 42, not an instance of a

#

so a() not necessarily returns an object of a

misty dagger
#

does T | Any act the same way as just Any? they're both no good?

wild wharf
#

yea pretty much

#

T | Any acts like Any

#

dont know how to solve it

#

to be fair a lying typehint in that regard is not very bad

#
class post_init_support_metaclass(type):
    def __call__(cls: type[T], *args: Any, **kwargs: Any) -> T:
        self = super().__call__(*args, **kwargs)

        if isinstance(self, cls):
            self.__post_init__(*args, **kwargs)

        return self
#

watch my call dunder

#

it uses the call of the class pretty much, gets a new object (self)

#

then i check if self is an instance of cls, so, basically, i do check that inside the function

#

and then i return self

#

self is not necessarily an instance of cls

#

what about init_subclass, could also work

misty dagger
#

is post_init_support_metaclass inherited from? or called?

#

(i don't know how metaclasses work)

#

oh wait, do you use a meta keyword?

wild wharf
#

yea

misty dagger
#

ok i think i'm following

wild wharf
#

basically the metaclass is for creating classes

misty dagger
#

metaclass=post_init_support_metaclass

wild wharf
#

the call of the metaclass is when you call with the class

#

a() for example

misty dagger
#

rightt makes sense

wild wharf
#

so the problem is type hinting it

#

because a class might return on call, an object that is not an instasnce of it

wild wharf
#

a() would return 42, not an instance of a

misty dagger
wild wharf
#

yea, and cls.init

misty dagger
#

ok

wild wharf
#

and the cls.new might return an object that is not an instance of the cls

#

so what you say in the return type of call?

misty dagger
#

yeah

#

i'd imagine there'd be a standard way to type hint metaclasses

#

but i don't know

wild wharf
#

i dont know either...

misty dagger
#

ty for teaching me about it haha but yeah, hopefully someone else might know

wild wharf
#

1 solution is to just lie

#

and say cls is type[T] and return type is T

#

it got me thinking that python is pretty weird for allowing to return objects that are not instances of the class

#

and np yea

misty dagger
#

that is true tbh

wild wharf
#

alright, pretty much thats it for now, anyone who wants to add something feel free

zinc patrolBOT
#
Python help channel closed for inactivity

This help channel has been closed. Feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.