如何改进代码?了解通用 Python 类型合约

发布于:2022-12-10 ⋅ 阅读:(328) ⋅ 点赞:(0)

想知道在哪里可以改进你的代码?

扫码关注《Python学研大本营》,加入读者群,分享更多精彩

想知道如何改进你的代码?许多改进将直接归结为实际向函数添加类型签名并使用 mypy覆盖模糊测试。

然而,因为这本身似乎是一知半解的问题。 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 函数返回函数。

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

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@outputs是独立的装饰器,可以相互自由使用。 如果同时使用两者,您可能会担心目标函数会激活两次,但不,它不会,因为它通过将修饰函数传递给另一个函数来工作,它安全地调用,正如您在此示例中看到的那样。

@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__(). floatint都重载该方法以证明 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_intis_float是有效的扁平合同,很容易证明,并且 int实际上不能作为 float在这个系统中,所以 Python 不做任何强制。

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

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运算符,其中左侧或右侧的一个值必须满足谓词才能算作真实。 为此,我们检查每个值,并在类型列表中运行它,如果没有出现单个匹配,我们认为它是无效的。 在我们的例子中,我们把 intfloat成一个 or_contract,这将帮助我们验证传入的值。

更复杂的合同是 and_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

它的核心逻辑同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

看起来很相似吧? 那是因为它的代码几乎与 expects()outputs()。 这个在合约函数上运行而不是简单的类型构造函数,允许在函数的输入和输出阶段进行新的谓词检查。

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

在这里看着都没问题! 但是,为什么布尔值传递 is_num合同?

>>> isinstance(True, int)
True

原来Python 认为 True 是一个布尔值,也是一个数字。 它甚至有一个 __add__()方法绑定为整数加法,但不是浮点数。 这没关系,因为布尔值在传统数学中被认为是一个数字(True=0,False=1/其他)。 通常这会很好,但由于布尔不是基本加法的有用数字,我们可能想提出一个更好的选择,我认为我们都可以责怪 isinstance()对于这个有趣的小错误。

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

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

现在将确切的类构造函数与用于构造给定值的类进行比较。 我们可以扩展 or_contractand_contract也可以使用它,或者我们可以添加一个关键字切换来改变行为并添加一个 strict合同的模式,但我将把它留给读者作为练习。

使用类型注释覆盖

由于类型注释是一个直接的 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': <class 'int'>, 'return': <class 'str'>}

这将是一个相对简单的过程,可以使用我们可以调用的装饰器进行检查 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")

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

import typing

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

使用它的注释:

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

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

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

概括

我们是否应该定义一个 enforce()函数强制执行中 所有内容 的 typing库,您将有很长的路要走,以提供逻辑和巨大的 switch 语句来证明所有部分的有效性 typing图书馆。 对于大多数 Python 社区来说,这可能很好,因为这是他们都可以努力帮助他人的共同目标。 这 mypy出于这个原因,库很重要,因为它是一个很棒的静态分析工具,其他人可以加入并提供帮助,并且只需很少的努力即可开始使用。

但是,我可能更倾向于合同制度。我可能是唯一一个认为像这样智能的合同系统会更好地约束程序的人。 mypy它实际上并不能帮助 Python 开发人员编写更强大的代码,它会告诉他们目前的代码做错了什么。而合约系统有助于强制执行代码有效性并巩固抽象到位。

不管你怎么做,希望这是一篇关于如何提高 Python 代码的整体智能的启发性文章。您可以轻松编写更多合约以使代码更智能的几个示例:

  • 一定范围内的整数

  • 奇数或偶数

  • 字符串非零或一定长度

  • 检查列表是否包含统一类型

  • 限制字典的值

  • 检查某些值是否具有绑定方法

  • 自动正则表达式检查有效输入

https://dev.to/sleibrock/general-purpose-python-type-contracts-3f85

推荐书单

《Python从入门到精通》(第二版)

《Python从入门到精通(第2版)》从初学者角度出发,通过通俗易懂的语言、丰富多彩的实例,详细介绍了使用Python进行程序开发应该掌握的各方面技术。全书共分23章,包括初识Python、Python语言基础、运算符与表达式、流程控制语句、列表和元组、字典和集合、字符串、Python中使用正则表达式、函数、面向对象程序设计、模块、异常处理及程序调试、文件及目录操作、操作数据库、GUI界面编程、Pygame游戏编程、网络爬虫开发、使用进程和线程、网络编程、Web编程、Flask框架、e起去旅行网站、AI图像识别工具等内容。所有知识都结合具体实例进行介绍,涉及的程序代码都给出了详细的注释,读者可轻松领会Python程序开发的精髓,快速提升开发技能。除此之外,该书还附配了243集高清教学微视频及PPT电子教案。

《Python从入门到精通(第2版)》可作为软件开发入门者的学习用书,也可作为高等院校相关专业的教学参考用书,还可供开发人员查阅、参考使用。

这本书有如下特色:

  • 循序渐进,实战讲述

  • 243集教学微课视频,39小时知识点精讲,可听可看,随时随地扫码学

  • 趣味解读,易教易学

  • 赠送Python实战训练背记手册

  • 在线解答,高效学习

    企业QQ、QQ群在线答疑,明日学院社区答疑。

    每周清大文森学堂在线直播答疑。

购买链接:https://u.jd.com/XIgAG8g

精彩回顾

想用Python赚钱?——安排!

【案例】如何使用Flask构建天气预报 

手把手教你创建简单的Python Flask

扫码关注《Python学研大本营》,加入读者群,分享更多精彩

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到