Repository navigation
Consider adding generics support #8
Description
Activity
- addedenhancementNew feature or requestNew feature or requesthelp wantedExtra attention is neededExtra attention is needed
on Nov 25, 2019 I am not certain the use case is entirely clear. Does
Somecontainer not represent separate types for a value versus no value inside so that your example from #126 can't be used?Regarding
List[str], the predicate will have to checkisinstance(instance[0])- or how would it distingiuish them since, as far as I understand,instance.__args__will be empty due to type erasure even if one ofvalue's parent classes wasList[str]?List[str]can be checked asall(isinstance(item, str) for item in some_list)The usecase I have in mind is:
@some.instance(Some[FirstType]) ... @some.instance(Some[SecondType]) ... @some.instance(Some[ThirdType]) ...
Understood. I am all for
Some[TypeA]vsSome[TypeB]selectors. However, I wonder if it would be possible to write them just like you just did, withoutunrwap()in a predicate, which exposes the internals of the container and ruins the charm of declarativity.I could propose an implementation for that as follows.
First of all,
Some,Resultand any other containers which expect to be used this way should define__class_getitem__, which on invocation subclasses the container class. Like this:class Success(...): def __class_getitem__(cls, value): if blablabla: return type( f'{cls.__name__}[{value.__name__}]', (cls, ), { '_contained_type': value } )
(I know
Successis not a class, it's just an example). This is actually preventing type erasure. Of course there should be a cache of subclass instances created like that. See python/typing#400 and python/typing#79 - one of them says creating a new subclass makes a piece of code 250x slower; let's minimize instances of this.However,
>>> Success[str]._contained_type <type 'str'>
Now, let's define an instance method:
class Success(...): def wrapped_type(self) -> type: return type(self._inner_value)
This means that:
>>> Success(5).wrapped_type() <type 'int'>
Why is all this needed anyway? That's why.
class Success(...): def _instancecheck_(cls, instance): return isinstance(instance, Success) and ( instance.wrapped_type() == self._contained_type or self._contained_type is None ) >>> isintance(Success(5), Success[int]) True >>> isintance(Success(5), Success[str]) False >>> isinstance(Success(5), Success) True
This probably means the implementation for containers in
classeswould be no different from implementation for other classes. This is of course not generalizeable - you need to heavily modify your class for it to work. For the classes you cannot modify probably the predicate will have to be used.P. S.
all(isinstance(item, str) for item in some_list), while theoretically making sure the list indeed contains only strings, is to my opinion definitely intolerable in any production code due to itsO(N)time complexity.Reacted by sobolevn@anatoly-scherbakov great idea! So, we can provide a protocol for other generics to follow.
And modify our own types to support this protocol.And possibly leave
predicate=for people who need generics without this protocol support.Thanks.
I have stumbled across
__class_getitem__override because I'd like to use generic type arguments at runtime; I ended up with https://github.com/anatoly-scherbakov/platonic/blob/master/platonic/platonic/model.py#L44I've come to believe that fixing runtime type erasure is a valid and sufficiently generic use case to be implemented as an independent component useful in many contexts. As per my links above, however, calls like
def very_slow_stuff(): v = Mapping[int](5)
... would be quite undesirable and should be avoided. But, if used in inheritance context - they would only slow down the startup of the application and provide a neat interface to type specification which is also well supported by static analysis. Example:
class PeopleGraph(Graph[Person, Friendship]): ...
looks more pythonic and native than
class PeopleGraph(Graph): node_type = Person edge_type = Friendship
Thoughts?
Hey! Don't know how I ended up here, but actually
Mapping[int]()shouldn't be slow any more given that there is type caching.@MadcowD thanks! 👍
(Be careful with isinstance calls as well). Lastly, you might consider implementing you're own generic's system which doesn't do class instantiation, but rather acts as a partial function on an object constructor:
class Meta(type): def __getitem__(cls, typin): def partial(*args, **kwargs): return cls(typin, *args, **kwargs) partial.__doc__ = cls.__doc__ # Do other things here return partial class TypeSpecificClass(metaclass=Meta): def __init__(self, mytype, someotherarg, somekwarg=0): assert isinstance(mytype, type), "Must instantiate with a type." self._type = mytype # Usage intlabeledobj = TypeSpecificClass[int]("hi", somekwarg=1)
This is honestly so much faster than using the typing library (mainly for isinstance resolution). You can also extend this
__getitem__override to work with isinstance.Good luck :)
Reacted by Anatoly Scherbakov@MadcowD it seems to me like
SomeClass[something]can improve expressiveness and readability of code in some cases, not only to assist type system.I am using a cache to avoid re-creating new subclasses every time, but this idea of yours is an interesting alternative. Thanks!
- added a commit that references this issue
on Jun 3, 2021 - added a commit that references this issue
on Jun 11, 2021
We need to be able to work with generic like
List[str]orResult[str, Exception]andMaybe[int]I propose the following API:
It should be supported on both side: types and runtime.