使用通用Python类型合约改进代码

大家好,很多朋友想知道如何改进代码,实际上许多改进将直接归结为向函数添加类型签名并使用 mypy覆盖模糊测试。然而这本身似乎是一知半解的问题,mypy实现更智能的代码覆盖率,但它对运行时或程序员开发部分没有太大帮助。其实只要看第一句 typing文档展示的:

Note: The Python runtime does not enforce function and
variable type annotations. They can be used by third party
tools such as type checkers, IDEs, linters, etc.

Python是一种动态语言,因此不存在 "静态类型 "这种东西,除非使用工具 (像 mypy) 来强迫 Python 和开发项目简单地表现为一种动态语言。很久以前写的一个项目试图解决这个问题,通过使用函数装饰器来“修复”语言来做到这一点。深受 Haskell 的影响,直到今天仍然如此,并且喜欢 Haskell 或 OCaml 等函数式语言的想法,它们通过简化静态类型语言的编写来消除动态语言问题 ,这样做是因为会使编译器更加智能。

Decorator Patching(装饰器补丁)

使用装饰器来修复问题是我们可以做的最简单、最低障碍的修复,因为它只是覆盖在普通的 Python 函数之上。装饰器吸收基本信息并将其传递给目标,一旦执行,输出其目标函数的输出。

我们可以实现这两个部分: expects()函数和 outputs()expect()函数接受描述目标函数参数的参数列表,而 outputs()函数接受描述最终返回值类型的参数列表,两部分可以同时使用,也可以选择只使用其中之一,这取决于希望如何进一步巩固代码。

def expects(*types):
    def func_in(fn):
        def in_wrap(*args, **kwargs):
            if len(types) != len(args):
                raise SyntaxError(f"Expected {len(types)}, got {len(args)}.")
            for t, v in zip(types, args):
                if not isinstance(v, t):
                    raise TypeError(f"Value '{v}' not of type '{t}'")
            return fn(*args, **kwargs)
        return in_wrap
    return func_in 

def outputs(*types):
    def func_out(fn):
        def in_wrap(*args, **kwargs):
            finalv = fn(*args, **kwargs)
            if not hasattr(finalv, '__iter__'):
                if not isinstance(finalv, types):
                    raise TypeError(f"Value '{finalv}' not of type '{types}'")
            else:
                for t, v in zip(types, finalv):
                    if not isinstance(v, t):
                        raise TypeError(f"Value '{v}' not of type '{t}'")
            return finalv
        return in_wrap
    return func_out

装饰器背后的目标是将纯代码函数视为一等级别,允许传递对函数本身的引用。通过这样做,我们可以接收并调用函数,并从其他 Python 函数返回函数。

装饰器返回一个函数,该函数接受一个函数,在某些情况下,它们的存在是为了以某种有意义的方式修改函数的参数,并帮助我们修改函数的逻辑而不改变它们的大部分定义本身,或者通过重用装饰器在代码库中共享它们。我们可以编写装饰器来帮助调试函数的输入和输出,所有这些都是通过装饰来实现的。

def logger(fn):
    def handler(*args, **kwargs):
        print(f"{fn.__name__}({args}, {kwargs})")
        v = fn(*args, **kwargs)
        print(f"=> {v}")
        return v
    return handler

@logger
def f(x, y):
    return x*y

只要将参数传递到最终的函数调用,这些装饰器就可以正确地完成工作。如果同时使用两者,您可能会担心目标函数会激活两次,但这种情况不会出现,因为它通过将修饰函数传递给另一个函数来工作,正如在此示例中看到的那样。

@expects(int) 
def do_with_int(x):
    print("Hi, I handled an int")

@outputs(str) 
def output_str():
    print("Hello, returning a string")
    return "Hi, I'm a string"

@expects(int, int)
@outputs(int) 
def multiply(x, y):
    print("I don't get activated twice")
    return x * y

Flat Contracts(平面合约)

使用装饰器可以快速开始更好的类型检查,巩固代码并产生运行时错误,这些错误几乎就像编译时错误一样。但是像 Haskell 和 OCaml 这样的语言的真正威力来自于扩展类型系统的想法,它允许函数产生在一定范围内可以是任意类型的值。

以此函数为例,它将数字加一并返回:

