How do I type hint a method with the type of the enclosing class?

前端 未结 5 525
抹茶落季
抹茶落季 2020-11-21 07:15

I have the following code in python 3:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self,          


        
5条回答
  •  迷失自我
    2020-11-21 07:55

    TL;DR: if you are using Python 3.10 or later, it just works. As of today (2019) in 3.7+ you must turn this feature on using a future statement (from __future__ import annotations) - for Python 3.6 or below use a string.

    I guess you got this exception:

    NameError: name 'Position' is not defined
    

    This is because Position must be defined before you can use it in an annotation unless you are using Python 3.10 or later.

    Python 3.7+: from __future__ import annotations

    Python 3.7 introduces PEP 563: postponed evaluation of annotations. A module that uses the future statement from __future__ import annotations will store annotations as strings automatically:

    from __future__ import annotations
    
    class Position:
        def __add__(self, other: Position) -> Position:
            ...
    

    This is scheduled to become the default in Python 3.10. Since Python still is a dynamically typed language so no type checking is done at runtime, typing annotations should have no performance impact, right? Wrong! Before python 3.7 the typing module used to be one of the slowest python modules in core so if you import typing you will see up to 7 times increase in performance when you upgrade to 3.7.

    Python <3.7: use a string

    According to PEP 484, you should use a string instead of the class itself:

    class Position:
        ...
        def __add__(self, other: 'Position') -> 'Position':
           ...
    

    If you use the Django framework this may be familiar as Django models also use strings for forward references (foreign key definitions where the foreign model is self or is not declared yet). This should work with Pycharm and other tools.

    Sources

    The relevant parts of PEP 484 and PEP 563, to spare you the trip:

    Forward references

    When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

    A situation where this occurs commonly is the definition of a container class, where the class being defined occurs in the signature of some of the methods. For example, the following code (the start of a simple binary tree implementation) does not work:

    class Tree:
        def __init__(self, left: Tree, right: Tree):
            self.left = left
            self.right = right
    

    To address this, we write:

    class Tree:
        def __init__(self, left: 'Tree', right: 'Tree'):
            self.left = left
            self.right = right
    

    The string literal should contain a valid Python expression (i.e., compile(lit, '', 'eval') should be a valid code object) and it should evaluate without errors once the module has been fully loaded. The local and global namespace in which it is evaluated should be the same namespaces in which default arguments to the same function would be evaluated.

    and PEP 563:

    In Python 3.10, function and variable annotations will no longer be evaluated at definition time. Instead, a string form will be preserved in the respective __annotations__ dictionary. Static type checkers will see no difference in behavior, whereas tools using annotations at runtime will have to perform postponed evaluation.

    ...

    The functionality described above can be enabled starting from Python 3.7 using the following special import:

    from __future__ import annotations
    

    Things that you may be tempted to do instead

    A. Define a dummy Position

    Before the class definition, place a dummy definition:

    class Position(object):
        pass
    
    
    class Position(object):
        ...
    

    This will get rid of the NameError and may even look OK:

    >>> Position.__add__.__annotations__
    {'other': __main__.Position, 'return': __main__.Position}
    

    But is it?

    >>> for k, v in Position.__add__.__annotations__.items():
    ...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
    return is Position: False
    other is Position: False
    

    B. Monkey-patch in order to add the annotations:

    You may want to try some Python meta programming magic and write a decorator to monkey-patch the class definition in order to add annotations:

    class Position:
        ...
        def __add__(self, other):
            return self.__class__(self.x + other.x, self.y + other.y)
    

    The decorator should be responsible for the equivalent of this:

    Position.__add__.__annotations__['return'] = Position
    Position.__add__.__annotations__['other'] = Position
    

    At least it seems right:

    >>> for k, v in Position.__add__.__annotations__.items():
    ...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
    return is Position: True
    other is Position: True
    

    Probably too much trouble.

    Conclusion

    If you are using 3.6 or below use a string literal containing the class name, in 3.7 use from __future__ import annotations and it will just work.

提交回复
热议问题