编写简单的CLI程序:Python vs Go

当我第一次开始运行站立训练时,我发现当我们进行爆米花更新时,两次更新之间总是会有一个尴尬的暂停,因为没人愿意接下来再去。我很快就决定要在站立中定义一个顺序,但也要随机化顺序以使情况保持变化。这在使用random.shuffle()以下代码的硬编码Python脚本中实现起来非常简单:

import random
from datetime import date

members = ["Alice", "Bob", "Carol", "David"]
random.shuffle(members)
print(f"# {date.today()}:\n")
[print(name) for name in members]

在终端中,其调用看起来像:

$ python standup-script-py
# 2021-04-27

David
Bob
Alice
Carol

在开始前几分钟,我可以轻松地将此输出复制并粘贴到我们的会议聊天中,这样每个人都可以提前知道顺序。无论如何,这都不是一个精心设计的程序,但它确实完成了工作。

改写

几周前,我开始学习Go。我喜欢它,很多。Go看起来很像C,没有手动内存管理功能,甚至比C稀疏的词典还简单的语法。在我的旅途中,我认为用Go重写我的小型站立式随机程序是一项有趣的练习,它具有以下附加要求:

广义的:无需对团队成员进行硬编码,最好读取定义团队花名册的TOML文件

测试覆盖

可发布到pkg.go.dev

安装到PATH与go get(我发现go install后)

纯CI / CD PR检查和自动发布

它使用的团队名册TOML如下所示:

[Subteam-1]
members = [
        "Alice",                # TOML spec allows whitespace to break arrays
        "Bob",
        "Carol",
        "David"
        ]

["Subteam 2"]                   # Keys can have whitespace in quoted strings
members = ["Erin", "Frank", "Grace", "Heidi"]

["Empty Subteam"]               # Subteam with 0 members won't be printed

["Subteam 3"]
members = [
        "Ivan",
        "Judy",
        "Mallory",
        "Niaj"
]

调用时,程序输出:

$ random-standup example-roster.toml
# 2021-03-27
## Subteam-1
Alice
David
Bob
Carol

## Subteam 2
Grace
Heidi
Frank
Erin

## Subteam 3
Judy
Niaj
Ivan
Mallory

重写:Python版

我认为尝试用Python编写相同的工具,只是比较编写CLI工具的过程(这也是我有理由写博客)的尝试,将是一个更加有趣的练习。可以在此处看到此工具的Python实现。它接受Go实施接受的相同TOML文件,并以相同的方式调用。

差异
项目结构

在这方面,Go的实现非常简单。确实,尽管有些组织声称Go显然没有对文件和文件夹应位于何处的任何要求。我的仓库中唯一真正的Go代码是2个.go文件(该程序1个文件,其测试1个文件)以及go.modgo.sum清单文件。我遇到的一个棘手问题是,我最初在其中错误地定义了模块名称go.mod-该名称必须与存储库名称(github.com/jidicula/random-standup)相匹配。

对我来说,用Python弄清楚这一点并不容易,尤其是在pyproject.toml用于依赖项规范时。我选择使用Poetry来组织我的依赖项和项目设置(稍后会详细介绍),并且Poetry具有一个内置命令(poetry new ),用于创建一个“推荐”项目结构,看起来像这样:

foo-bar
├── README.rst
├── foo_bar
│   └── __init__.py
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_foo_bar.py

(此漂亮的文件树输出由提供tree,也可以通过Homebrew获得。)

这似乎是一种更适合于Python软件包的格式,该软件包打算用作其他项目导入的库-可能对CLI工具而言是过大的。经过一番挖掘之后,我改而遵循了Python Packaging Authority建议的结构:

packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.cfg
├── setup.py  # optional, needed to make editable pip installs work
├── src/
│   └── example_pkg/
│       └── __init__.py
└── tests/

这里的关键是包的源代码在其中,project_name/src/package_name/some_name.py而其测试在中project_name/tests/test_some_name.py,其光标__init__.py位于包含.py文件的目录中。在回溯此博客文章的步骤时,我还遇到了《The Hitchhiker’s Python指南》中的推荐结构:

foo
├── LICENSE
├── README.rst
├── docs
│   ├── conf.py
│   └── index.rst
├── requirements.txt
├── sample
│   ├── __init__.py
│   ├── core.py
│   └── helpers.py
├── setup.py
└── tests
    ├── test_advanced.py
    └── test_basic.py

