当我第一次开始运行站立训练时,我发现当我们进行爆米花更新时,两次更新之间总是会有一个尴尬的暂停,因为没人愿意接下来再去。我很快就决定要在站立中定义一个顺序,但也要随机化顺序以使情况保持变化。这在使用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.mod
和go.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.toml
和poetry.lock
与setup.py
和requirements.txt
。当时我并没有尝试尝试不同的选择,因为我不确定Poetry是否能够用不同的项目结构来构建轮子。
总而言之,Python的打包和文件结构并不像Go那样明显。
包装出版
Go在这方面也很简单。在pkg.go.dev上列出所需的所有go.mod
文件都是有效文件。该站点还具有其他一些建议,例如稳定的标记版本和LICENSE文件。Go的程序包注册表不需要其他身份验证-当您导航到时pkg.go.dev/github.com/username/repo-name
,它会提示您触发程序包条目的自动填充:
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
第二个命令将打开一个浏览器窗口,显示该程序的源代码,并按覆盖范围进行颜色编码:
去测试覆盖率
如果你跑
$ 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
您会得到测试覆盖率的热图,其中颜色强度指示单元测试覆盖一条线的次数:
去测试覆盖率热图
(当然,您也可以获取文本输出以进行覆盖。)
不幸的是,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群文件自取还有大神在线指导交流哦