Python Typing : Create a class function with a generic type, and also access that type through the class - python-3.x

I have a publisher that can publish messages of a certain type:
T = TypeVar('T')
class Publisher(Generic[T]):
def __init__(self, topic: str) -> None:
self.__topic = topic
def publish(self, msg: T):
pass
# As an example, create a publisher that can publish ints
p = Publisher[int]("chatter")
p.publish(1)
This works, and the publish function has the correct type hint, but I want to be able to access the type of the publisher with a get_type() function.
A simple way to do this is to pass the message type into the constructor:
T = TypeVar('T')
class Publisher(Generic[T]):
def __init__(self, msg_type: type, topic: str) -> None:
self.__msg_type = msg_type
self.__topic = topic
def publish(self, msg: T):
pass
def get_type(self) -> type:
return self.__msg_type
p = Publisher[int](int, "chatter")
p.publish(1)
But this requires writing int twice in the line p = Publisher[int](int, "chatter") which seems a bit clumsy and redundant.
I tried wrapping the creation of the publisher in a function so that you don't have to write int twice, but I get a problem:
T = TypeVar('T', bound=type)
class Publisher(Generic[T]):
def __init__(self, msg_type: type, topic: str) -> None:
self.__msg_type = msg_type
self.__topic = topic
def publish(self, msg: T):
pass
def get_type(self) -> type:
return self.__msg_type
def create_publisher(msg_type: T, topic: str) -> Publisher[T]:
return Publisher[T](msg_type, topic)
p = create_publisher(int, "hello")
p.publish(1) #Fails because its expecting Type[int], not an instance of an int
So what I need, is a method to get convert Type[x] into x in a typehint context. Essentially the inverse of what Type does.
e.g, the last example would become:
T = TypeVar('T', bound=type)
class Publisher(Generic[T]):
def __init__(self, msg_type: type, topic: str) -> None:
self.__msg_type = msg_type
self.__topic = topic
def publish(self, msg: InstanceOf[T]):
pass
def get_type(self) -> type:
return self.__msg_type
def create_publisher(msg_type: T, topic: str) -> Publisher[T]:
return Publisher[T](msg_type, topic)
p = create_publisher(int, "hello")
p.publish(1)
But I do not know how to make the InstanceOf generic.
Is there anyway I can do this? Or any other way to get the functionality I want without having to write int twice in the line p = Publisher[int](int, "chatter")
edit
Here is another attempt that also doesn't work, but should clarify what I'm trying to do:
T = TypeVar('T')
class Publisher(Generic[T]):
def __init__(self, topic: str) -> None:
self.__topic = topic
def publish(self, msg: T):
pass
def get_type(self) -> type:
return get_args(Publisher[T])[0]
#This works
print(get_args(Publisher[int])[0])
#This doesn't
p = Publisher[int]("hello")
print(p.get_type())
In this example p.get_type() returns ~T instead of int

Pass in the type explicitly, but annotate it as Type[T]. This allows inference of T without having to specify it, making it enough to specify the type only once (as the argument).
class Publisher(Generic[T]):
# knowing `msg_type` defines `T`
def __init__(self, msg_type: Type[T], topic: str) -> None:
self._msg_type = msg_type
self._topic = topic
def publish(self, msg: T):
pass
def get_type(self) -> Type[T]:
return self._msg_type
# argument of `int` implies T = int
p = Publisher(int, "hello")
print(p.get_type()) # <class 'int'>
if TYPE_CHECKING:
reveal_type(p) # note: Revealed type is 'aaa_testbed.Publisher[builtins.int*]'

I have an answer, based on this answer, but it depends on undocumented implementation details.
from typing import TypeVar, Generic, get_args, get_origin
T = TypeVar('T')
class Publisher(Generic[T]):
def __init__(self, topic: str) -> None:
self.__topic = topic
def publish(self, msg: T):
pass
def get_type(self) -> type:
return get_args(self.__orig_class__)[0]
p = Publisher[int]("hello")
print(p.get_type())

Related

Type hinting subclass which contains references to other instances of its own type