@expects(int) 
@outputs(int) 
def add1(x):
    return x+1

add1(5) # 6
add1(5.1) # exception thrown, not an int

这 + 运算符,快捷方式 operator.add在里面 operator库,重载实现的不同数字类型 __add__(). float和 int都重载该方法以证明 operator.add方法。但是,粗略的类型检查器没有为一个输入变量绑定多种类型的方法,这会导致这种情况下的重复代码。

更好的方法是提出一个系统,该系统可用于定义一系列类型检查和其他有关值的有用事实。当然我们有 int用于类型比较和构造值的函数,但其中没有上下文关联,它纯粹是一个数字。

断言是否满足条件是我们可以称为该类型的“合同”,它必须满足定义的一些界限才能认为它有效。必须满足值和函数之间的合同协议,这样函数才能在该范围内运行,从而减少由于诸如越界、偏离一甚至为空等愚蠢的事情而导致的运行时无效值值或错误的数据。

平面合约通常是基于其边界和输入很容易证明的合约,假设我们有一个采用一种 Python 类型的合约,并检查赋予它的值是否都匹配该一种类型。

def flat_contract(t):
    def inner_check(*vals):
        for x in vals:
            if not isinstance(t, x):
                return False
        return isinstance(x, t)
    return inner_check

is_int = flat_contract(int)
is_float = flat_contract(float)

print(is_int(3)) # true
print(is_float(3)) # false
print(is_float(3.1)) # true

我们可以看到 is_int和 is_float是有效的扁平合同,很容易证明,并且 int实际上不能作为 float在这个系统中,所以 Python 不做任何强制。

下一步是提供工具来检查输入是否属于一系列类型,以满足遇到的数字重载问题。如果我们可以编写一个合约来检查一个值是否与一系列类型匹配,这将帮助编写更好、更可重用的通用 Python 代码,为此需要编写实现与 any和 all功能。

def or_contract(*types):
    def inner_check(*vals):
        for x in vals:
            res = [isinstance(x, t) for t in types]
            if not any(res):
                return False
        return True
    return inner_check

is_num = or_contract(int, float)

print(is_num(3, "seven")) # false
print(is_num(3.1, 4.1, 700)) # true
print(is_num("3.1", "300")) # false

在这里,我们强制逻辑类似于布尔值 or运算符,其中左侧或右侧的一个值必须满足谓词才能算作真实。为此检查每个值,并在类型列表中运行它,如果没有出现单个匹配,就认为它是无效的。在例子中把 int和 float成一个 or_contract,这将帮助我们验证传入的值。

def and_contract(*types):
    def inner_check(*vals):
        for x in vals:
            res = [isinstance(x, t) for t in types]
            if not all(res):
                return False
        return True
    return inner_check

is_num = and_contract(int, int)

print(is_num(3, 3.1)) # false
print(is_num(3.1, 4.1, 700)) # false
print(is_num(3, 4, 5, 6)) # true

更复杂的合同是 and_contract,其核心逻辑同or_contract一样,但之所以说这很复杂,是因为 Python 中很少有类型会满足这一规则,除非发生大量面向对象的继承。例如,一个 float不能是 int,但很少有情况需要一种类型可能需要遵守另一种类型规则的情况,例如 dict类型可能需要满足多个界限,例如 defaultdict

>>> d = {}
>>> isinstance(d, dict)
True
>>> isinstance(d, defaultdict)
False
>>> d2 = defaultdict()
>>> isinstance(d, defaultdict)
False # dict() does not qualify as defaultdict()
>>> isinstance(d2, defaultdict)
True # qualifies as a defaultdict
>>> isinstance(d2, dict)
True # also qualifies as a dict, can be and_contract'd

现在需要帮助验证合约的只是一种将其绑定到函数的方法,通过所谓的函数合约,这很像 expects()和 outputs(),但我们不能使用,因为平面合约是函数而不是严格的类型,为此将使用名称 contract_in()和 contract_out()

def contract_in(*contracts):
    if not all([callable(c) for c in contracts]):
        raise TypeError("All types must be callable contracts")
    def fn_wrap(fn):
        def arg_wrap(*args, **kwargs):
            if len(contracts) != len(args):
                raise SyntaxError(f"Expected {len(contracts)} inputs, got {len(args)}")
            for con, val in zip(contracts, args):
                if not con(val):
                    raise TypeError(f"Expecting a value to satisfy {con.__name__}, got {type(val)}")
            return fn(*args, **kwargs)
        return arg_wrap
    return fn_wrap


