作者:Charlie Marsh

译者:豌豆花下猫@Python猫

英文:Using Mypy in production at Spring (https://notes.crmarsh.com/using-mypy-in-production-at-spring)

Spring ,我们维护了一个大型的 Python 单体代码库(英:monorepo),用上了 Mypy 最严格的配置项,实现了 Mypy 全覆盖。简而言之,这意味着每个函数签名都是带注解的,并且不允许有隐式的 Any 转换。

(译注:此处的 Spring 并不是 Java 中那个著名的 Spring 框架,而是一家生物科技公司,专注于找到与年龄相关的疾病的疗法,2022 年 3 月曾获得比尔&梅琳达·盖茨基金会 120 万美元的资助。)

诚然,代码行数是一个糟糕的衡量标准,但可作一个粗略的估计:我们的代码仓有超过 30 万行 Python 代码,其中大约一半构成了核心的数据平台,另一半是由数据科学家和机器学习研究员编写的终端用户代码。

我有个大胆的猜测,就这个规模而言,这是最全面的加了类型的 Python 代码仓之一。

我们在 2019 年 7 月首次引入了 Mypy,大约一年后实现了全面的类型覆盖,从此成为了快乐的 Mypy 用户。

几周前,我跟 Leo BoytsovErik Bernhardsson 在 Twitter 上对 Python 类型有一次简短的讨论——然后我看到 Will McGugan 也对类型大加赞赏。由于 Mypy 是我们在 Spring 公司发布和迭代 Python 代码的关键部分,我想写一下我们在过去几年中大规模使用它的经验。

一句话总结:虽然采用 Mypy 是有代价的(前期和持续的投入、学习曲线等),但我发现它对于维护大型 Python 代码库有着不可估量的价值。Mymy 可能不适合于所有人,但它十分适合我。

Mypy 是什么?

(如果你很熟悉 Mypy,可跳过本节。)

Mypy 是 Python 的一个静态类型检查工具。如果你写过 Python 3,你可能会注意到 Python 支持类型注解,像这样:

def greeting(name: str) -> str:
return 'Hello ' + name

Python 在 2014 年通过 PEP-484 定义了这种类型注解语法。虽然这些注解是语言的一部分,但 Python(以及相关的第一方工具)实际上并不拿它们来强制做到类型安全。

相反,类型检查通过第三方工具来实现。Mypy 就是这样的工具。Facebook 的 Pyre也是这样的工具——但就我所知,Mypy 更受欢迎(Mypy 在 GitHub 上有两倍多的星星,它是 Pants 默认使用的工具)。IntelliJ 也有自己的类型检查工具,支持在 PyCharm 中实现类型推断。这些工具都声称自己“兼容 PEP-484”,因为它们使用 Python 本身定义的类型注解。

(译注:最著名的类型检查工具还有谷歌的pytype 和微软的pyright ,关于基本情况介绍与对比,可查阅这篇文章

换句话说:Python 认为自己的责任是定义类型注解的语法和语义(尽管 PEP-484 本身很大程度上受到了 Mypy 现有版本的启发),但有意让第三方工具来检查这些语义。

请注意,当你使用像 Mypy 这样的工具时,你是在 Python 本身之外运行它的——比如,当你运行mypy path/to/file.py 后,Mypy 会把推断出的违规代码都吐出来。Python 在运行时显露但不利用那些类型注解。

(顺便一提:在写本文时,我了解到相比于 Pypy 这样的项目,Mypy 最初有着非常不同的目标。那时还没有 PEP-484(它的灵感来自 Mypy!),所以 Mypy 定义了自己的语法,与 Python 不同,并实现了自己的运行时(也就是说,Mypy 代码是通过 Mypy 执行的)。当时,Mypy 的目标之一是利用静态类型、不可变性等来提高性能——而且明确地避开了与 CPython 兼容。Mypy 在 2013 年切换到兼容 Python 的语法,而 PEP-484 在 2015 年才推出。(“使用静态类型加速 Python”的概念催生了 Mypyc,它仍然是一个活跃的项目,可用于编译 Mypy 本身。))

在 Spring 集成 Mypy

我们在 2019 年 7 月将 Mypy 引入代码库(#1724)。当首次发起提议时,我们有两个主要的考虑:

  1. 虽然 Mypy 在 2012 年的 PyCon 芬兰大会上首次亮相,并在 2015 年初发布了兼容 PEP-484 的版本,但它仍然是一个相当新的工具——至少对我们来说是这样。尽管我们在一些相当大的 Python 代码库上工作过(在可汗学院和其它地方),但团队中没有人使用过它。
  2. 像其它增量类型检查工具一样(例如 Flow),随着代码库的注解越来越多,Mypy 的价值会与时俱增。由于 Mypy 可以并且将会用最少的注解捕获 bug,所以你在代码库上投入注解的时间越多,它就会变得越有价值。

尽管有所犹豫,我们还是决定给 Mypy 一个机会。在公司内部,我们有强烈偏好于静态类型的工程师文化(除了 Python,我们写了很多 Rust 和 TypeScript)。所以,我们准备使用 Mypy。

我们首先类型化了一些文件。一年后,我们完成了全部代码的类型化(#2622),并升级到最严格的 Mypy 设置(最关键的是 disallow_untyped_defs ,它要求对所有函数签名进行注解),从那时起,我们一直维护着这些设置。(Wolt 团队有一篇很好的文章,他们称之为“专业级的 Mypy 配置”,巧合的是,我们使用的正是这种配置。)

Mypy 配置:https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/

反馈

总体而言:我对 Mypy 持积极的看法。 作为核心基础设施的开发人员(跨服务和跨团队使用的公共库),我认为它极其有用。

我将在以后的任何 Python 项目中继续使用它。

好处

Zulip 早在 2016 年写了一篇漂亮的文章,内容关于使用 Mypy 的好处(这篇文章也被收入了 Mypy 官方文档 中)。

Zulip 博文:https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy/#benefitsofusingmypy

我不想重述静态类型的所有好处(它很好),但我想简要地强调他们在帖子中提到的几个好处:

  1. 改善可读性:有了类型注解,代码趋向于自描述(与文档字符串不同,这种描述的准确性可以静态地强制执行)。(英:self-documenting)
  2. 捕获错误:是真的!Mypy 确实能找出 bug。从始至终。
  3. 自信地重构:这是 Mypy 最有影响力的一个好处。有了 Mypy 的广泛覆盖,我可以自信地发布涉及数百甚至数千个文件的更改。当然,这与上一条好处有关——我们用 Mypy 找出的大多数 bug 都是在重构时发现的。

第三点的价值怎么强调都不为过。毫不夸张地说,在 Mypy 的帮助下,我发布更改的速度快了十倍,甚至快了一百倍。

虽然这是完全主观的,但在写这篇文章时,我意识到:我信任 Mypy。虽然程度还不及,比如说 OCaml 编译器,但它完全改变了我维护 Python 代码的关系,我无法想象回到没有注解的世界。

痛点

Zulip 的帖子同样强调了他们在迁移 Mypy 时所经历的痛点(与静态代码分析工具的交互,循环导入)。

坦率地说,我在 Mypy 上经历的痛点与 Zulip 文章中提到的不一样。我把它们分成三类:

  1. 外部库缺乏类型注解
  2. Mypy 学习曲线
  3. 对抗类型系统

让我们来逐一回顾一下:

1. 外部库缺乏类型注解

最重要的痛点是,我们引入的大多数第三方 Python 库要么是无类型的,要么不兼容 PEP-561。在实践中,这意味着对这些外部库的引用会被解析为不兼容,这会大大削弱类型的覆盖率。

每当在环境里添加一个第三方库时,我们都会在mypy.ini 里添加一个许可条目,它告诉 Mypy 要忽略那些模块的类型注解(有类型或提供类型存根的库,比较罕见):

[mypy-altair.*]
ignore_missing_imports = True [mypy-apache_beam.*]
ignore_missing_imports = True [mypy-bokeh.*]
ignore_missing_imports = True ...

由于有了这样的安全出口,即使是随便写的注解也不会生效。例如,Mypy 允许这样做:

import pandas as pd

def return_data_frame() -> pd.DataFrame:
"""Mypy interprets pd.DataFrame as Any, so returning a str is fine!"""
return "Hello, world!"

除了第三方库,我们在 Python 标准库上也遇到了一些不顺。例如,functools.lru_cache 尽管在 typeshed 里有类型注解,但由于复杂的原因,它不保留底层函数的签名,所以任何用 @functools.lru_cache 装饰的函数都会被移除所有类型注解。

例如,Mypy 允许这样做:

import functools

@functools.lru_cache
def add_one(x: float) -> float:
return x + 1 add_one("Hello, world!")

第三方库的情况正在改善。例如,NumPy 在 1.20 版本中开始提供类型。Pandas 也有一系列公开的类型存根 ,但它们被标记为不完整的。(添加存根到这些库是非常重要的,这是一个巨大的成就!)另外值得一提的是,我最近在 Twitter 上看到了 Wolt 的 Python 项目模板 ,它也默认包括类型。

所以,类型正在变得不再罕见。过去当我们添加一个有类型注解的依赖时,我会感到惊讶。有类型注解的库还是少数,并未成为主流。

2. Mypy 学习曲线

大多数加入 Spring 的人没有使用过 Mypy(写过 Python),尽管他们基本知道并熟悉 Python 的类型注解语法。

同样地,在面试中,候选人往往不熟悉typing 模块。我通常在跟候选人作广泛的技术讨论时,会展示一个使用了typing.Protocol 的代码片段,我不记得有任何候选人看到过这个特定的构造——当然,这完全没问题!但这体现了 typing 在 Python 生态的流行程度。

所以,当我们招募团队成员时,Mypy 往往是他们必须学习的新东西。虽然类型注解语法的基础很简单,但我们经常听到这样的问题:“为什么 Mypy 会这样?”、“为什么 Mypy 在这里报错?”等等。

例如,这是一个通常需要解释的例子:

if condition:
value: str = "Hello, world"
else:
# Not ok -- we declared `value` as `str`, and this is `None`!
value = None ... if condition:
value: str = "Hello, world"
else:
# Not ok -- we already declared the type of `value`.
value: Optional[str] = None ... # This is ok!
if condition:
value: Optional[str] = "Hello, world"
else:
value = None

另外,还有一个容易混淆的例子:

from typing import Literal

def my_func(value: Literal['a', 'b']) -> None:
... for value in ('a', 'b'):
# Not ok -- `value` is `str`, not `Literal['a', 'b']`.
my_func(value)

当解释之后,这些例子的“原因”是有道理的,但我不可否认的是,团队成员需要耗费时间去熟悉 Mypy。有趣的是,我们团队中有人说 PyCharm 的类型辅助感觉还不如在同一个 IDE 中使用 TypeScript 得到的有用和完整(即使有足够的静态类型)。不幸的是,这只是使用 Mypy 的代价。

除了学习曲线之外,还有持续地注解函数和变量的开销。我曾建议对某些“种类”的代码(如探索性数据分析)放宽我们的 Mypy 规则——然而,团队的感觉是注解是值得的,这件事很酷。

3. 对抗类型系统

在编写代码时,我会尽量避免几件事,以免导致自己与类型系统作斗争:写出我知道可行的代码,并强迫 Mypy 接受。

首先是@overload ,来自typing 模块:非常强大,但很难正确使用。当然,如果需要重载一个方法,我就会使用它——但是,就像我说的,如果可以的话,我宁可避免它。

基本原理很简单:

@overload
def clean(s: str) -> str:
... @overload
def clean(s: None) -> None:
... def clean(s: Optional[str]) -> Optional[str]:
if s:
return s.strip().replace("\u00a0", " ")
else:
return None

但通常,我们想要做一些事情,比如“基于布尔值返回不同的类型,带有默认值”,这需要这样的技巧:

@overload
def lookup(
paths: Iterable[str], *, strict: Literal[False]
) -> Mapping[str, Optional[str]]:
... @overload
def lookup(
paths: Iterable[str], *, strict: Literal[True]
) -> Mapping[str, str]:
... @overload
def lookup(
paths: Iterable[str]
) -> Mapping[str, Optional[str]]:
... def lookup(
paths: Iterable[str], *, strict: Literal[True, False] = False
) -> Any:
pass

即使这是一个 hack——你不能传一个boolfind_many_latest,你必须传一个字面量 TrueFalse

同样地,我也遇到过其它问题,使用 @typing.overload 或者@overload 、在类方法中使用@overload ,等等。

其次是TypedDict ,同样来自typing 模块:可能很有用,但往往会产生笨拙的代码。

例如,你不能解构一个TypedDict ——它必须用字面量 key 构造——所以下方第二种写法是行不通的:

from typing import TypedDict

class Point(TypedDict):
x: float
y: float a: Point = {"x": 1, "y": 2} # error: Expected TypedDict key to be string literal
b: Point = {**a, "y": 3}

在实践中,很难用TypedDict对象做一些 Pythonic 的事情。我最终倾向于使用 dataclasstyping.NamedTuple 对象。

第三是装饰器。Mypy 的文档 对保留签名的装饰器和装饰器工厂有一个规范的建议。它很先进,但确实有效:

F = TypeVar("F", bound=Callable[..., Any])

def decorator(func: F) -> F:
def wrapper(*args: Any, **kwargs: Any):
return func(*args, **kwargs) return cast(F, wrapper) @decorator
def f(a: int) -> str:
return str(a)

但是,我发现使用装饰器做任何花哨的事情(特别是不保留签名的情况),都会导致代码难以类型化或者充斥着强制类型转换。

这可能是一件好事!Mypy 确实改变了我编写 Python 的方式:耍小聪明的代码更难被正确地类型化,因此我尽量避免编写讨巧的代码。

(装饰器的另一个问题是我前面提过的@functools.lru_cache :由于装饰器最终定义了一个全新的函数,所以如果你不正确地注解代码,就可能会出现严重而令人惊讶的错误。)

我对循环导入也有类似的感觉——由于要导入类型作为注解使用,这就可能导致出现本可避免的循环导入(这也是 Zulip 团队强调的一个痛点)。虽然循环导入是 Mypy 的一个痛点但这通常意味着系统或代码本身存在着设计缺陷,这是 Mypy 强迫我们去考虑的问题。

不过,根据我的经验,即使是经验丰富的 Mypy 用户,在类型检查通过之前,他们也需对本来可以正常工作的代码进行一两处更正。

(顺便说一下:Python 3.10 使用ParamSpec 对装饰器的情况作了重大的改进。)

提示与技巧

最后,我要介绍几个在使用 Mypy 时很有用的技巧。

1. reveal_type

在代码中添加reveal_type 可以让 Mypy 在对文件进行类型检查时,显示出变量的推断类型。这是非常非常非常有用的。

最简单的例子是:

# No need to import anything. Just call `reveal_type`.
# Your editor will flag it as an undefined reference -- just ignore that.
x = 1
reveal_type(x) # Revealed type is "builtins.int"

当你处理泛型时,reveal_type 特别地有用,因为它可以帮助你理解泛型是如何被“填充”的、类型是否被缩小了,等等。

2. Mypy 作为一个库

Mypy 可以用作一个运行时库!

我们内部有一个工作流编排库,看起来有点像 Flyte 或 Prefect。细节并不重要,但值得注意的是,它是完全类型化的——因此我们可以静态地提升待运行任务的类型安全性,因为它们被链接在一起。

把类型弄准确是非常具有挑战性的。为了确保它完好,不被意外的Any毒害,我们在一组文件上写了调用 Mypy 的单元测试,并断言 Mypy 抛出的错误能匹配一系列预期内的异常:

def test_check_function(self) -> None:
result = api.run(
[
os.path.join(
os.path.dirname(__file__),
"type_check_examples/function.py",
),
"--no-incremental",
],
) actual = result[0].splitlines()
expected = [
# fmt: off
'type_check_examples/function.py:14: error: Incompatible return value type (got "str", expected "int")', # noqa: E501
'type_check_examples/function.py:19: error: Missing positional argument "x" in call to "__call__" of "FunctionPipeline"', # noqa: E501
'type_check_examples/function.py:22: error: Argument "x" to "__call__" of "FunctionPipeline" has incompatible type "str"; expected "int"', # noqa: E501
'type_check_examples/function.py:25: note: Revealed type is "builtins.int"', # noqa: E501
'type_check_examples/function.py:28: note: Revealed type is "builtins.int"', # noqa: E501
'type_check_examples/function.py:34: error: Unexpected keyword argument "notify_on" for "options" of "Expression"', # noqa: E501
'pipeline.py:307: note: "options" of "Expression" defined here', # noqa: E501
"Found 4 errors in 1 file (checked 1 source file)",
# fmt: on
] self.assertEqual(actual, expected)

3. GitHub 上的问题

当搜索如何解决某个类型问题时,我经常会找到 Mypy 的 GitHub Issues (比 Stack Overflow 还多)。它可能是 Mypy 类型相关问题的解决方案和 How-To 的最佳知识源头。你会发现其核心团队(包括 Guido)对重要问题的提示和建议。

主要的缺点是,GitHub Issue 中的每个评论仅仅是某个特定时刻的评论——2018 年的一个问题可能已经解决了,去年的一个变通方案可能有了新的最佳实践。所以在查阅 issue 时,一定要把这一点牢记于心。

4. typing-extensions

typing 模块在每个 Python 版本中都有很多改进,同时,还有一些特性会通过typing-extensions 模块向后移植。

例如,虽然只使用 Python 3.8,但我们借助typing-extensions ,在前面提到的工作流编排库中使用了3.10 版本的ParamSpec。(遗憾的是,PyCharm 似乎不支持通过typing-extensions 引入的ParamSpec 语法,并将其标记为一个错误,但是,还算好吧。)当然,Python 本身语法变化而出现的特性,不能通过typing-extensions 获得。

5. NewType

typing 模块中有很多有用的辅助对象,NewType 是我的最爱之一。

NewType 可让你创建出不同于现有类型的类型。例如,你可以使用NewType 来定义合规的谷歌云存储 URL,而不仅是str 类型,比如:

from typing import NewType

GCSUrl = NewType("GCSUrl", str)

def download_blob(url: GCSUrl) -> None:
... # Incompatible type "str"; expected "GCSUrl"
download_blob("gs://my_bucket/foo/bar/baz.jpg") # Ok!
download_blob(GCSUrl("gs://my_bucket/foo/bar/baz.jpg"))

通过向download_blob 的调用者指出它的意图,我们使这个函数具备了自描述能力。

我发现 NewType对于将原始类型(如 strint )转换为语义上有意义的类型特别有用。

6. 性能

Mypy 的性能并不是我们的主要问题。Mypy 将类型检查结果保存到缓存中,能加快重复调用的速度(据其文档称:“Mypy 增量地执行类型检查,复用前一次运行的结果,以加快后续运行的速度”)。

在我们最大的服务中运行 mypy,冷缓存大约需要 50-60 秒,热缓存大约需要 1-2 秒。

至少有两种方法可以加速 Mypy,这两种方法都利用了以下的技术(我们内部没有使用):

  1. Mypy 守护进程在后台持续运行 Mypy,让它在内存中保持缓存状态。虽然 Mypy 在运行后将结果缓存到磁盘,但是守护进程确实是更快。(我们使用了一段时间的默认 Mypy 守护进程,但因共享状态导致一些问题后,我禁用了它——我不记得具体细节了。)
  2. 共享远程缓存。如前所述,Mypy 在每次运行后都会将类型检查结果缓存到磁盘——但是如果在新机器或新容器上运行 Mypy(就像在 CI 上一样),则不会有缓存的好处。解决方案是在磁盘上预置一个最近的缓存结果(即,预热缓存)。Mypy 文档概述了这个过程,但它相当复杂,具体内容取决于你自己的设置。我们最终可能会在自己的 CI 系统中启用它——暂时还没有去做。

结论

Mypy 对我们产生了很大的影响,提升了我们发布代码时的信心。虽然采纳它需要付出一定的成本,但我们并不后悔。

除了工具本身的价值之外,Mypy 还是一个让人印象非常深刻的项目,我非常感谢维护者们多年来为它付出的工作。在每一个 Mypy 和 Python 版本中,我们都看到了对 typing模块、注解语法和 Mypy 本身的显著改进。(例如:新的联合类型语法( X|Y)、 ParamSpecTypeAlias,这些都包含在 Python 3.10 中。)

原文发布于 2022 年 8 月 21 日。

作者:Charlie Marsh

译者:豌豆花下猫@Python猫

英文:Using Mypy in production at Spring (https://notes.crmarsh.com/using-mypy-in-production-at-spring)

使用 Mypy 检查 30 万行 Python 代码,总结出 3 大痛点与 6 个技巧!的更多相关文章

  1. 30行Python代码实现人脸检测

    参考OpenCV自带的例子,30行Python代码实现人脸检测,不得不说,Python这个语言的优势太明显了,几乎把所有复杂的细节都屏蔽了,虽然效率较差,不过在调用OpenCV的模块时,因为模块都是C ...

  2. [置顶] 如何用PYTHON代码写出音乐

    如何用PYTHON代码写出音乐 什么是MIDI 博主本人虽然五音不全,而且唱歌还很难听,但是还是非常喜欢听歌的.我一直在做这样的尝试,就是通过人工智能算法实现机器自动的作词和编曲(在这里预告下,通过深 ...

  3. 60行python代码分析2018互联网大事件

    2018年是改革开放四十周年,也是互联网发展的重要一年.经历了区块链,人工智能潮的互联网行业逐渐迎来了冬天.这一年里有无数的事件发生着,正好学了python数据处理相关,那么就用python对18年的 ...

  4. 如何用PYTHON代码写出音乐

    什么是MIDI 博主本人虽然五音不全,而且唱歌还很难听,但是还是非常喜欢听歌的.我一直在做这样的尝试,就是通过人工智能算法实现机器自动的作词和编曲(在这里预告下,通过深度学习写歌词已经实现了,之后会分 ...

  5. 20行以内python代码画出各种减压图

    一.太阳花 看到一个很有意思的代码,你若安好,便是晴天!太阳花向你开~ 绘画效果如下: 代码如下: from turtle import * color('red', 'yellow') begin_ ...

  6. python代码检查工具pylint 让你的python更规范

    1.pylint是什么? Pylint 是一个 Python 代码分析工具,它分析 Python 代码中的错误,查找不符合代码风格标准(Pylint 默认使用的代码风格是 PEP 8,具体信息,请参阅 ...

  7. 30段极简Python代码:这些小技巧你都Get了么

    学 Python 怎样才最快,当然是实战各种小项目,只有自己去想与写,才记得住规则.本文是 30 个极简任务,初学者可以尝试着自己实现:本文同样也是 30 段代码,Python 开发者也可以看看是不是 ...

  8. 30段极简Python代码

    Python 是机器学习最广泛采用的编程语言,它最重要的优势在于编程的易用性.如果读者对基本的 Python 语法已经有一些了解,那么这篇文章可能会给你一些启发.作者简单概览了 30 段代码,它们都是 ...

  9. 解决“无法连接到Python代码运行助手。请检查本机的设置”问题

    廖雪峰老师python课程里有个代码运行助手,可以让你在线输入Python代码,然后通过本机运行的一个Python脚本来执行代码,很方便的一个脚本工具,但是很多人用过之后出现了这样的提示:“无法连接到 ...

随机推荐

  1. SAP Using Text Modules in Adobe Forms

    In this demo we will create an adobe form which displays text in two different languages (English or ...

  2. nginx配置的server_name无法访问

    问题: 我的nginx.conf配置文件中的server_name是这样子的,然后无法访问. 但是如果说server_name后面改成服务器的IP地址却是可以访问的. 解决方案: 在本机上(不是服务器 ...

  3. HBuilderX配置外部服务器(tomcat)查看编辑jsp界面

    HBuilderX配置外部服务器(tomcat)查看编辑jsp界面 一.第一种方法,通过启动本地tomcat,查看jsp 在tomcat的webapps目录下创建文件夹HBuilderX 打开HBui ...

  4. [ARC087D] Squirrel Migration 补题记录

    题目链接 简要题意: 给你一个\(N\)个节点的树,求一个\(1\cdots N\)的排列\((p_1,p_2,\cdots p_N)\) ,使得\(\sum dist(i,p_i)\)最大. 求这样 ...

  5. 注意力机制最新综述:A Comprehensive Overview of the Developments in Attention Mechanism

    (零)注意力模型(Attention Model) 1)本质:[选择重要的部分],注意力权重的大小体现选择概率值,以非均匀的方式重点关注感兴趣的部分. 2)注意力机制已成为人工智能的一个重要概念,其在 ...

  6. Linux-Day01

    Linux-Day01 课程内容 Linux简介 Linux安装 Linux常用命令 1. 前言 1.1 什么是Linux Linux是一套免费使用和自由传播的操作系统.说到操作系统,大家比较熟知的应 ...

  7. 业务可视化-让你的流程图"Run"起来(2.问题与改进)

    前言 首先,感谢大家对上一篇文章[业务可视化-让你的流程图"Run"起来]的支持. 分享一下近期我对这个项目的一些改进. 问题&改进 问题1: 流程运行开始后,异步执行,无 ...

  8. pyhon推荐的命名规范

    类别 public Internal Modules(模块) low_with_under _low_with_under Packages(包) low_with_under   Classes(类 ...

  9. APISpace 日出日落API接口 免费好用

    日出是每个早晨太阳的一半从地平线探出头的时刻,也指太阳穿过地平线的整个过程及其伴随的大气效应.日落是指由于地球自转导致太阳每天从地平线消失的时刻.从赤道上看,春分和秋分时,太阳从正西方落下.从中纬度地 ...

  10. 「一本通 1.1 例 4」加工生产调度(贪心算法)(luogu P1248)题解

    加工生产调度 题目描述 某工厂收到了 n n n 个产品的订单,这 n n n 个产品分别在 A.B 两个车间加工,并且必须先在 A 车间加工后才可以到 B 车间加工. 某个产品 i i i 在 A. ...