Skip to content

Latest commit

 

History

History
155 lines (109 loc) · 4.54 KB

GENERICS.rst

File metadata and controls

155 lines (109 loc) · 4.54 KB

Generics

The subscript operation on container types is overloaded to support specifying the item type for the container as part of a type hint. Example:

from typing import List

def space_join(a: List[str]) -> str:
    """Join list items with spaces."""
    return ' '.join(a)

Given this definition, the following code will be flagged as a type error, because the argument has type List[int] (list of integers) rather than List[str] (list of strings):

space_join([1, 2, 3])  # Error

Depending on the definition of the container, multiple type arguments may be given in this notation:

from collections import defaultdict
from typing import Iterable, Dict

def word_count(words: Iterable[str]) -> Dict[str, int]:
    """Count words in document."""
    res = defaultdict(int)
    for word in words:
        res[word] += 1
    return dict(res)

print(word_count(['to', 'be', 'or', 'not', 'to', 'be']))
# {'to': 2, 'be': 2, 'or': 1, 'not': 1}

Sometimes we want the type of several arguments and/or the return type to vary collectively. We can do this using type variables. A type variable must be defined using the TypeVar() factory, after which it can be used in mutiple function or method signatures. This is called a generic function. Example:

from collections import defaultdict
from typing import Iterable, Mapping, TypeVar

T = TypeVar('T')

def thing_count(things: Iterable[T]) -> Dict[T, int]:
    """Count words in document."""
    res = defaultdict(int)
    for thing in things:
        res[thing] += 1
    return dict(res)

print(thing_count([2, 3, 5, 7, 2, 3])
# {2: 2, 3: 2, 5: 1, 7: 1}

Note that the argument to TypeVar() must be a string, and it must be assigned to a variable with exactly that name. Type variables cannot be redefined in the same module. These constraints are enforced by the type checker (but not by the runtime implementation of TypeVar()).

We can also define generic classes, as follows:

from typing import Generic, TypeVar

T = TypeVar('T')

class Node(Generic[T]):

    def __init__(self, label: T) -> None:
        self.label = label

    def get_label(self) -> T:
        return self.label

def mknod(x: int) -> Node[int]:
    return Node(x)

print(mknod(40).get_label() + 2)
# 42

This same mechanism is used to define the container classes exported by typing (although the implementation is hairier due to the desire to also emulate collection ABCs).

Type variables have a few more tricks up their sleeves:

  • Additional positional arguments must be type expressions that will be used to constrain the types that are acceptable substitutions. This feature is used for example by the predefined type variable AnyStr, which is defined as:

    AnyStr = TypeVar('AnyStr', str, bytes)
    

    When such a constrained type variable is used in an argument type, the actual type must be a subtype of one of the constraints. When used in a return value type, the inferred return type will be exactly the corresponding constraint (not the inferred argument type, which may be a subtype thereof). For example:

    from typing import AnyStr
    
    def add_strings(a: AnyStr, b: AnyStr) -> AnyStr:
        return a+b
    
    add_string('x', 'y')  # 'xy'
    add_string(b'a', b'b')  # b'ab'
    add_string('x', b'z')  # Error
    
    class MyStr(str):
        pass
    
    add_string(MyStr('a'), MyStr('b'))  # 'ab', not MyStr('ab')
    
  • Type variables may be declared as covariant or contravariant. The default is invariant. Covariance is best explained using an example:

    from typing import TypeVar
    
    T = TypeVar('T')
    Tco = TypeVar('Tco', covariant=True)
    
    class MyTuple(Generic[Tco]):
        ...  # Implements immutable sequence operations
    
    class MyList(MyTuple[T]):
        ...  # Adds mutable sequence operations
    
    class Employee:
        ...
    
    class Manager(Employee):
        ...
    
    issubclass(MyTuple[Manager], MyTuple[Employee])  # True
    issubclass(MyList[Manager], MyList[Employee])  # False
    
    def print_employees(emps: MyTuple[Employee]) -> None:
        for emp in emps:
            print(emp)
    
    def add_employee(emps: MyList[Employee], emp: Employee) -> None:
        emps.append(emp)
    
    mgrs = MyList[Manager](...)  # Undecided if this is allowed
    print_employees(mgrs)  # OK
    bob = Manager(...)
    add_employee(mgrs, bob)  # Error
    

    For a good if theoretical explanation of covariance and contravariance see the Wikipedia article: http://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29