I am trying to create a base class for a 2 dimensional linked list. The code below is already working fine from a functionality point of view. The problem is, when I subclass LinkedList2D to create a more specific version all my attributes (up, down, before, after) and my generator functions still resolve the type hints to LinkedList2D and not to the more specific subclass, which is necessary because I want to access the additional attributes and methods of it.
I have a feeling, that I need to use bound generics somehow but I can't seem to wrap my head around it.
class LinkedList2D:
up: Optional[LinkedList2D]
down: Optional[LinkedList2D]
before: Optional[LinkedList2D]
after: Optional[LinkedList2D]
def __init__(self) -> None:
self.up = None
self.down = None
self.before = None
self.after = None
def iterate_children(self) -> Generator[LinkedList2D, None, None]:
link = self.down
while link:
yield link
link = link.after
def iterate_tree(self) -> Generator[LinkedList2D, None, None]:
for child in self.iterate_children():
if child:
yield child
yield from child.iterate_children()
def get_children(self) -> List[LinkedList2D]:
return list(self.iterate_children())
def insert_under(self, op: LinkedList2D) -> bool:
if op:
if op.down:
self.insert_before(op.down)
op.down = self
self.up = op
return True
def insert_under_last(self, op: LinkedList2D) -> bool:
self.up = op
if op:
if op.down:
last = self.get_children()[-1]
self.insert_after(last)
else:
op.down = self
return True
def insert_after(self, obj: LinkedList2D) -> bool:
if obj:
if obj.after:
obj.after.before = self
obj.after = self
self.after = obj.after
self.up = obj.up
self.before = obj
return True
def insert_before(self, obj: LinkedList2D) -> bool:
if obj:
if obj.before:
obj.before.after = self
obj.before = self
self.up = obj.up
self.before = obj.before
self.after = obj
return True
The generic version would look like this, I think. I used bound to make sure the potential instances are subclasses of LinkedList2D and implement the necessary functions and attributes.
T = TypeVar("T", bound="LinkedList2D")
class LinkedList2D(Generic[T]):
up: Optional[T]
down: Optional[T]
before: Optional[T]
after: Optional[T]
def __init__(self) -> None:
...
def iterate_children(self) -> Generator[T, None, None]:
...
def iterate_tree(self) -> Generator[T, None, None]:
...
def get_children(self) -> List[T]:
...
def insert_under(self, op: T) -> bool:
if op:
if op.down:
self.insert_before(op.down)
op.down = self
self.up = op
return True
def insert_under_last(self, op: T) -> bool:
...
def insert_before(self, obj: T) -> bool:
...
The Problem is, that now pylance is complaining about the line op.down = self in the insert_under method:
Cannot assign member "down" for type "LinkedList2D[T#LinkedList2D]"
Expression of type "LinkedList2D[T#LinkedList2D]" cannot be assigned to member "down" of class "LinkedList2D[T#LinkedList2D]"
Type "LinkedList2D[T#LinkedList2D]" cannot be assigned to type "T#LinkedList2D | None"
Type "LinkedList2D[T#LinkedList2D]" cannot be assigned to type "T#LinkedList2D"
Cannot assign to "None"PylancereportGeneralTypeIssues
Any ideas and hints are greatly appreciated. Thanks
So apparently it is possible to type hint self with a bound TypeVar which makes it possible for the subclasses of the generic LinkedList2D to return the correctly hinted types for get_children, iterate_children and iterate_tree. The insert_under method will also not complain anymore since it is accepting LinkedList2D instances and any subclass of it.
from __future__ import annotations
from typing import Optional, Generator, List, TypeVar
T = TypeVar("T", bound="LinkedList2D")
class LinkedList2D:
up: Optional[LinkedList2D]
down: Optional[LinkedList2D]
before: Optional[LinkedList2D]
after: Optional[LinkedList2D]
def __init__(self) -> None:
self.up = None
self.down = None
self.before = None
self.after = None
def iterate_children(self: T) -> Generator[T, None, None]:
link = self.down
while link:
yield link
link = link.after
def iterate_tree(self: T) -> Generator[T, None, None]:
for child in self.iterate_children():
if child:
yield child
yield from child.iterate_children()
def get_children(self: T) -> List[T]:
return list(self.iterate_children())
def insert_under(self, op: LinkedList2D) -> bool:
if op:
if op.down:
self.insert_before(op.down)
op.down = self
self.up = op
return True
def insert_under_last(self, op: LinkedList2D) -> bool:
self.up = op
if op:
if op.down:
last = self.get_children()[-1]
self.insert_after(last)
else:
op.down = self
return True
def insert_after(self, obj: LinkedList2D) -> bool:
if obj:
if obj.after:
obj.after.before = self
obj.after = self
self.after = obj.after
self.up = obj.up
self.before = obj
return True
def insert_before(self, obj: LinkedList2D) -> bool:
if obj:
if obj.before:
obj.before.after = self
obj.before = self
self.up = obj.up
self.before = obj.before
self.after = obj
return True
class BaseObject(LinkedList2D):
...
test = BaseObject()
test2 = BaseObject()
test.insert_under(test2) # will accept BaseObject
test.get_children() # hint result is: List[BaseObject]

Type inference for class derived from Mapping

