This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Enable usage of object.__orig_class__ in __init__
Type: enhancement Stage:
Components: Library (Lib) Versions: Python 3.11
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: AlexWaygood, Gobot1234, JelleZijlstra, gvanrossum, kj, med2277
Priority: normal Keywords:

Created on 2022-02-13 23:49 by Gobot1234, last changed 2022-04-11 14:59 by admin.

Messages (11)
msg413198 - (view) Author: Gobot1234 (Gobot1234) * Date: 2022-02-13 23:49
When using `__call__` on a `typing/types.GenericAlias` `__orig_class__` is set to the `GenericAlias` instance, however currently the mechanism for this does not allow the `__origin__` to access the `GenericAlias` from `__origin__.__init__` as it performs something akin to:
```py
def __call__(self, *args, **kwargs):
    object = self.__origin__(*args, **kwargs)
    object.__orig_class__ = self
    return object
```
I'd like to propose changing this to something like:
```py
def __call__(self, *args, **kwargs):
    object = self.__origin__.__new__(*args, **kwargs)
    object.__orig_class__ = self
    object.__init__(*args, **kwargs)
    return object
```
(Ideally `__orig_class__` should also be available in `__new__` but I'm not entirely sure if that's possible)

AFAICT this was possible in the typing version back in 3.6 (https://github.com/python/typing/issues/658 and maybe https://github.com/python/typing/issues/519). Was there a reason this was removed?
msg413200 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-14 00:11
In the early days the design and implementation were changed so many times, your question (why was it changed) is unanswerable.

I do not actually understand your description of your problem -- you mention so many dunders that it obscures your use case.

Can you provide a small example use case that demonstrates code that currently doesn't work but that you think ought to work, and walk us through it?
msg413201 - (view) Author: Gobot1234 (Gobot1234) * Date: 2022-02-14 00:23
Currently I have code that uses the generic argument to a class as the constructor for its `items` https://github.com/Gobot1234/steam.py/blob/f99cb77d58b552f1d493663e7b3455f94977347e/steam/trade.py#L380. As this is called from `BaseInventory.__init__` it currently fails if I just subclass `GenericAlias`, so I had to implement `__call__` myself https://github.com/Gobot1234/steam.py/blob/f99cb77d58b552f1d493663e7b3455f94977347e/steam/trade.py#L299-L307.
msg413204 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-14 01:14
I'm sorry, that's not a small example that I can follow, and I have no idea why you are doing all those things.

I fear that if you cannot be more articulate this will remain unfixed.
msg413205 - (view) Author: Gobot1234 (Gobot1234) * Date: 2022-02-14 01:39
```py
class DefaultBox(Generic[T]):
    def __init__(self, value: T | None = None):
        self.value = (
            value if value is not None else  # the arg
            self.__orig_class__.__args__[0]()  # or the default for the type argument 
        )

int_box = DefaultBox[int]()
print(int_box.value)  # should print 0
str_box = DefaultBox[str](value="this")
print(str_box.value)  # should print this
```
Currently this doesn't work, but I really think it should.
msg413207 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-14 02:05
Thanks for the small example, I understand what you are trying to do now.

I don't think it's possible in general, since you are blindly instantiating the type (self.__orig_class__.__args__[0]) without arguments. That doesn't work for all types.

But that aside, the problem with implementing what you wish is that we would have to refactor class instantiation to break it in two parts, one part that calls __new__() and produces the instance, and one part that (if certain conditions are satisifed) calls __init__().

I now also understand the two code fragments you gave, and breaking the construction in those two parts is exactly what you are doing there.

However. The "if certain conditions are satisfied" part is complex. For example, self.__origin__.__new__() could return an instance of a different type -- if that's not a subclass of self.__origin__, the call to __init__() will be skipped.

There may also be other that happen between __new__() and __init__(), and other things may happen before __new__() or after __init__() that we would skip by simply calling __new__() followed by __init__() ourselves -- someone else should research this.

And we may have the same issue in the C code in ga_call() in genericaliasobject.c. (I'm not sure if that's relevant.)

I'm also not sure what static type checkers would make of this. But I presume you don't care about that.
msg413212 - (view) Author: Mehdi2277 (med2277) Date: 2022-02-14 05:15
Hmm, I actually have code that does something very similar to that. But it does not set it in `__init__` and instead uses it in a property where it is available. Tweaking your code,

```py
class DefaultBox(Generic[T]):
    def __init__(self, value: T | None = None):
        self._default_value = value
        self._value_set = False
   
     @property
     def value(self) -> T:
       if not self._value_set:
         self._value = self._default_value if self._default_value is not None else self.__orig_class__.__args__[0]()  

       return self._value
```

I think should work fine. Any reason initializing value afterwards is an issue? Or is the main goal that the original code is simpler then this lazy initialization workaround.
msg413221 - (view) Author: Gobot1234 (Gobot1234) * Date: 2022-02-14 11:06
On the general class instanciation point would there be anything wrong with just adding a big red warning saying (on the non-existent) docs for these classes that they don't follow normal class initization or is this too insignificant of an issue to bother?

> I don't think it's possible in general, since you are blindly instantiating the type (self.__orig_class__.__args__[0]) without arguments. That doesn't work for all types.

I think you could make this work with a Protocol as the bound TypeVar("T", bound=HasTheCorrectNewSignature)?

> I'm also not sure what static type checkers would make of this. But I presume you don't care about that.

Yeah considering this is only accessed in one place and how likely difficult it would be to implement it doesn't bother me much no. (You can avoid the attribute access errors using __orig_class__: GenericAlias if you know what your doing is always safe).

> Or is the main goal that the original code is simpler then this lazy initialization workaround.

Pretty much yeah I'd like the code to be much simplier and avoid this work around (although you could probably simplify it with a cached_property)
msg413232 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-14 16:34
> On the general class instanciation point would there be anything wrong with just adding a big red warning saying (on the non-existent) docs for these classes that they don't follow normal class initization or is this too insignificant of an issue to bother?

Which classes? Every class that inherits from Generic? That would be problematic -- we don't want the addition of typing information to change the behavior of a construct (or at least as little as possible).

> I think you could make this work with a Protocol as the bound TypeVar("T", bound=HasTheCorrectNewSignature)?

Sure.

But I am still inclined to reject the feature request as too obscure.
msg413236 - (view) Author: Gobot1234 (Gobot1234) * Date: 2022-02-14 16:59
> Which classes? Every class that inherits from Generic? That would be problematic -- we don't want the addition of typing information to change the behavior of a construct (or at least as little as possible).

The class itself would remain unchanged, the only thing I propose changing is what happens when you subscript it and then attempt to instantiate it.
msg413238 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-14 17:00
Yeah, that should erase the type, not have special semantics.
History
Date User Action Args
2022-04-11 14:59:56adminsetgithub: 90899
2022-02-14 17:00:51gvanrossumsetmessages: + msg413238
2022-02-14 16:59:15Gobot1234setmessages: + msg413236
2022-02-14 16:34:58gvanrossumsetmessages: + msg413232
2022-02-14 11:06:07Gobot1234setmessages: + msg413221
2022-02-14 05:15:32med2277setnosy: + med2277
messages: + msg413212
2022-02-14 02:05:26gvanrossumsetmessages: + msg413207
2022-02-14 01:55:14AlexWaygoodsetnosy: + AlexWaygood
2022-02-14 01:43:02JelleZijlstrasetnosy: + JelleZijlstra
2022-02-14 01:39:22Gobot1234setmessages: + msg413205
2022-02-14 01:14:27gvanrossumsetmessages: + msg413204
2022-02-14 00:23:28Gobot1234setmessages: + msg413201
2022-02-14 00:11:25gvanrossumsetmessages: + msg413200
2022-02-13 23:49:55Gobot1234create