总体而言,非常相似,我有去,减去src/似乎没有目录做多,而且更换pyproject.tomlpoetry.locksetup.pyrequirements.txt。当时我并没有尝试尝试不同的选择,因为我不确定Poetry是否能够用不同的项目结构来构建轮子。

总而言之,Python的打包和文件结构并不像Go那样明显。

包装出版

Go在这方面也很简单。在pkg.go.dev上列出所需的所有go.mod文件都是有效文件。该站点还具有其他一些建议,例如稳定的标记版本和LICENSE文件。Go的程序包注册表不需要其他身份验证-当您导航到时pkg.go.dev/github.com/username/repo-name,它会提示您触发程序包条目的自动填充:
编写简单的CLI程序:Python vs Go_第1张图片

pkg提示
(您可以在URL的末尾添加一个version标签,例如@v1.0.0,以自动填充您的软件包的新发布的版本。)

也有一些其他编程的方式来触发另外一个包到注册表,上市这里。

再一次,Python不像Go那样简单。我选择使用Poetry确实简化了过程-我只需要运行poetry publish --build并按照提示进行PyPI用户名和密码身份验证。在我的CI配置中,这甚至更加简单,因为Poetry和PyPI允许基于令牌的身份验证-我的GitHub Actions工作流发布步骤如下所示:

  - name: Publish to PyPI
    env:
      PYPI_TOKEN: ${
     {
      secrets.PYPI_TOKEN }}
    run: |
      poetry config pypi-token.pypi $PYPI_TOKEN
      poetry publish --build

如果我在了解Go之前是使用Python进行的,则此过程似乎非常简单。即使是简化的过程,我的主要问题还是需要一个PyPI帐户。表面上,这有助于防止诸如依赖混乱之类的供应链攻击,其中一个帐户掩盖了内部软件包的名称,或者打错了一个流行的软件包,以期一个胖子的开发人员打字时过于仓促。但是,我不确定PyPI是否真的对恶意程序包进行了审查-据我所知,我的程序包没有经过检查。另一方面,Go采取了一种相当明智的方法,即将任何安全问题推迟到托管模块源代码的Forge(即GitHub,GitLab,Bitbucket等),并且没有自己的身份验证步骤。由于Go模块是通过其位置(伪造和用户名)以及程序包名称本身来命名的,因此,将公司内部的程序包名称与发布到的名称一起隐藏起来会有些棘手pkg.go.dev。此外,错字抢注仍然是可能的。

诗歌还处理了处理依赖关系和虚拟环境的极其混乱的Python环境。通过遵循PEP 631规范,它具有用于设置虚拟环境并将开发与主要依赖项分离的简单功能pyproject.toml。在Poetry出现之前,大多数项目会选择从那里使用requirements.txt和pip安装,或者使用setup.py或使用其他Python实现(例如Anaconda)。这些都不能处理以下3种情况:依赖项的版本锁定,具有相同依赖项的多个软件包的版本解析以及虚拟环境设置。使用Poetry设置此项目仅涉及:

$ poetry shell      # creates virtual environment
$ poetry install    # installs main and dev dependencies

如果我不使用Poetry,则可能必须使用setuptools程序包配置-我对这个过程不太了解,但是由于有更多的步骤,它似乎更加复杂(通常意味着需要更广泛的表面处理)。错误)。Python的打包教程首先建议使用pip自身,根据(首选)或(建议反对)从项目中构建发行档案,然后使用Twine上载构建工件。我对这种工具不熟悉,但是没有一个受PyPI祝福的方法,这本身就是造成混乱的原因。setup.cfgsetup.py

分配

Go可以编译为单个本机二进制文件-它包含运行时所需的所有内容,而无需链接到任何系统库(除非您采用更长的路径并显式链接到它们)。这种包含电池的方法最明显的优势是,Go编译器允许您使用GOOS和GOARCH环境变量将其交叉编译为44个OS /体系结构对!这意味着您可以仅在这44种OS和体系结构组合中的任何一种上运行本机二进制文件,而无需任何其他依赖项。您可以通过运行查看所有的OS / arch对go tool dist list。但是,这种包含电池的方法有明显的缺点。一个简单的Hello,World程序将包含:

package main