I have a class implementing MutableMapping
V = TypeVar('V')
K = TypeVar('K')
class MM(MutableMapping[K, V]):
def __getitem__(self, k):
return 1
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0
which gets used like this:
test: MM[str, int] = MM()
test["a"] = 1
var = test["a"]
The variable var should be of type int (according to the type hint), yet pycharm tells me that its type cannot be inferred.
Is this a bug in pycharm or is there something I am doing wrong?
Another thing that is surprising is that when I change the class definition to
MM(MutableMapping[str, V])
Pycharm indicates that the type of var is string. What am I missing?
You probably need to add type hints to each of your methods. For example:
from typing import MutableMapping, TypeVar, Iterator
K = TypeVar('K')
V = TypeVar('V')
class MM(MutableMapping[K, V]):
def __getitem__(self, k: K) -> V:
return ???
def __setitem__(self, k: K, v: V) -> None:
pass
def __delitem__(self, k: K) -> None:
pass
def __iter__(self) -> Iterator[K]:
return iter(())
def __len__(self) -> int:
return 0
While I'm not 100% how exactly Pycharm's inference engine works, it seems reasonable to me that if you leave your functions untyped, it'll just default to assuming you meant to leave that function dynamically typed and that your params/return values are of type 'Any'.

Building Custom Type Hints

I have a simple class:
from typing import TypeVar, Generic
T = TypeVar('T')
class CustomParameter(Generic[T]):
def __init__(self, value: T) -> None:
print(value, T)
self.value = value
def get(self) -> T:
print(self.value)
return self.value
then I declare my custom type like this:
def is_dark(x: CustomParameter['Sunshine']) -> int: return x
How can I get the value Sunshine on from the type, so I can use it to check that the input is actually some object of type Sunshine?
Thanks

Python return typing dynamically based on parameter

I have a method that returns dynamic type based on the class I pass in:
def foo(cls):
return cls()
How can I setup typing for this function?
After reading this article https://blog.yuo.be/2016/05/08/python-3-5-getting-to-grips-with-type-hints/, I found solution myself:
from typing import TypeVar, Type
class A:
def a(self):
return 'a'
class B(A):
def b(self):
return 'b'
T = TypeVar('T')
def foo(a: T) -> T:
return a()
This template suites my question above, but actually, my need is a little bit different that I need to work more. Below I include my problem and solution:
Problem: I want to use the with keyword like this:
with open_page(PageX) as page:
page.method_x() # method x is from PageX
Solution
from typing import TypeVar, Type, Generic
T = TypeVar('T')
def open_page(cls: Type[T]):
class __F__(Generic[T]):
def __init__(self, cls: Type[T]):
self._cls = cls
def __enter__(self) -> T:
return self._cls()
def __exit__(self, exc_type, exc_val, exc_tb):
pass
return __F__(cls)
So, when I use with PyCharm, it's able to suggest method_x when I pass PageX into with open_page(PageX) as page:

how to use type hint in python to perform that a method returns exactly its parameter

here is my basic code.
from typing import Optional
class State:
name = 'unnamed state'
def __init__(self, name: str = None) -> None:
super().__init__()
if name is not None:
self.name = name
def on_enter(self) -> None:
pass
def on_leave(self) -> None:
pass
def to(self, dest: Optional['State']) -> Optional['State']:
self.on_leave()
if dest is not None:
dest.on_leave()
return dest
class AState(State):
pass
class BState(State):
def b_func(self):
pass
a = AState()
b = BState()
a.to(b).b_func()
currently type hint shows the type of a.to(b) is Optional[State], but what I wanted is to return the parameter is self, which is BState. Is this available in the type system in Python?
If you'd like to have a function always return exactly the same type as its parameter, you can use generics via the TypeVars class:
from typing import Optional, TypeVar
# Note that "TState" is an arbitrary name.
TState = TypeVar('TState', bound='State')
class State:
name = 'unnamed state'
def __init__(self, name: str = None) -> None:
super().__init__()
if name is not None:
self.name = name
def on_enter(self) -> None:
pass
def on_leave(self) -> None:
pass
def to(self, dest: Optional[TState]) -> Optional[TState]:
self.on_leave()
if dest is not None:
dest.on_leave()
return dest
class AState(State):
pass
class BState(State):
def b_func(self):
pass
a = AState()
b = BState()
a.to(b).b_func()
The expression TState = TypeVar('TState', bound=State) means "create a new generic parameter named TState which must always be a subclass of a State object." However, since the State class isn't defined yet, we need to use a forward reference and have the bound be a string instead of the class name: TState = TypeVar('TState', bound='State').
You could also do TState = TypeVar('TState') which means "create a new generic parameter named TState which can be anything", but that probably isn't what you want so I don't recommend it.
You can learn more about upper bounds in TypeVars here: http://mypy.readthedocs.io/en/latest/generics.html#type-variables-with-upper-bounds

Resources