def contract_out(*contracts):
    def func_out(fn):
        def in_wrap(*args, **kwargs):
            finalv = fn(*args)
            if not hasattr(finalv, '__iter__'):
                for con in contracts:
                    if not con(finalv):
                        raise TypeError(f"Expecting value to satisfy {con.__name__}, got '{type(finalv)}'")
            else:
                for con, val in zip(contracts, finalv):
                    if not con(val):
                        raise TypeError(f"Expecting value to satisfy {con.__name__},  got '{type(val)}'")
            return finalv
        return in_wrap
    return func_out

这个在合约函数上运行而不是简单的类型构造函数,允许在函数的输入和输出阶段进行新的谓词检查。

is_num = or_contract(int, float)

@contract_in(is_num) 
@contract_out(is_num) 
def square(x):
    return x * x

square(500) # passes
square(True) # passes?
square("string") # fails
>>> isinstance(True, int)
True

布尔值传递 is_num合同,Python 认为 True 是一个布尔值,也是一个数字。它甚至有一个 __add__()方法绑定为整数加法,但不是浮点数,因为布尔值在传统数学中被认为是一个数字(True=0,False=1/其他)。

在某些情况下,比较类名而不是 isinstance()功能。isinstance()将遵循面向对象编程的继承,因此某些类可能会继承不期望的属性,拥有一个可能是有益的 strict_contract()函数来比较确切的类类型。

def strict_contract(t):
    def inner_wrap(val):
        return val.__class__ == t
    return inner_wrap

使用类型注释覆盖

由于类型注释是一个直接的 Python 库,因此人们想知道它们实际上是如何工作的。自从发布了几个版本以来,所有函数现在都带有一个隐藏变量,可以在此处查看:


>>> def fn(x): return x
>>> fn.__annotations__
{}
>>> from typing import *
>>> def gn(x: Any) -> Any: return x
>>> gn.__annotations__
{'x': typing.Any, 'return': typing.Any}

信息附加到函数对象,这几乎是所有第三方工具与 Python 一起工作以收集此类型信息的方式。

以这段代码为例:

def fn(x: int) -> str:
    return f"{x}"

它的注释:

>>> fn.__annotations__
{'x': , 'return': }

这将是一个相对简单的过程,使用可以调用的装饰器进行检查 enforce(),这将强制执行它用于注释函数的类型。

def enforce(fn):
    def in_wrap(*args, **kwargs):
        annotes = fn.__annotations__
        for vname, val in zip(fn.__code__.co_varnames, args):
            if not val.__class__ == annotes[vname]:
                raise SyntaxError("Mis-matched annotation type")
        finalv = fn(*args, **kwargs)
        if not finalv.__class__ == annotes['return']:
            raise SyntaxError("Mis-matched annotation type")
        return finalv
    return in_wrap

@enforce
def fn(x: int) -> str:
    print("activated")
    return "YO"

fn(500)
fn("oops")

其作用与我们之前在合约系统中定义的相似,检查类型如果没有正确输入值,则会引发错误。这段代码实际上深入到 Python 代码对象本身以扫描局部变量名称以与输入进行比较并检查有效性,但是这不会延续到这样的代码:

import typing

def fn(x: int) -> typing.List[int]:
    return [y*y for y in range(x)]

使用它的注释:

>>> fn.__annotations__
{'x': , 'return': typing.List[int]}

typing背后的重点是添加一种以更抽象的方式定义函数的方法,但由于 typing库不提供任何强制执行,这只是留给插件开发人员解决并完成所有繁忙工作的工作。typing.List描述了一些应该是列表的可迭代对象,但是由于存在延迟生成,这并不能很好地说明输出应该是一个平面列表,还是一个延迟生成的产生值的列表。

Python开发者可能是期望程序员能够为他们通过提供类型库而留给我们的巨大工作量想出一个解决方案,如果这样做 dir(typing),可以看到所有定义的名称,而且大多是枚举名称或特殊语法名称来包装其他类型。

你可能感兴趣的:(python,开发语言)