Should I force Python type checking?

前端 未结 7 1947
一个人的身影
一个人的身影 2020-12-09 04:56

Perhaps as a remnant of my days with a strongly-typed language (Java), I often find myself writing functions and then forcing type checks. For example:

def o         


        
7条回答
  •  眼角桃花
    2020-12-09 05:38

    If you insist on adding type checking to your code, you may want to look into annotations and how they might simplify what you have to write. One of the questions on StackOverflow introduced a small, obfuscated type-checker taking advantage of annotations. Here is an example based on your question:

    >>> def statictypes(a):
        def b(a, b, c):
            if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
            return c
        return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
    
    >>> @statictypes
    def orSearch(d: dict, query: dict) -> type(None):
        pass
    
    >>> orSearch({}, {})
    >>> orSearch([], {})
    Traceback (most recent call last):
      File "", line 1, in 
        orSearch([], {})
      File "", line 5, in 
        return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
      File "", line 5, in 
        return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
      File "", line 3, in b
        if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
    TypeError: d should be , not 
    >>> orSearch({}, [])
    Traceback (most recent call last):
      File "", line 1, in 
        orSearch({}, [])
      File "", line 5, in 
        return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
      File "", line 5, in 
        return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
      File "", line 3, in b
        if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
    TypeError: query should be , not 
    >>> 
    

    You might look at the type-checker and wonder, "What on earth is that doing?" I decided to find out for myself and turned it into readable code. The second draft eliminated the b function (you could call it verify). The third and final draft made a few improvements and is shown down below for your use:

    import functools
    
    def statictypes(func):
        template = '{} should be {}, not {}'
        @functools.wraps(func)
        def wrapper(*args):
            for name, arg in zip(func.__code__.co_varnames, args):
                klass = func.__annotations__.get(name, object)
                if not isinstance(arg, klass):
                    raise TypeError(template.format(name, klass, type(arg)))
            result = func(*args)
            klass = func.__annotations__.get('return', object)
            if not isinstance(result, klass):
                raise TypeError(template.format('return', klass, type(result)))
            return result
        return wrapper
    

    Edit:

    It has been over four years since this answer was written, and a lot has changed in Python since that time. As a result of those changes and personal growth in the language, it seems beneficial to revisit the type-checking code and rewrite it to take advantage of new features and improved coding technique. Therefore, the following revision is provided that makes a few marginal improvements to the statictypes (now renamed static_types) function decorator.

    #! /usr/bin/env python3
    import functools
    import inspect
    
    
    def static_types(wrapped):
        def replace(obj, old, new):
            return new if obj is old else obj
    
        signature = inspect.signature(wrapped)
        parameter_values = signature.parameters.values()
        parameter_names = tuple(parameter.name for parameter in parameter_values)
        parameter_types = tuple(
            replace(parameter.annotation, parameter.empty, object)
            for parameter in parameter_values
        )
        return_type = replace(signature.return_annotation, signature.empty, object)
    
        @functools.wraps(wrapped)
        def wrapper(*arguments):
            for argument, parameter_type, parameter_name in zip(
                arguments, parameter_types, parameter_names
            ):
                if not isinstance(argument, parameter_type):
                    raise TypeError(f'{parameter_name} should be of type '
                                    f'{parameter_type.__name__}, not '
                                    f'{type(argument).__name__}')
            result = wrapped(*arguments)
            if not isinstance(result, return_type):
                raise TypeError(f'return should be of type '
                                f'{return_type.__name__}, not '
                                f'{type(result).__name__}')
            return result
        return wrapper
    

提交回复
热议问题