Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mypy: how to define a generic subclass

Tags:

python

types

mypy

I have a subclass of queue.Queue like so:

class SetQueue(queue.Queue):
    """Queue which will allow a given object to be put once only.

    Objects are considered identical if hash(object) are identical.
    """

    def __init__(self, maxsize=0):
        """Initialise queue with maximum number of items.

        0 for infinite queue
        """
        super().__init__(maxsize)
        self.all_items = set()

    def _put(self):
        if item not in self.all_items:
            super()._put(item)
            self.all_items.add(item)

I am trying to use mypy for static type checking. In this case, the SetQueue should take a generic object T. This is my attempt so far:

from typing import Generic, Iterable, Set, TypeVar

# Type for mypy generics
T = TypeVar('T')

class SetQueue(queue.Queue):
    """Queue which will allow a given object to be put once only.

    Objects are considered identical if hash(object) are identical.
    """

    def __init__(self, maxsize: int=0) -> None:
        """Initialise queue with maximum number of items.

        0 for infinite queue
        """
        super().__init__(maxsize)
        self.all_items = set()  # type: Set[T]

    def _put(self, item: T) -> None:
        if item not in self.all_items:
            super()._put(item)
            self.all_items.add(item)

mypy throws a warning on the class definition line saying "Missing type parameters for generic type".

I think that I need a Generic[T] somewhere but every attempt that I have made throws a syntax error. All of the examples in the docs show subclassing from Generic[T] but don't subclass from any other object.

Does anyone know how to define the generic type for SetQueue?

like image 716
blokeley Avatar asked Jul 31 '17 10:07

blokeley


People also ask

How do you define generic type?

Definition: “A generic type is a generic class or interface that is parameterized over types.” Essentially, generic types allow you to write a general, generic class (or method) that works with different types, allowing for code re-use.

How do you specify a generic type in C#?

You can create an instance of generic classes by specifying an actual type in angle brackets. The following creates an instance of the generic class DataStore . DataStore<string> store = new DataStore<string>(); Above, we specified the string type in the angle brackets while creating an instance.

How do you make a generic type in Java?

To update the Box class to use generics, you create a generic type declaration by changing the code "public class Box" to "public class Box<T>". This introduces the type variable, T, that can be used anywhere inside the class. As you can see, all occurrences of Object are replaced by T.


2 Answers

The problem here is that queue.Queue does not actually not inherit from typing.Generic, but the typeshed stubs for it says that it does. This is a bit of a necessary evil until the stdlib fully buys into typing, if ever. As a result, the actual queue.Queue does not have the typing.GenericMeta metaclass that gives generic classes their __getitem__ ability at runtime:

For example, this code type-checks ok in mypy, but fails at runtime:

from typing import Generic, Iterable, Set, TypeVar, TYPE_CHECKING
import queue

# Type for mypy generics
T = TypeVar('T')


class SetQueue(queue.Queue[T]):
    """Queue which will allow a given object to be put once only.

    Objects are considered identical if hash(object) are identical.
    """

    def __init__(self, maxsize: int=0) -> None:
        """Initialise queue with maximum number of items.

        0 for infinite queue
        """
        super().__init__(maxsize)
        self.all_items = set()  # type: Set[T]

    def _put(self, item: T) -> None:
        if item not in self.all_items:
            super()._put(item)
            self.all_items.add(item)


my_queue = queue.Queue()  # type: queue.Queue[int]
my_queue.put(1)
my_queue.put('foo')  # error

my_set_queue = SetQueue()  # type: SetQueue[int]
my_set_queue.put(1)
my_set_queue.put('foo')  # error

The error raised is TypeError: 'type' object is not subscriptable, meaning that queue.Queue[T] (i.e. queue.Queue.__getitem__) is not supported.

Here's a hack to make it work at runtime as well:

from typing import Generic, Iterable, Set, TypeVar, TYPE_CHECKING
import queue

# Type for mypy generics
T = TypeVar('T')

if TYPE_CHECKING:
    Queue = queue.Queue
else:
    class FakeGenericMeta(type):
        def __getitem__(self, item):
            return self

    class Queue(queue.Queue, metaclass=FakeGenericMeta):
        pass


class SetQueue(Queue[T]):
    """Queue which will allow a given object to be put once only.

    Objects are considered identical if hash(object) are identical.
    """

    def __init__(self, maxsize: int=0) -> None:
        """Initialise queue with maximum number of items.

        0 for infinite queue
        """
        super().__init__(maxsize)
        self.all_items = set()  # type: Set[T]

    def _put(self, item: T) -> None:
        if item not in self.all_items:
            super()._put(item)
            self.all_items.add(item)


my_queue = queue.Queue()  # type: queue.Queue[int]
my_queue.put(1)
my_queue.put('foo')  # error

my_set_queue = SetQueue()  # type: SetQueue[int]
my_set_queue.put(1)
my_set_queue.put('foo')  # error

There may be a better way to patch in the metaclass. I'm curious to know if anyone comes up with a more elegant solution.

Edit: I should note that multiple inheritance did not work because class SetQueue(queue.Queue, Generic[T]) fails to relate SetQueue's T to queue.Queue's

like image 98
chadrik Avatar answered Oct 19 '22 17:10

chadrik


Composition vs inheritance ("has a" vs "is a") can be extremely useful here since you can specify exactly what you want typing to be rather than relying on the state of typing in your intended parent classes (which may not be great).

Below is a complete implementation of SetQueue (from the question) that 100% passes mypy --strict today without any issues (or hackery). I stripped the docstrings for brevity.

from typing import Generic, TypeVar, Set, Optional
import queue

T = TypeVar('T')  # Generic for the item type in SetQueue

class SetQueue(Generic[T]):
    def __init__(self, maxsize: int=0) -> None:
        self._queue: queue.Queue[T] = queue.Queue(maxsize)
        self.all_items: Set[T] = set()

    def _put(self, item: T) -> None:
        if item not in self.all_items:
            self._queue.put(item)
            self.all_items.add(item)

    # 100% "inherited" methods (odd formatting is to condense passthrough boilerplate)
    def task_done(self)           -> None: return self._queue.task_done()
    def join(self)                -> None: return self._queue.join()
    def qsize(self)               -> int:  return self._queue.qsize()
    def empty(self)               -> bool: return self._queue.empty()
    def full(self)                -> bool: return self._queue.full()
    def put_nowait(self, item: T) -> None: return self.put(item)
    def get_nowait(self)          -> T:    return self.get()
    def get(self, block: bool = True, timeout: Optional[float] = None) -> T:
        return self._queue.get(block, timeout)
    def put(self, item: T, block: bool = True, timeout: Optional[float] = None) -> None:
        return self._queue.put(item, block, timeout)

Although composition is definitely more verbose than inheritance (since it requires defining all the passthrough methods), code clarity can be better. In addition, you don't always want all the parent methods and composition lets you omit them.

Composing like this can be particularly important today since the current state of typing in the python ecosystem (including the python standard library) isn't 100% awesome. There are essentially two parallel worlds: 1) Actual code, and 2) typing. Although you might be subclassing a great class from a code perspective, that does not necessarily equate to inheriting great (or even functional) type definitions. Composition can circumvent this frustration.

like image 30
Russ Avatar answered Oct 19 '22 17:10

Russ