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

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

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

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

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

几周前,我跟 Leo Boytsov 和 Erik 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 pddef 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 + 1add_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 Literaldef 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 TypedDictclass Point(TypedDict):x: floaty: floata: 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 NewTypeGCSUrl = 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)

-------- End --------

图解Pandas

  • 图文00-内容框架介绍

  • 图文01-数据结构介绍

  • 图文02-创建数据对象

  • 图文03-操作Excel文件

  • 图文04-常见的数据访问

  • 图文05-常见的数据运算

  • 图文06-常见的数学计算

  • 图文07-常见的数据统计

  • 图文08-常见的数据筛选

  • 图文09-常见的缺失值处理

  • 图文10-数据合并操作

使用 Mypy 检查 30 万行 Python 代码,总结出 3 大痛点与 6 个技巧!相关推荐

  1. 使用 Mypy 检查 30 万行 Python 代码,总结出 3 大痛点与 6 个技巧

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

  2. Spring 使用 Mypy 检查 30 万行代码,总结出三大痛点与六个技巧

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

  3. 写了 30 万行基础设施代码后,我们得出 5 个有用的经验

    在 Gruntwork,我们创建并维护着一个包含 30 万行基础设施代码的库,有数百家公司在他们的生产环境中使用这个库.在这篇文章中,我将分享我们在开发和维护这个库的实践过程中学到的非常重要的五课. ...

  4. 30行python代码设计_30行Python代码实现3D数据可视化

    原标题:30行Python代码实现3D数据可视化 作者:潮汐 来源:Python技术 欢迎来到 编程教室~ 我们之前的文章中有讲解过不少 Matplotlib 的用法,比如: 之前我们基本都是用它来绘 ...

  5. 王者荣耀——bat批处理文件,自动刷金币版(脱胎于30行Python代码刷金币版),Windows双击即可运行!

    参考<30行Python代码刷王者荣耀金币>:https://segmentfault.com/a/1190000012520431 1.源代码 以下是源代码部分,全部复制到文本文档, 用 ...

  6. 如何用python破解热点_用30行Python代码制作wifi万能钥匙,邻居家wifi网速好快

    原标题:用30行Python代码制作wifi万能钥匙,邻居家wifi网速好快 当我们拖着疲惫的身体下班回到家,想开开心心的吹着空调风扇吃着西瓜,然后手机连上wifi打一把游戏好好舒服下,然而家里wif ...

  7. 用Python代码画出灰太狼

    文章目录 简介 代码 运行效果 备注 简介 用python代码画出灰太狼,仅使用turtle库.如下: 绘画过程可以在下列平台查看: 抖音:用代码画灰太狼,不是一个合格的狼,但一定是合格的丈夫和父亲 ...

  8. 用Python代码画出麻衣学姐

    文章目录 简介 代码 运行效果 备注 简介 用python代码画出麻衣学姐,仅使用turtle库.如下: 绘画过程可以在下列平台查看: 抖音:只需35秒!就可代码画出麻衣学姐! b站:只需35秒!就可 ...

  9. python求反余弦_余弦相似度计算公式:python代码找出相似文章

    余弦相似度计算公式:python代码找出相似文章 用TF-IDF算法可以自动提取关键词.除了找到关键词,怎么找到与原文章相似的其他文章.比如,"百科TA说"在词条最下方,还提供多条 ...

最新文章

  1. inner join on, left join on, right join on要详细点的介绍?内连接,左外连接,右外连接。...
  2. Spring Boot 2.4.5、2.3.10 发布
  3. APPENDIX B-菜单计划和食谱-Pagano博士的七天菜单计划样例-未完待续
  4. C++笔记——.和::和:和-的区别
  5. 深度学习面试的一些知识
  6. VMware虚拟机 硬盘空间不足 磁盘大小调整方案
  7. Fujitsu Lifebook U1010安装XP TabletPC 2005完全攻略
  8. 宝塔面板安装MySQL数据库
  9. android组件化解耦,android module解耦组件化总体概述
  10. 面试官这样,面试就有戏了!
  11. 虚幻4和Unity3D应该学哪个?
  12. 小论文中添加脚注(可以不显示标号)
  13. 推荐一波 Linux 网络工具
  14. 交换机 ensp基本命令
  15. 请问如何提高文件的读写速度
  16. STM32F1单片机零基础学习(1)
  17. dodo:人脸识别方法个人见解(三部分汇总)
  18. 中消协:多款邮箱、通讯、金融理财APP过度收集个人信息!
  19. 如何实现服务注册与发现?
  20. java删除确认_删除添加确认事件

热门文章

  1. ipv6头部格式 c语言,2.2.1 IPv6和IPv4基本头部格式
  2. 前端表格里的数据不换行
  3. python名片识别_百度AI攻略:名片识别
  4. torchaudio::is_sox_available关于使用pyinstaller 编译的问题
  5. Mobaxterm终端工具和Neokylin7基础
  6. 世界有几个终端服务器,全球互联网终端服务器共13根,美国占据10根,美真可以关闭中国网络?...
  7. 中国移动发力5G,月增900万,反超中国电信
  8. 值得您收藏的png图标第二辑
  9. [转]NSIS常用代码整理
  10. java中如何在键盘中输入一串以逗号隔开数字然后存入数组中,并输出。