mypy: how to define a generic subclass

2019-07-04 05:52发布

问题:

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?

回答1:

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