import "fmt"

func main() {
     
    fmt.Println("Hello, World!")
}

当它在Intel处理器上的macOS 10.15.7上使用Go 1.16.3进行编译时,Hello,World二进制文件的重量最大为1.9 MB。Go实现的第一个版本random-standup 约为3 MB。

Python的实现在如此广泛的硬件中分布并不那么简单。因为Python是一种解释性语言,所以执行Python源代码需要在主机上安装Python运行时(具有正确的版本!)。安装和配置对于嵌入式系统而言可能是乏味的,有时甚至是不可能的-即使是在个人计算机上,管理多个Python版本也是一件令人头疼的事情。在大小方面,堆栈溢出问题表明Python解释器的大小约为1 MB。再加上v1.0.0的4 KB大小random-standup-py,我们只占用了Go实现二进制文件的一半以下的空间,其中99.6%的空间用于可以被其他程序重用的运行时。

测验

Go内置了出色的测试支持。常见的Go模式是使用表驱动的测试,您可以在其中创建映射或结构列表,其中包含要测试的功能的输入以及所需的输出。在此表中,我正在创建一个表,用于测试编写的函数,该函数接受子团队成员名称和子团队名称的一部分,并返回成员的字符串化混洗列表。测试用例存储在映射中,测试名称作为关键字,结构表示测试表作为值。

tests := map[string]struct {
     
    teamMembers []string
    teamName    string
    want        string
}{
     
    "four names": {
     
        []string{
     "Alice", "Bob", "Carol", "David"},
        "Subteam 1",
        "## Subteam 1\nCarol\nBob\nAlice\nDavid\n"},
}

然后,我将遍历地图,在每次迭代时建立测试工具。里面的测试工具,我把被测试的功能,并检查是否返回结果shuffleTeam(tests["four names"].teamMembers, tests["four names"].teamName)的比赛tests["four names"].want:

for name, tt := range tests {
     
    t.Run(name, func(t *testing.T) {
     
        rand.Seed(0)
        got := shuffleTeam(tt.teamMembers, tt.teamName)
        if got != tt.want {
     
            t.Errorf("%s: got %s, want %s", name, got, tt.want)
        }
    })
}

Dave Cheney写了一篇很棒的关于表驱动测试的博客,他出于以下两个原因,建议使用地图存储测试用例:

映射键指示测试用例的名称,因此您可以轻松地找到哪个用例失败。

Go中未定义地图迭代顺序,这意味着使用地图可以帮助您找出仅以定义的顺序通过测试的条件。
所有单元测试都可以简单地运行go test-无需第三方工具。

但是,语言运行时中内置了一个更酷的工具:测试覆盖率。Rob Pike在Go中撰写了有关测试覆盖率工具的文章,该工具在设计和功能上都非常出色。tl; dr是您可以运行:

$ go test -coverprofile=coverage.out
PASS
coverage: 55.8% of statements
ok      github.com/jidicula/random-standup  0.030s
$ go tool cover -html=coverage.out

第二个命令将打开一个浏览器窗口,显示该程序的源代码,并按覆盖范围进行颜色编码:

编写简单的CLI程序:Python vs Go_第2张图片

去测试覆盖率

如果你跑

$ go test -covermode=count -coverprofile=count.out
PASS
coverage: 55.8% of statements
ok      github.com/jidicula/random-standup  0.010s
$ go tool cover -html=count.out

您会得到测试覆盖率的热图,其中颜色强度指示单元测试覆盖一条线的次数:

编写简单的CLI程序:Python vs Go_第3张图片

去测试覆盖率热图

(当然,您也可以获取文本输出以进行覆盖。)

不幸的是,Python没有内置强大的测试支持(它具有unittest,但是有点麻烦),这导致了第3方(注意到模式?)工具pytest成为事实上的测试标准。pytest很容易为它设置测试。这里指定了测试发现规则,但是pytest本质上将test在任何与test_*.py或匹配的文件中运行带有前缀的任何函数*_test.py。在这些测试功能中,assert语句用于定义和检查测试用例-如果它们失败,则整个测试将失败:

def test_standup_cli():
    runner = CliRunner()
    result = runner.invoke(standup, ["example-roster.toml"])
    assert result.exit_code == 0
    assert str(date.today()) in result.output
    assert "## Subteam-1" in result.output

通过运行以下命令来调用此测试:

$ pytest
============================= test session starts ==============================
platform darwin -- Python 3.8.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /Users/johanan/prog/random-standup-py
collected 1 item                                                               

tests/test_random_standup.py .                                           [100%]

============================== 1 passed in 0.07s ===============================

对于该程序的Python实现,这是我包括的唯一测试-它不完整,并且没有对逻辑进行彻底测试,但是对于通过简单的练习获得更好的覆盖范围,我并不感兴趣。有趣的部分是,我正在这里进行黑盒测试-我捕获的是整个程序的输出,而不是测试其中的特定单元。我没有在Go中尝试过这种方法,但是研究是否可以这样做是很有趣的。

获得测试覆盖范围并不像在Go中那样简单-尽管我没有为此程序尝试过它,但在其他项目中,我使用了其他第三方服务,例如Coverovers,它们具有用于计算覆盖率的第三方程序包。

CLI设置

Go有两个用于构建CLI接口的内置选项:os.Args,它是os程序包中的一个变量,其中包含代表程序的CLI参数的字符串切片,或者是flag程序包,它提供了一些方便的实用程序来解析CLI标志和参数。例如,flag.Arg(0)打印传递给程序的第一个非标志参数。flag还有一个Usage()函数,可以为stdout通过标志-h或–help(usage在此main()函数外部定义)时打印到的自定义帮助输出遮蔽:

func main() {
     
    flag.Usage = func() {
     
        fmt.Fprintf(os.Stderr, "%s\n", usage)
    }

    flag.Parse()
    if flag.NArg() < 1 {
     
        flag.Usage()
        os.Exit(1)
    }

    file := flag.Arg(0)

    // rest of main()
}

Python为CLI提供了一些内置选项:sys.argv,类似于Go的os.Args功能,并且功能很底层,或者argparse,它可以处理标志和参数解析。Click是另一个第三方软件包,它使用装饰符简化了代表CLI命令的函数的CLI设置:

@click.command()
@click.argument("rosterfile")
def standup(rosterfile):
    """random-standup is a tool for randomizing the order of team member
    updates in a standup meeting.
    """
    print(date.today())
    with open(rosterfile, "r") as f:
        roster = f.read()

    parsed_roster = parse(roster)

@click.command()装饰转动standup()功能为CLI命令,并@click.argument("rosterfile")给出了一个名字向被注入到帮助消息的命令所需的输入参数。帮助消息是从函数的文档字符串构建的,可以使用-h或–help标志来调用:

$ standup --help
Usage: standup [OPTIONS] ROSTERFILE

  random-standup is a tool for randomizing the order of team member updates
  in a standup meeting.

Options:
  --help  Show this message and exit.

如果使用@click.option()装饰器添加了其他选项,则这些选项也会显示在Options:帮助消息的列表中。

正如我们之前在“测试”中所看到的,Click还为CLI黑盒测试提供了一个不错的界面。

概括

总体而言,Go提供的用于构建简单的CLI工具的产品给我留下了深刻的印象。我已经对这部分进行了结构设计,以展示其内置选项与Python一样好或更好,在Python中,您通常必须获取许多第三方软件包才能简化结构或获得基本功能。Go显然在工具方面也很出色:它对测试,依赖项管理和交叉编译的核心支持比Python拥有的任何东西都领先。

在语言中内置高质量功能的主要好处是能够减少简单程序的依赖面,从而大大简化了可维护性。对于我的Go程序,我只有一个第三方依赖项来解析TOML(go-toml),而该依赖关系本身仅go-spew用于漂亮地打印其基于树的数据结构。Python实现的依赖关系图要复杂得多,即使它只有3个核心(还有更多用于格式化和插入的核心)第三方依赖关系:Click,pytest和TOML套件。

对于使用Go编写简单的CLI程序,我看到的最大缺点是它的命令式语义-从思想到Python的代码,对我来说仍然更快,这是我第一个对列表进行混排的脚本所证明的。但是,随着程序大小,复杂性或性能需求的增加,或者如果您甚至想要一些生活质量的改进(例如测试或多平台支持),我发现Go远远领先于Python。

Python学习资料与视频加Q裙:949222410群文件自取还有大神在线指导交流哦

你可能感兴趣的:(Python,python,go,编程语言,程序人生,经验分享)