在本系列的 上一篇文章 中，我们用 TensorFlow 构建了第一个神经网络，然后还通过 Keras 接触了第一个数据集。在本系列的第七篇文章中，我们将继续探索神经网络，并使用数据集来训练模型。我们还将介绍另一个强大的机器学习 Python 库 scikit-learn。不过在进入正题之前，我要介绍两个轰动性的人工智能应用：ChatGPT 和 DALL-E 2。（LCTT 译注：此文原文发表于 2023 年初，恰值以 ChatGPT 为代表的 AI 热潮开始掀起。）OpenAI 是一个人工智能研究实验室，它在人工智能和机器学习领域做了很多研究。 埃隆·马斯克 Elon Musk 是该组织的联合创始人之一。2022 年 11 月，该实验室推出了一款名为 ChatGPT 的在线工具。它是一个可以像人类一样聊天的人工智能聊天机器人。它是使用监督学习和强化学习技术训练的 大型语言模型 large language model （LLM）。ChatGPT 使用了 OpenAI 的 GPT-3.5 语言模型，这是 GPT-3（ 生成式预训练变换器 Generative Pre-trained Transformer ）的改进版本，GPT-3 是一种使用深度学习来生成类似人类文本的语言模型。（LCTT 译注：OpenAI 已于 2023 年 3 月 14 日 发布了 GPT-4.0，它支持图文混合的输入输出，并大幅提升了推理能力和准确性。）我仍然记得第一次使用 ChatGPT 时的兴奋。它清楚地展现了人工智能的能力。ChatGPT 的回答质量很高，通常与人类给出的答案没有区别。你可以使用它来纠正语法错误、改写句子、总结段落、编写程序等。实际上，我就用 ChatGPT 改写了本文中的许多句子。此外，我还故意使用有语法错误的文本测试了 ChatGPT，它纠正后的句子非常准确。它重新措辞和总结段落的能力也很惊人。程序员甚至有可能使用 ChatGPT 在短时间内解决编程难题。在 编程探险挑战赛 2022 Advent of Code 2022 中，就有人这样宣称（LCTT 译注：比赛官方只是没有完全禁止使用人工智能作为辅助，但是并不很推崇这样的作法。消息来源）。事实上在 2022 年 12 月，也就是 ChatGPT 发布的一个月后，Stack Overflow 发布了一条新的规定，禁止提交 GPT 或 ChatGPT 生成答案。（LCTT 译注：消息来源：Temporary policy Generative AI (e.g., ChatGPT) is banned - Meta Stack Overflow）图 1 显示了 ChatGPT 编写的将两个矩阵相加的 Python 程序。我要求用 BASIC、FORTRAN、Pascal、Haskell、Lua、Pawn、C、c++、Java 等语言编写程序，ChatGPT 总能给出答案，甚至对于像 Brainfuck 和 Ook! 这样生僻的编程语言也是如此。我很确定 ChatGPT 没有从互联网上复制程序代码。更确切地说，我认为 ChatGPT 是基于对上述编程语言的语法知识生成了这些答案的，这些知识是从训练它的大量数据中获得的。许多专家和观察人士认为，随着 ChatGPT 的发展，人工智能已经成为主流。ChatGPT 的真正力量将在未来几个月或几年里被看到。OpenAI 的另一个令人惊叹的在线人工智能工具是 DALL-E 2，它以卡通机器人 WALL-E（LCTT 译注：电源《机器人总动员》中的主角）和著名画家/艺术家 萨尔瓦多·达利 Salvador Dalí 的名字命名。DALL-E 2 是一个可以根据英文描述来生成绘画的人工智能系统。该工具支持丰富的图像风格，如油画、卡通、漫画、现实主义、超现实主义、壁画等。它还可以模仿著名画家的风格，如达利、毕加索、梵高等。由 DALL-E 2 生成的图像质量非常高。我用下面的描述测试了这个工具：“一个快乐的人在海滩旁看日出的立体主义画作”。图 2 是 DALL-E 2 根据我的描述生成的图像之一。立体主义是毕加索推广的一种绘画风格。问问你的任何一个画家朋友，他/她都会说这确实是一幅立体主义风格的画。令人惊讶的是软件——它也许很复杂——能够模仿像毕加索、梵高、达利这样的大师。我强烈建议你体验一下它。这种体验将非常有趣，同时也体现了人工智能的威力。但请记住，像 ChatGPT 和 DALL-E 2 这样的工具也会带来很多问题，比如版权侵犯、学生的作业抄袭等。（LCTT 译注：本文的题图就是 DALL-E 3 生成的。）介绍 scikit-learnscikit-learn 是一个非常强大的机器学习 Python 库。它是一个采用 新 BSD 许可协议 new BSD licence （LCTT 译注：即三句版 BSD 许可证） 的自由开源软件。scikit-learn 提供了回归、分类、聚类和降维等当面的算法，如 支持向量机 Support Vector Machine （SVM）、随机森林、k-means 聚类等。在下面关于 scikit-learn 的介绍中，我们将通过代码讨论支持向量机。支持向量机是机器学习中的一个重要的监督学习模型，可以用于分类和回归分析。支持向量机的发明人 Vladimir Vapnik 和 Alexey Chervonenkis。他们还一起提出了 VC 维 Vapnik–Chervonenkis dimension 概念，这是一个评估模型分类能力的理论框架。图 3 是使用支持向量机对数据进行分类的程序。第 1 行从 scikit-learn 导入 svm 模块。跟前面几篇中介绍的 python 库一样，scikit-learn 也可以通过 Anaconda Navigator 轻松安装。第 2 行定义了一个名为 X 的列表，其中包含训练数据。X 中的所有元素都是大小为 3 的列表。第 3 行定义了一个列表 y，其中包含列表 X 中数据的类别标签。在本例中，数据属于两个类别，标签只有 0 和 1 两种。但是使用该技术可以对多个类别的数据进行分类。第 4 行使用 svm 模块的 SVC() 方法生成一个支持向量分类器。第 5 行使用 svm 模块的 fit() 方法，根据给定的训练数据（本例中为数组 X 和 y）拟合 svm 分类器模型。最后，第 6 行和第 7 行基于该分类器进行预测。预测的结果也显示在图 3 中。可以看到，分类器能够正确区分我们提供的测试数据。图 4 中的代码是一个使用 SVM 进行回归的例子。第 1 行次从 scikit-learn 导入 svm 模块。第 2 行定义了一个名为 X 的列表，其中包含训练数据。注意，X 中的所有元素都是大小为 2 的列表。第 3 行定义了一个列表 y，其中包含与列表 X 中的数据相关的值。第 4 行使用 svm 模块的 SVR() 方法生成支持向量回归模型。第 5 行使用 svm 模块的 fit() 方法，根据给定的训练数据（本例中为数 X 和 y）拟合 svm 回归模型。最后，第 6 行根据该 svm 回归模型进行预测。此预测的结果显示在图 4 中。除了 SVR() 之外，还有 LinearSVR() 和 NuSVR() 两种支持向量回归模型。将第 4 行替换为 regr = svm.LinearSVR() 和 regr = svm.NuSVR()，并执行代码来查看这些支持向量回归模型的效果。现在让我们把注意力转到神经网络和 TensorFlow 上。但在下一篇讲无监督学习和聚类时，我们还会学习 scikit-learn 提供的其他方法。神经网络和 TensorFlow在上一篇中我们已经看到了 TensorFlow 的 nn 模块提供的 ReLU （ 整流线性单元 rectified linear unit ）和 Leaky ReLU 两个激活函数，下面再介绍两个其他激活函数。tf.nn.crelu() 是串联 ReLU 激活函数。tf.nn.elu() 是 指数线性单元 exponential linear unit 激活函数。我们将在后续用 TensorFlow 和 Keras 训练我们的第一个模型时用到其中一个激活函数。在开始训练模型之前，我想向你分享 TensorFlow 的提供的“神经网络实验场”工具。它通过可视化的方式帮助你理解神经网络的工作原理。你可以直观地向神经网络中添加神经元和隐藏层，然后训练该模型。你可以选择 Tanh、Sigmoid、Linear 和 ReLU 等激活函数。分类模型和回归模型都可以使用该工具进行分析。训练的效果以动画的形式显示。图 5 显示了一个示例神经网络和它的输出。你可以通过 https://playground.tensorflow.org 访问它。训练第一个模型现在，我们使用 上一篇 提到的 MNIST 手写数字数据集来训练模型，然后使用手写数字图像对其进行测试。完整的程序 digital.py 相对较大，为了便于理解，我将程序拆分成几个部分来解释，并且添加了额外的行号。import numpy as np from tensorflow import keras, expand_dims from tensorflow.keras import layers num_classes = 10 input_shape = (28, 28, 1) (x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data( ) 第 1 行到第 3 行加载必要的包和模块。第 4 行将类别的数量定义为 10，因为我们试图对 0 到 9 进行分类。第 5 行将输入维度定义为 (28,28,1)，这表明我们使用是 28 x 28 像素的灰度图像数据。第 6 行加载该数据集，并将其分为训练数据和测试数据。关于该数据集的更多信息可以参考 上一篇 的相关介绍。x_train = x_train.astype("float32") / 255 x_test = x_test.astype("float32") / 255 x_train = np.expand_dims(x_train, 3) x_test = np.expand_dims(x_test, 3) y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) 第 7 行和第 8 行将图像像素值从 [0,255] 转换到 [0,1]。其中 astype() 方法用于将整数值类型转换为浮点值。第 9 行和第 10 行将数组 x_test 和 x_train 的维度从 (60000,28,28) 扩展为 (60000,28,28,1)。列表 y_train 和 y_test 包含从 0 到 9 的 10 个数字的标签。第 11 行和第 12 行将列表 y_train 和 y_test 转换为二进制类别矩阵。 try: model = keras.models.load_model(“existing_model”) except IOError: model = keras.Sequential( [ keras.Input(shape=input_shape), layers.Conv2D(32, kernel_size=(3, 3), activation=”relu”), layers.MaxPooling2D(pool_size=(2, 2)), layers.Conv2D(64, kernel_size=(3, 3), activation=”relu”), layers.MaxPooling2D(pool_size=(2, 2)), layers.Flatten( ), layers.Dropout(0.5), layers.Dense(num_classes, activation=”softmax”), ] ) batch_size = 64 epochs = 25 model.compile(loss=”categorical_crossentropy”, optimizer=”adam”, metrics=[“accuracy”]) model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, validation_split=0.1) model.save(“existing_model”) 训练模型是一个处理器密集和高内存消耗的操作，我们可不希望每次运行程序时都要重新训练一遍模型。因此，在第 13 行和第 14 行中，我们先尝试从 existing_model 目录加载模型。第一次执行此代码时，没有模型存在，因此会引发异常。第 16 到 21 行通过定义、训练和保存模型来处理这个异常。第 16 行代码（跨越多行）定义了模型的结构。这一行的参数决定了模型的行为。我们使用的是一个序列模型，它有一系列顺序连接的层，每一层都有一个输入张量和一个输出张量。我们将在下一篇文章中讨论这些定义模型的参数。在此之前，将这个神经网络看作一个黑箱就可以了。第 17 行将批大小定义为 64，它决定每批计算的样本数量。第 18 行将 epoch 设置为 25，它决定了整个数据集将被学习算法处理的次数。第 19 行对模型的训练行为进行了配置。第 20 行根据给定的数据和参数训练模型。对这两行代码的详细解释将推迟到下一篇文章中。最后，第 21 行将训练好的模型保存到 existing_model 目录中。模型会以多个 .pb 文件的形式保存在该目录中。注意，第 16 到 21 行位于 except 块中。print(model.summary( )) score = model.evaluate(x_test, y_test, verbose=0) print(“Test loss:”, score[0]) print(“Test accuracy:”, score[1]) 第 22 行打印我们训练的模型的摘要信息（见图 6）。回想一下，在加载数据集时将其分为了训练数据和测试数据。第 23 行使用测试数据来测试我们训练的模型的准确性。第 24 行和第 25 行打印测试的详细信息（见图 8）。img = keras.utils.load_img("sample1.png").resize((28, 28)).convert('L') img = keras.utils.img_to_array(img) img = img.reshape((1, 28, 28, 1)) img = img.astype('float32')/255 score = model.predict(img) print(score) print("Number is", np.argmax(score)) print("Accuracy", np.max(score) * 100.0) 现在，是时候用实际数据来测试我们训练的模型了。我在纸上写了几个数字，并扫描了它们。图 7 是我用来测试模型的一个图像。第 26 行加载图像，然后将其大小调整为 28 x 28 像素，最后将其转换为灰度图像。第 27 到 29 行对图像进行必要的预处理，以便将它输入到我们训练好的模型中。第 30 行预测图像所属的类别。第 31 到 33 行打印该预测的详细信息。图 8 显示了程序 digital.py 的这部分输出。从图中可以看出，虽然图像被正确识别为 7，但置信度只有 23.77%。进一步，从图 8 中可以看到它被识别为 1 的置信度为 12.86%，被识别为 8 或 9 的置信度约为 11%。此外，该模型甚至在某些情况下会是分类错误。虽然我找不到导致性能低于标准的准确原因，但我认为相对较低的训练图像分辨率以及测试图像的质量可能是主要的影响因素。这虽然不是最好的模型，但我们现在有了第一个基于人工智能和机器学习原理的训练模型。希望在本系列的后续文章中，我们能构建出可以处理更困难任务的模型。在本文介绍了 scikit-learn，在下一篇文章中我们还会继续用到它。然后介绍了一些加深对神经网络的理解的知识和工具。我们还使用 Keras 训练了第一个模型，并用这个模型进行预测。下一篇文章将继续探索神经网络和模型训练。我们还将了解 PyTorch，这是一个基于 Torch 库的机器学习框架。PyTorch 可以用于开发 计算机视觉 computer vision （CV） 和 自然语言处理 natural language processing （NLP） 相关的应用程序。致谢：感谢我的学生 Sreyas S. 在撰写本文过程中提出的创造性建议。（题图：DA/c8e10cac-a5a5-4d53-b5eb-db06f448e60e）via: https://www.opensourceforu.com/2023/02/ai-an-introduction-to-scikit-learn-and-our-first-trained-model/作者：Deepu Benson 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在本文中我们将继续学习概率论的知识。在本系列的 上一篇文章中，我们学习了使用 Anaconda，加强了概率论的知识。在本文中我们将继续学习概率论的知识，学习使用 seaborn 和 Pandas 进行数据可视化，并进一步介绍 TensorFlow 和 Keras 的使用。让我们从增长人工智能和机器学习的理论知识开始。众所周知人工智能、机器学习、数据科学、深度学习等是当今计算机科学的热门话题。然而，计算机科学还其他热门的话题，比如 区块链 blockchain 、 物联网 Internet of Things （IoT）、 量子计算 quantum computing 等。那么，人工智能领域的发展是否会对这些技术产生积极的影响呢？首先，让我们讨论一下区块链。根据维基百科的定义，“区块链是一种分布式账本，它由不断增长的记录（称为 区块 block ）组成，这些记录使用加密技术安全地连接在一起。”乍一看，人工智能和区块链似乎是两个高速发展的独立技术。但令人惊讶的是，事实并非如此。区块链相关的行话是 完整性 integrity ，人工智能相关的行话是数据。我们将大量数据交给人工智能程序去处理。虽然这些应用程序产生了惊人的结果，但我们如何信任它们呢？这就提出了对可解释的人工智能的需求。它可以提供一定的保证，以便最终用户可以信任人工智能程序提供的结果。许多专家认为，区块链技术可以用来提高人工智能软件做出的决策的可信度。另一方面， 智能合约 smart contract （区块链技术的一部分）可以从人工智能的验证中受益。从本质上讲，智能合约和人工智能通常都是做决策。因此，人工智能的进步将对区块链技术产生积极影响，反之亦然。下面让我们讨论一下人工智能和物联网之间的影响。早期的物联网设备通常没有强大的处理能力或备用电池。这使得需要大量处理能力的机器学习的软件无法部署在物联网设备上。当时，大多数物联网设备中只部署了基于规则的人工智能软件。基于规则的人工智能的优势在于它很简单，需要相对较少的处理能力。如今的物联网设备具备更强大的处理能力，可以运行更强大的机器学习软件。特斯拉开发的高级驾驶辅助系统 特斯拉自动驾驶仪 Tesla Autopilo 是物联网与人工智能融合的典范。人工智能和物联网对彼此的发展产生了积极影响。最后，让我们讨论人工智能和量子计算是如何相互影响的。尽管量子计算仍处于起步阶段，但 量子机器学习 quantum machine learning （QML）是其中非常重要的课题。量子机器学习基于两个概念：量子数据和量子-经典混合模型。量子数据是由量子计算机产生的数据。 量子神经网络 quantum neural network （QNN）用于模拟量子计算模型。TensorFlow Quantum 是一个用于量子-经典混合机器学习的强大工具。这类工具的存在表明，在不久的将来将会有越来越多的基于量子计算的人工智能解决方案。seaborn 入门seaborn 是一个基于 Matplotlib 的数据可视化 Python 库。用它能够绘制美观且信息丰富的统计图形。通过 Anaconda Navigator 可以轻松安装 seaborn。我用 ESPNcricinfo 网站上 T20 国际板球赛的击球记录，创建了一个名为 T20.csv 的 CSV（ 逗号分隔值 comma-separated value ）文件，其中包含以下 15 列：球员姓名、职业生涯跨度、比赛场次、局数、未出局次数、总得分、最高得分、平均得分、面对球数、击球率、百分次数、五十分次数、零分次数、四分次数和六分次数。图 1 是使用 Pandas 库读取这个 CSV 文件的程序代码。我们已经在前面介绍过 Pandas 了。下面逐行解释程序代码的作用。第 1 行和第 2 行导入 Pandas 和 seaborn 包。第 3 行从 JupyterLab 的工作目录中读取文件 T20.csv。第 4 行打印元数据和第一行数据。图 1 显示了这行数据，它显示了 T20 国际板球赛中得分最高的 Virat Kohli 的击球记录。第 5 行将元数据和 T20.csv 中的前五行数据保存到 Best5.csv 中。在执行该行代码时会在 JupyterLab 的工作目录中创建这个文件。第 6 行根据列百分次数按升序对 CSV 文件进行排序，并将前 10 名世纪得分手的详细信息存储到 Highest100.csv 中。该文件也将存储在 JupyterLab 的工作目录中。最后，第7行代码提取了第 5 列（总得分）和第 7 列（平均得分）的数据信息，并生成散点图。图 2 显示了程序在执行时生成的散点图。在程序末尾添加如下代码行并再次运行。sns.kdeplot(data=df.iloc[:, [5, 7]].head(50), x=’Ave’, y=’Runs’) 这行代码调用 kdeplot() 函数绘制第 5 列和第 7 列数据的 核密度估计 Kernel Distribution Estimation （KDE）图。KDE 图可以描述连续或非参数数据变量的概率密度函数。这个定义可能无法让您了解函数 kdeploy() 将要执行的实际操作。图 3 是在单个图像上绘制的 KDE 图和散点图。从图中我们可以看到，散点图绘制的数据点被 KDE 图分组成簇。seaborn 还提供了许多其他绘图功能。在图 1 中程序的第 7 行分别替换为下面的的代码行（一次一行），并再次执行该程序，你会看到不同风格的绘图显示。探索 seaborn 提供的其他绘图功能，并选择最适合你需求的功能。sns.histplot(data=df.iloc[:, [5, 7]].head(50), x=’Ave’, y=’Runs’) sns.rugplot(data=df.iloc[:, [5, 7]].head(50), x=’Ave’, y=’Runs’) 更多概率论在本系列之前的一篇文章中，我们看到可以用正态分布来对现实场景进行建模。但正态分布只是众多重要概率分布中的一种。图 4 中的程序绘制了三种概率分布。下面我来解释这个程序。第 1 行导入 NumPy 的 random 模块。第 2 行和第 3 行导入用于绘图的 Matplotlib 和 seaborn。第 5 行生成带有参数 n（试验次数）和 p（成功概率）的 二项分布 binomial distribution 数据。二项分布是一种离散概率分布，它给出了在一系列 n 次独立实验中成功的数量。第三个参数 size 决定了输出的形状。第 6 行绘制生成的数据的直方图。由于参数 kde=True，它还会绘制 KDE 图。第三个参数 color='r' 表示绘图时使用红色。第 7 行生成一个泊松分布。泊松分布是一种离散概率分布，它给出了二项分布的极限。参数 lam 表示在固定时间间隔内发生预期事件的次数。这里的参数 size 也决定了输出的形状。第 8 行将生成的数据绘制为绿色的直方图。第 9 行生成大小为 1000 的指数分布。第 10 行将生成的数据绘制为蓝色的直方图。最后，第 11 绘制三个概率分布的所有图像（见图 5）。NumPy 的 random 模块提供了大量的其他概率分布，如 Dirichlet 分布、Gamma 分布、几何分布、拉普拉斯分布等。学习和熟悉它们将是非常值得的。现在，让我们学习线性回归。使用线性回归分析可以根据一个变量来预测一个变量的值。线性回归的一个重要应用是数据拟合。线性回归非常重要，因为它很简单。机器学习中的监督学习范式实际上就是回归建模。因此，线性回归可以被认为是一种重要的机器学习策略。这种学习范式通常被统计学家称为统计学习。线性回归是机器学习中的重要操作。NumPy 和 SciPy 都提供了线性回归的函数。下面我们展示使用 NumPy 和 SciPy 进行线性回归的示例。图 6 是使用 NumPy 进行线性回归的程序。第 1 行和第 2 行导入 NumPy 和 Matplotlib。第 4 行和第 5 行初始化变量 a和 b。第 6 行使用函数 linspace() 在 0 和 1 之间等间隔地生成 100 个数字。第 7 行使用变量 a、b 和数组 x 生成数组 y 中的值。函数 randn() 返回标准正态分布的数据。第 8 行将数组 x 和 y 中的值绘制成散点图（见图 7），图中的 100 个数据点用红色标记。第 9 行使用函数 polyfit() 执行称为 最小二乘多项式拟合 least squares polynomial fit 的线性回归技术。函数 polyfit() 的输入参数包括数组 x 和 y，以及第三个表示拟合多项式的阶数的参数，在本例中为 1，表示拟合一条直线。该函数的返回值是多项式的系数，代码中将其存储在数组 p 中。第 10 行使用函数 polyval() 对多项式求值，并将这些值存储在数组y_l中。第 11 行用蓝色绘制拟合得到的直线（见图 7）。最后，第 12 行显示所有的图像。可以用这条回归直线预测可能的 (x, y) 数据对。图 8 是使用 SciPy 进行线性回归的程序代码。下面我解释一下这个程序的工作原理。第 1 行和第 2 行导入库 NumPy 和 Matplotlib。第 3 行从库 SciPy 导入 stats 模块。第 4 到 8 行与前面的程序执行类似的任务。第 9 行使用 SciPy 的 stats 模块的 linregression() 函数计算两组测量值的线性最小二乘回归——在本例中是数组 x 和 y 中的值。该函数返回值中的 m 和 c 分别表示回归直线的 斜率 slope 和 截距 intercept 。第 10 行使用斜率和截距的值生成回归线。第 11 行用绿色绘制回归线。最后，第 12 行显示所有的图像（见图 9），其中数据点以黄色显示，回归线以绿色显示。在本系列的前面几篇文章中，我们学习了概率和统计学的一些概念。尽管还不够全面的，但我认为这已经打下了一个良好的基础，现在是时候将注意力转移到其他同样重要的问题上了。Keras 简介Keras 一般与 TensorFlow 一起使用。因此，我们先从使用 TensorFlow 开始。图 10 所示的程序虽然只包含四行代码，但确实构建了在本系列中的第一个神经网络。下面我来介绍它的工作原理。第 1 行导入库 TensorFlow。第 2 行创建了一个名为 x 的张量。第 3 行和第 4 行分别在张量 x 上应用 ReLU（ 整流线性单元 Rectified Linear Unit ）和 Leaky ReLU 激活函数。在神经网络中，激活函数定义了节点的输出由输入计算出的规则。ReLU 激活函数是定义为 Relu(x) = max(0,x)。第 3 行代码的输出如图 10 所示。可以观察到，在应用 ReLU 激活函数后，张量 x 中的负值被替换为零。Leaky ReLU 是 ReLU 激活函数的改进版本。从图 10 中的第 4 行代码的输出可以看出 Leaky ReLU 激活函数保留了全量的正值和 20% 的负值。在后面我们将会继续讨论 Keras，同时学习更多神经网络和激活函数的知识。下面我们开始使用 Keras。Keras 的安装也可以通过 Anaconda Navigator 轻松完成。图 11 所示的程序导入了我们的第一个数据集并显示了其中的一个数据样本。在下一篇文章中，我们将使用这个数据集来训练和测试模型，从而开启我们开发人工智能和机器学习程序的下一个阶段。下面介绍这个程序的工作原理。第 1 行导入 Keras。第 2 行从 Keras 导入手写数字数据集 MNIST。它包含 6 万个训练样本和 1 万个测试样本。第 3 行导入 Matplotlib。第 5 行加载 MNIST 数据集。第 6 到 9 行打印训练集和测试集的维数和维度。图 12 显示了这些代码行的输出。可以看出，训练集和测试集都是三维的，所有数据样本的分辨率都是 28 × 28。第 10 行加载了第 1234 个训练图像。第 11 行和第 12 行显示这个图像。从图 12 可以看到它是数字 3 的手写图像。本文中我们开始涉及到神经网络，并通过 Keras 接触到了第一个数据集。在下一篇文章中，我们将继续讲解神经网络和 Keras，并通过使用该数据集训练自己的模型。我们还会遇到 scikit-learn, 它是另一个强大的机器学习 Python 库 。（题图：DA/2f8f2e0c-c9a7-4a55-8a03-3b5105721013）via: https://www.opensourceforu.com/2023/01/ai-introduction-to-keras-and-our-first-data-set/作者：Deepu Benson 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
大家好！我一直在慢慢摸索如何解释 Git 中的各个核心理念（提交、分支、远程、暂存区），而提交这个概念却出奇地棘手。要明白 Git 提交是如何实现的对我来说相当简单（这些都是确定的！我可以直接查看！），但是要弄清楚别人是怎么看待提交的却相当困难。所以，就像我最近一直在做的那样，我在 Mastodon 上问了一些问题。大家是怎么看待 Git 提交的？我进行了一个 非常不科学的调查，询问大家是怎么看待 Git 提交的：是快照、差异，还是所有之前提交的列表？（当然，把它看作这三者都是合理的，但我很好奇人们的 主要 观点）。这是调查结果：结果是：51% 差异42% 快照4% 所有之前的提交的历史记录3% “其他”我很惊讶差异和快照两个选项的比例如此接近。人们还提出了一些有趣但相互矛盾的观点，比如 “在我看来，提交是一个差异，但我认为它实际上是以快照的形式实现的” 和 “在我看来，提交是一个快照，但我认为它实际上是以差异的形式实现的”。关于提交的实际实现方式，我们稍后再详谈。在我们进一步讨论之前：我们的说 “一个差异” 或 “一个快照” 都是什么意思？什么是差异？我说的“差异”可能相当明显：差异就是你在运行 git show COMMIT_ID 时得到的东西。例如，这是一个 rbspy 项目中的拼写错误修复：diff --git a/src/ui/summary.rs b/src/ui/summary.rs index 5c4ff9c..3ce9b3b 100644 --- a/src/ui/summary.rs +++ b/src/ui/summary.rs @@ -160,7 +160,7 @@ mod tests { "; let mut buf: Vec<u8> = Vec::new(); - stats.write(&mut buf).expect("Callgrind write failed"); + stats.write(&mut buf).expect("summary write failed"); let actual = String::from_utf8(buf).expect("summary output not utf8"); assert_eq!(actual, expected, "Unexpected summary output"); } 你可以在 GitHub 上看到它： https://github.com/rbspy/rbspy/commit/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b什么是快照？我说的 “快照” 是指 “当你运行 git checkout COMMIT_ID 时得到的所有文件”。Git 通常将提交的文件列表称为 “树”（如“目录树”），你可以在 GitHub 上看到上述提交的所有文件：https://github.com/rbspy/rbspy/tree/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b（它是 /tree/ 而不是 /commit/）“Git 是如何实现的”真的是正确的解释方式吗？我最常听到的关于学习 Git 的建议大概是 “只要学会 Git 在内部是如何表示事物的，一切都会变得清晰明了”。我显然非常喜欢这种观点（如果你花了一些时间阅读这个博客，你就会知道我 喜欢 思考事物在内部是如何实现的）。但是作为一个学习 Git 的方法，它并没有我希望的那么成功！通常我会兴奋地开始解释 “好的，所以 Git 提交是一个快照，它有一个指向它的父提交的指针，然后一个分支是一个指向提交的指针，然后……”，但是我试图帮助的人会告诉我，他们并没有真正发现这个解释有多有用，他们仍然不明白。所以我一直在考虑其他方案。但是让我们还是先谈谈内部实现吧。Git 是如何在内部表示提交的 —— 快照在内部，Git 将提交表示为快照（它存储每个文件当前版本的 “树”）。我在 在一个 Git 仓库中，你的文件在哪里？ 中写过这个，但下面是一个非常快速的内部格式概述。这是一个提交的表示方式：$ git cat-file -p 24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b tree e197a79bef523842c91ee06fa19a51446975ec35 parent 26707359cdf0c2db66eb1216bf7ff00eac782f65 author Adam Jensen <adam@acj.sh> 1672104452 -0500 committer Adam Jensen <adam@acj.sh> 1672104890 -0500 Fix typo in expectation message 以及，当我们查看这个树对象时，我们会看到这个提交中仓库根目录下每个文件/子目录的列表：$ git cat-file -p e197a79bef523842c91ee06fa19a51446975ec35 040000 tree 2fcc102acd27df8f24ddc3867b6756ac554b33ef .cargo 040000 tree 7714769e97c483edb052ea14e7500735c04713eb .github 100644 blob ebb410eb8266a8d6fbde8a9ffaf5db54a5fc979a .gitignore 100644 blob fa1edfb73ce93054fe32d4eb35a5c4bee68c5bf5 ARCHITECTURE.md 100644 blob 9c1883ee31f4fa8b6546a7226754cfc84ada5726 CODE_OF_CONDUCT.md 100644 blob 9fac1017cb65883554f821914fac3fb713008a34 CONTRIBUTORS.md 100644 blob b009175dbcbc186fb8066344c0e899c3104f43e5 Cargo.lock 100644 blob 94b87cd2940697288e4f18530c5933f3110b405b Cargo.toml 这意味着检出一个 Git 提交总是很快的：对 Git 来说，检出昨天的提交和检出 100 万个提交之前的提交一样容易。Git 永远不需要重新应用 10000 个差异来确定当前状态，因为提交根本就不是以差异的形式存储的。快照使用 packfile 进行压缩我刚刚提到了 Git 提交是一个快照，但是，当有人说 “在我看来，提交是一个快照，但我认为它在实现上是一个差异” 时，这其实也是对的！Git 提交并不是以你可能习惯的差异的形式表示的（它们不是以与上一个提交的差异的形式存储在磁盘上的），但基本的直觉是，如果你要对一个 10,000 行的文件编辑 500 次，那么存储 500 份文件的效率会很低。Git 有一个将文件以差异的形式存储的方法。这被称为 “packfile”，Git 会定期进行垃圾回收，将你的数据压缩成 packfile 以节省磁盘空间。当你 git clone 一个仓库时，Git 也会压缩数据。这里，我没有足够的篇幅来完整地解释 packfile 是如何工作的（Aditya Mukerjee 的 《解压 Git packfile》是我最喜欢的解释它们是如何工作的文章）。不过，我可以在这里简单总结一下我对 deltas 工作原理的理解，以及它们与 diff 的区别：对象存储为 “原始文件” 和一个 “ 变化量 delta ” 的引用变化量是一系列例如 “读取第 0 到 100 字节，然后插入字节 ‘hello there’，然后读取第 120 到 200 字节” 的指令。它从原始文件中拼凑出新的文本。所以没有 “删除” 的概念，只有复制和添加。我认为变化量的层次较少：我不知道如何检查 Git 究竟要经过多少层变化量才能得到一个给定的对象，但我的印象是通常不会很多。可能少于 10 层？不过，我很想知道如何才能真正查出来。原始文件不一定来自上一个提交，它可以是任何东西。也许它甚至可以来自一个更晚的提交？我不确定。没有一个 “正确的” 算法来计算变化量，Git 只是有一些近似的启发式算法当你查看差异时，实际上发生了一些奇怪的事情当我们运行 git show SOME_COMMIT 来查看某个提交的差异时，实际上发生的事情有点反直觉。我的理解是：Git 会在 packfile 中查找并应用变化量来重建该提交和其父提交的树。Git 会对两个目录树（当前提交的目录树和父提交的目录树）进行差异比较。通常这很快，因为几乎所有的文件都是完全一样的，所以 git 只需比较相同文件的哈希值就可以了，几乎所有时候都不用做什么。最后 Git 会展示差异所以，Git 会将变化量转换为快照，然后计算差异。它感觉有点奇怪，因为它从一个类似差异的东西开始，最终得到另一个类似差异的东西，但是变化量和差异实际上是完全不同的，所以这是说得通的。也就是说，我认为 Git 将提交存储为快照，而 packfile 只是一个实现细节，目的是节省磁盘空间并加快克隆速度。我其实从来没必要知道 packfile 是如何工作的，但它确实能帮助我理解 Git 是如何在不占用太多磁盘空间的情况下将提交快照化的。一个 “错误的” Git 理解：提交是差异我认为一个相当常见的，对 Git 的 “错误” 的理解是：提交是以基于上一个提交的差异的形式存储的（加上指向父提交的指针和作者和消息）。要获取提交的当前状态，Git 需要从头开始重新应用所有之前的提交。这个理解当然是错误的（在现实中，提交是以快照的形式存储的，差异是从这些快照计算出来的），但是对我来说它似乎非常有用而且有意义！在考虑合并提交时会有一点奇怪，但是或许我们可以说这只是基于合并提交的第一个父提交的差异。我认为这个错误的理解有的时候非常有用，而且对于日常 Git 使用来说它似乎并没有什么问题。我真的很喜欢它将我们最常使用的东西（差异）作为最基本的元素——它对我来说非常直观。我也一直在思考一些其他有用但 “错误” 的 Git 理解，比如：提交信息可以被编辑（实际上不能，你只是复制了一个相同的提交然后给了它一个新的信息，旧的提交仍然存在）提交可以被移动到一个不同的基础上（类似地，它们是被复制了）我认为有一系列非常有意义的、 “错误” 的对 Git 的理解，它们在很大程度上都受到 Git 用户界面的支持，并且在大多数情况下都不会产生什么问题。但是当你想要撤销一个更改或者出现问题时，它可能会变得混乱。将提交视为差异的一些优势就算我知道在 Git 中提交是快照，我可能大部分时间也都将它们视为差异，因为：大多时候我都在关注我正在做的 更改 —— 如果我只是改变了一行代码，显然我主要是在考虑那一行代码而不是整个代码库的当前状态点击 GitHub 上的 Git 提交或者使用 git show 时，你会看到差异，所以这只是我习惯看到的东西我经常使用变基，它就是关于重新应用差异的将提交视为快照的一些优势但是我有时也会将提交视为快照，因为：Git 经常对文件的移动感到困惑：有时我移动了一个文件并编辑了它，Git 无法识别它是否被移动过，而是显示为 “删除了 old.py，添加了 new.py”。这是因为 Git 只存储快照，所以当它显示 “移动 old.py -> new.py” 时，只是猜测，因为 old.py 和 new.py 的内容相似。这种方式更容易理解 git checkout COMMIT_ID 在做什么（重新应用 10000 个提交的想法让我感到很有压力）合并提交在我看来更像是快照，因为合并的提交实际上可以是任何东西（它只是一个新的快照！）。它帮助我理解为什么在解决合并冲突时可以进行任意更改，以及为什么在解决冲突时要小心。其他一些关于提交的理解Mastodon 的一些回复中还提到了：有关提交的 “额外的” 带外信息，比如电子邮件、GitHub 拉取请求或者你和同事的对话将“差异”视为一个“之前的状态 + 之后的状态”以及，当然，很多人根据情况的不同以不同的方式看待提交人们在谈论提交时使用的其他一些词可能不那么含糊：“修订”（似乎更像是快照）“补丁”（看起来更像是差异）就到这里吧！我很难了解人们对 Git 有哪些不同的理解。尤其棘手的是，尽管 “错误” 的理解往往非常有用，但人们却非常热衷于警惕 “错误” 的心智模式，所以人们不愿意分享他们 “错误” 的想法，生怕有什么 Git 解释者会站出来向他们解释为什么他们是错的。（这些 Git 解释者通常是出于善意的，但是无论如何它都会产生一种负面影响）但是我学到了很多！我仍然不完全清楚该如何谈论提交，但是我们最终会弄清楚的。感谢 Marco Rogers、Marie Flanagan 以及 Mastodon 上的所有人和我讨论 Git 提交。（题图：DA/cc0cada9-4945-4248-8635-3f89dcebd6ef）via: https://jvns.ca/blog/2024/01/05/do-we-think-of-git-commits-as-diffs--snapshots--or-histories/作者：Julia Evans 选题：lujun9972 译者：Cubik65536 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
快速入门使用 TextualPython 在 Linux 上有像 TkInter 这样的优秀 GUI（图形用户界面）开发库，但如果你不能运行图形应用程序怎么办？文本终端，并非只在 Linux 上有，而且 BSD 和其它的出色的类 Unix 操作系统上也有。如果你的代码是用 Python 编写的，你应该使用 Textual 来帮助你编写 TUI（文本用户界面）。在这个快速介绍中，我将向你展示两个你可以用 Textual 做的示例，并且介绍它未来可能的应用方向。所以 Textual 是什么？Textual 是一个为 Python 构建的快速应用程序开发框架，由 Textualize.io 构建。它可以让你用简单的 Python API 构建复杂的用户界面，并运行在终端或网络浏览器上！你需要的跟进这个教程的工具你需要有以下条件：具备基础的编程经验，最好熟练使用 Python。理解基础的面向对象概念，比如类和继承。一台安装了 Linux 与 Python 3.9+ 的机器一款好的编辑器（Vim 或者 PyCharm 是不错的选择）我尽可能简单化代码，以便你能轻松理解。此外，我强烈建议你下载代码，或至少按照接下来的说明安装相关程序。安装步骤首先创建一个虚拟环境：python3 -m venv ~/virtualenv/Textualize 现在，你可以克隆 Git 仓库并创建一个可以编辑的发布版本：. ~/virtualenv/Textualize/bin/activate pip install --upgrade pip pip install --upgrade wheel pip install --upgrade build pip install --editable . 或者直接从 Pypi.org 安装：. ~/virtualenv/Textualize/bin/activate pip install --upgrade KodegeekTextualize 我们的首个程序：日志浏览器这个 日志浏览器 就是一款简单的应用，能执行用户 PATH 路径上的一系列 UNIX 命令，并在任务执行完毕后捕获输出。以下是该应用的代码：import shutil from textual import on from textual.app import ComposeResult, App from textual.widgets import Footer, Header, Button, SelectionList from textual.widgets.selection_list import Selection from textual.screen import ModalScreen # Operating system commands are hardcoded OS_COMMANDS = { "LSHW": ["lshw", "-json", "-sanitize", "-notime", "-quiet"], "LSCPU": ["lscpu", "--all", "--extended", "--json"], "LSMEM": ["lsmem", "--json", "--all", "--output-all"], "NUMASTAT": ["numastat", "-z"] } class LogScreen(ModalScreen): # ... Code of the full separate screen omitted, will be explained next def __init__(self, name = None, ident = None, classes = None, selections = None): super().__init__(name, ident, classes) pass class OsApp(App): BINDINGS = [ ("q", "quit_app", "Quit"), ] CSS_PATH = "os_app.tcss" ENABLE_COMMAND_PALETTE = False # Do not need the command palette def action_quit_app(self): self.exit(0) def compose(self) -> ComposeResult: # Create a list of commands, valid commands are assumed to be on the PATH variable. selections = [Selection(name.title(), ' '.join(cmd), True) for name, cmd in OS_COMMANDS.items() if shutil.which(cmd[0].strip())] yield Header(show_clock=False) sel_list = SelectionList(*selections, id='cmds') sel_list.tooltip = "Select one more more command to execute" yield sel_list yield Button(f"Execute {len(selections)} commands", id="exec", variant="primary") yield Footer() @on(SelectionList.SelectedChanged) def on_selection(self, event: SelectionList.SelectedChanged) -> None: button = self.query_one("#exec", Button) selections = len(event.selection_list.selected) if selections: button.disabled = False else: button.disabled = True button.label = f"Execute {selections} commands" @on(Button.Pressed) def on_button_click(self): selection_list = self.query_one('#cmds', SelectionList) selections = selection_list.selected log_screen = LogScreen(selections=selections) self.push_screen(log_screen) def main(): app = OsApp() app.title = f"Output of multiple well known UNIX commands".title() app.sub_title = f"{len(OS_COMMANDS)} commands available" app.run() if __name__ == "__main__": main() 现在我们逐条梳理一下程序的代码：每个应用都扩展自 App 类。其中最重要的有 compose 与 mount 等方法。但在当前应用中，我们只实现了 compose。在 compose 方法中，你会返回一系列 组件 Widget ，并按顺序添加到主屏幕中。每一个组件都有定制自身外观的选项。你可以设定单字母的 绑定 binding ，比如此处我们设定了 q 键来退出应用（参见 action_quit_app 函数和 BINDINGS 列表）。利用 SelectionList 组件，我们展示了待运行的命令列表。然后，你可以通过 @on(SelectionList.SelectedChanged) 注解以及 on_selection 方法告知应用获取所选的内容。对于无选定元素的应对很重要，我们会根据运行的命令数量来决定是否禁用 “exec” 按钮。我们使用类似的监听器（ @on(Button.Pressed) ）来执行命令。我们做的就是将我们的选择送到一个新的屏幕，该屏幕会负责执行命令并收集结果。你注意到 CSS_PATH = "os_app.tcss" 这个变量了吗？Textual 允许你使用 CSS 来控制单个或多个组件的外观（色彩、位置、尺寸）：Screen { layout: vertical; } Header { dock: top; } Footer { dock: bottom; } SelectionList { padding: 1; border: solid $accent; width: 1fr; height: 80%; } Button { width: 1fr } 引自 Textual 官方网站：Textual 中使用的 CSS 是互联网上常见 CSS 的简化版本，容易上手。这真是太棒了，只需要用一哥独立的 样式表，就可以轻松调整应用的样式。好，我们现在来看看如何在新屏幕上展示结果。在新屏幕上展示结果以下是在新屏幕上处理输出的代码：import asyncio from typing import List from textual import on, work from textual.reactive import reactive from textual.screen import ModalScreen from textual.widgets import Button, Label, Log from textual.worker import Worker from textual.app import ComposeResult class LogScreen(ModalScreen): count = reactive(0) MAX_LINES = 10_000 ENABLE_COMMAND_PALETTE = False CSS_PATH = "log_screen.tcss" def __init__( self, name: str | None = None, ident: str | None = None, classes: str | None = None, selections: List = None ): super().__init__(name, ident, classes) self.selections = selections def compose(self) -> ComposeResult: yield Label(f"Running {len(self.selections)} commands") event_log = Log( id='event_log', max_lines=LogScreen.MAX_LINES, highlight=True ) event_log.loading = True yield event_log button = Button("Close", id="close", variant="success") button.disabled = True yield button async def on_mount(self) -> None: event_log = self.query_one('#event_log', Log) event_log.loading = False event_log.clear() lst = '\n'.join(self.selections) event_log.write(f"Preparing:\n{lst}") event_log.write("\n") for command in self.selections: self.count += 1 self.run_process(cmd=command) def on_worker_state_changed(self, event: Worker.StateChanged) -> None: if self.count == 0: button = self.query_one('#close', Button) button.disabled = False self.log(event) @work(exclusive=False) async def run_process(self, cmd: str) -> None: event_log = self.query_one('#event_log', Log) event_log.write_line(f"Running: {cmd}") # Combine STDOUT and STDERR output proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) stdout, _ = await proc.communicate() if proc.returncode != 0: raise ValueError(f"'{cmd}' finished with errors ({proc.returncode})") stdout = stdout.decode(encoding='utf-8', errors='replace') if stdout: event_log.write(f'\nOutput of "{cmd}":\n') event_log.write(stdout) self.count -= 1 @on(Button.Pressed, "#close") def on_button_pressed(self, _) -> None: self.app.pop_screen() 你会注意到：LogScreen 类扩展自 ModalScreen 类， 该类负责处理模态模式的屏幕。这个屏幕同样有一个 compose 方法，我们在这里添加了组件以展示 Unix 命令的内容。我们创建了一个叫做 mount 的新方法。一旦你用 compose 编排好组件，你就可以运行代码来获取数据，并再进一步定制它们的外观。我们使用 asyncio 运行命令，这样我们就能让 TUI 主工作线程在每个命令的结果出来时就及时更新内容。对于“工作线程”，请注意 run_process 方法上的 @work(exclusive=False) 注解，该方法用于运行命令并捕获 STDOUT + STDERR 输出。使用 工作线程 来管理并发并不复杂，尽管它们在手册中确实有专门的章节。这主要是因为运行的外部命令可能会执行很长时间。在 run_process 中，我们通过调用 write 以命令的输出内容来更新 event_log。最后，on_button_pressed 把我们带回到前一屏幕（从堆栈中移除屏幕）。这个小应用向你展示了如何一份不到 200 行的代码来编写一个简单的前端，用来运行非 Python 代码。现在我们来看一个更复杂的例子，这个例子用到了我们还未探索过的 Textual 的新特性。示例二：展示赛事成绩的表格本示例将展示如何使用 DataTable 组件在表格中展示赛事成绩。你能通过这个应用实现：通过列来排序表格选择表格中的行，完整窗口展示赛事细节，我们将使用我们在日志浏览器中看到的 “推送屏幕” 技巧。能进行表格搜索，查看选手详情，或执行其他操作如退出应用。下面，我们来看看应用代码：#!/usr/bin/env python """ Author: Jose Vicente Nunez """ from typing import Any, List from rich.style import Style from textual import on from textual.app import ComposeResult, App from textual.command import Provider from textual.screen import ModalScreen, Screen from textual.widgets import DataTable, Footer, Header MY_DATA = [ ("level", "name", "gender", "country", "age"), ("Green", "Wai", "M", "MYS", 22), ("Red", "Ryoji", "M", "JPN", 30), ("Purple", "Fabio", "M", "ITA", 99), ("Blue", "Manuela", "F", "VEN", 25) ] class DetailScreen(ModalScreen): ENABLE_COMMAND_PALETTE = False CSS_PATH = "details_screen.tcss" def __init__( self, name: str | None = None, ident: str | None = None, classes: str | None = None, row: List[Any] | None = None, ): super().__init__(name, ident, classes) # Rest of screen code will be show later class CustomCommand(Provider): def __init__(self, screen: Screen[Any], match_style: Style | None = None): super().__init__(screen, match_style) self.table = None # Rest of provider code will be show later class CompetitorsApp(App): BINDINGS = [ ("q", "quit_app", "Quit"), ] CSS_PATH = "competitors_app.tcss" # Enable the command palette, to add our custom filter commands ENABLE_COMMAND_PALETTE = True # Add the default commands and the TablePopulateProvider to get a row directly by name COMMANDS = App.COMMANDS | {CustomCommand} def action_quit_app(self): self.exit(0) def compose(self) -> ComposeResult: yield Header(show_clock=True) table = DataTable(id=f'competitors_table') table.cursor_type = 'row' table.zebra_stripes = True table.loading = True yield table yield Footer() def on_mount(self) -> None: table = self.get_widget_by_id(f'competitors_table', expect_type=DataTable) columns = [x.title() for x in MY_DATA[0]] table.add_columns(*columns) table.add_rows(MY_DATA[1:]) table.loading = False table.tooltip = "Select a row to get more details" @on(DataTable.HeaderSelected) def on_header_clicked(self, event: DataTable.HeaderSelected): table = event.data_table table.sort(event.column_key) @on(DataTable.RowSelected) def on_row_clicked(self, event: DataTable.RowSelected) -> None: table = event.data_table row = table.get_row(event.row_key) runner_detail = DetailScreen(row=row) self.show_detail(runner_detail) def show_detail(self, detailScreen: DetailScreen): self.push_screen(detailScreen) def main(): app = CompetitorsApp() app.title = f"Summary".title() app.sub_title = f"{len(MY_DATA)} users" app.run() if __name__ == "__main__": main() 有哪些部分值得我们关注呢？compose 方法中添加了 表头，“命令面板” 就位于此处，我们的表格（DataTable）也在这里。表格数据在 mount 方法中填充。我们设定了预期的绑定（BINDINGS），并指定了外部的 CSS 文件来设置样式（CSS_PATH）。默认情况下，我们无需任何设置便能使用 命令面板，但在此我们显式启用了它（ENABLE_COMMAND_PALETTE = True）。我们的应用有一个自定义表格搜索功能。当用户输入一名选手的名字后，应用会显示可能的匹配项，用户可以点击匹配项查看该选手的详细信息。这需要告诉应用我们有一个定制的命令提供者（COMMANDS = App.COMMANDS | {CustomCo_ mmand}），即类 CustomCommand(Provider)。如果用户点击了表头，表格内容会按照该列进行排序。这是通过 on_header_clicked 方法实现的，该方法上具有 @on(DataTable.HeaderSelected) 注解。类似地，当选中表格中的一行时， on_row_clicked 方法会被调用，这得益于它拥有 @on(DataTable.RowSelected) 注解。当方法接受选中的行后，它会推送一个新的屏幕，显示选中行的详细信息（class DetailScreen(ModalScreen)）。现在，我们详细地探讨一下如何显示选手的详细信息。利用多屏展示复杂视图当用户选择表格中的一行，on_row_clicked 方法就会被调用。它收到的是一个 DataTable.RowSelected 类型的事件。从这里我们会用选中的行的内容构建一个 DetailScreen(ModalScreen) 类的实例：from typing import Any, List from textual import on from textual.app import ComposeResult from textual.screen import ModalScreen from textual.widgets import Button, MarkdownViewer MY_DATA = [ ("level", "name", "gender", "country", "age"), ("Green", "Wai", "M", "MYS", 22), ("Red", "Ryoji", "M", "JPN", 30), ("Purple", "Fabio", "M", "ITA", 99), ("Blue", "Manuela", "F", "VEN", 25) ] class DetailScreen(ModalScreen): ENABLE_COMMAND_PALETTE = False CSS_PATH = "details_screen.tcss" def __init__( self, name: str | None = None, ident: str | None = None, classes: str | None = None, row: List[Any] | None = None, ): super().__init__(name, ident, classes) self.row: List[Any] = row def compose(self) -> ComposeResult: self.log.info(f"Details: {self.row}") columns = MY_DATA[0] row_markdown = "\n" for i in range(0, len(columns)): row_markdown += f"* **{columns[i].title()}:** {self.row[i]}\n" yield MarkdownViewer(f"""## User details: {row_markdown} """) button = Button("Close", variant="primary", id="close") button.tooltip = "Go back to main screen" yield button @on(Button.Pressed, "#close") def on_button_pressed(self, _) -> None: self.app.pop_screen() 这个类的职责很直接：compose 方法取得此行数据，并利用一个 支持 Markdown 渲染的组件 来展示内容。它的便利之处在于，它会为我们自动生成一个内容目录。当用户点击 “close” 后，方法 on_button_pressed 会引导应用回到原始屏幕。注解 @on(Button.Pressed, "#close") 用来接收按键被点击的事件。最后，我们来详细讲解一下那个多功能的搜索栏（也叫做命令面板）。命令面板的搜索功能任何使用了表头的 Textual 应用都默认开启了 命令面板。有意思的是，你可以在 CompetitorsApp 类中添加自定义的命令，这会增加到默认命令集之上：COMMANDS = App.COMMANDS | {CustomCommand} 然后是执行大部分任务的 CustomCommand(Provider) 类：from functools import partial from typing import Any, List from rich.style import Style from textual.command import Provider, Hit from textual.screen import ModalScreen, Screen from textual.widgets import DataTable from textual.app import App class CustomCommand(Provider): def __init__(self, screen: Screen[Any], match_style: Style | None = None): super().__init__(screen, match_style) self.table = None async def startup(self) -> None: my_app = self.app my_app.log.info(f"Loaded provider: CustomCommand") self.table = my_app.query(DataTable).first() async def search(self, query: str) -> Hit: matcher = self.matcher(query) my_app = self.screen.app assert isinstance(my_app, CompetitorsApp) my_app.log.info(f"Got query: {query}") for row_key in self.table.rows: row = self.table.get_row(row_key) my_app.log.info(f"Searching {row}") searchable = row[1] score = matcher.match(searchable) if score > 0: runner_detail = DetailScreen(row=row) yield Hit( score, matcher.highlight(f"{searchable}"), partial(my_app.show_detail, runner_detail), help=f"Show details about {searchable}" ) class DetailScreen(ModalScreen): def __init__( self, name: str | None = None, ident: str | None = None, classes: str | None = None, row: List[Any] | None = None, ): super().__init__(name, ident, classes) # Code of this class explained on the previous section class CompetitorsApp(App): # Add the default commands and the TablePopulateProvider to get a row directly by name COMMANDS = App.COMMANDS | {CustomCommand} # Most of the code shown before, only displaying relevant code def show_detail(self, detailScreen: DetailScreen): self.push_screen(detailScreen) 所有继承自 Provider 的类需实现 search 方法。在我们的例子中，我们还覆盖了 startup 方法，为了获取到我们应用表格（和其内容）的引用，这里使用到了 App.query(DataTable).first()。在类的生命周期中， startup 方法只会被调用一次。在 search 方法内，我们使用 Provider.matcher 对每个表格行的第二列（即名字）进行模糊搜索，以与用户在 TUI 中输入的词条进行比较。matcher.match(searchable) 返回一个整型的评分，大于零说明匹配成功。在 search 方法中，如果评分大于零，则返回一个 Hit 对象，以告知命令面板搜索查询是否成功。每个 Hit 都有以下信息：评分（用于在命令面板中对匹配项排序）、高亮显示的搜索词、一个可调用对象的引用（在我们的案例中，它是一个可以将表格行推送到新屏幕的函数）。Provider 类的所有方法都是异步的。这使你能释放主线程，只有当响应准备好后才返回结果，这个过程不会冻结用户界面。理解了这些信息，我们就可以现在展示赛手的详细信息了。尽管这个架构的追踪功能相对直观，但是组件间传递的消息复杂性不可忽视。幸运的是，Textual 提供了有效的调试工具帮助我们理解背后的工作原理。Textual 应用的问题排查对于 Python 的 Textual 应用进行 调试 相较而言更具挑战性。这是因为其中有一些操作可能是异步的，而在解决组件问题时设置断点可能颇为复杂。根据具体情况，你可以使用一些工具。但首先，确保你已经安装了 textual 的开发工具：pip install textual-dev==1.3.0 确保你能捕捉到正确的按键不确定 Textual 应用是否能捕捉到你的按键操作？运行 keys 应用：textual keys 这让你能够验证一下你的按键组合，并确认在 Textual 中产生了哪些事件。图片比千言万语更直观如果说你在布局设计上遇到了问题，想向他人展示你当前的困境，Textual 为你的运行应用提供了截图功能：textual run --screenshot 5 ./kodegeek_textualize/log_scroller.py 就像你所看到的，我是通过这种方式为这篇教程创建了插图。捕获事件并输出定制消息在 Textual 中，每一个应用实例都有一个日志记录器，可以使用如下方式访问：my_app = self.screen.app my_app.log.info(f"Loaded provider: CustomCommand") 想要查看这些消息，首先需要开启一个控制台：. ~/virtualenv/Textualize/bin/activate textual console 然后在另一个终端运行你的应用程序：. ~/virtualenv/Textualize/bin/activate textual run --dev ./kodegeek_textualize/log_scroller.py 在运行控制台的终端中，你可以看到实时的事件和消息输出：▌Textual Development Console v0.46.0 ▌Run a Textual app with textual run --dev my_app.py to connect. ▌Press Ctrl+C to quit. ─────────────────────────────────────────────────────────────────────────────── Client '127.0.0.1' connected ──────────────────────────────────────────────────────────────────────────────── [20:29:43] SYSTEM app.py:2188 Connected to devtools ( ws://127.0.0.1:8081 ) [20:29:43] SYSTEM app.py:2192 --- [20:29:43] SYSTEM app.py:2194 driver=<class 'textual.drivers.linux_driver.LinuxDriver'> [20:29:43] SYSTEM app.py:2195 loop=<_UnixSelectorEventLoop running=True closed=False debug=False> [20:29:43] SYSTEM app.py:2196 features=frozenset({'debug', 'devtools'}) [20:29:43] SYSTEM app.py:2228 STARTED FileMonitor({PosixPath('/home/josevnz/TextualizeTutorial/docs/Textualize/kodegeek_textualize/os_app.tcss')}) [20:29:43] EVENT 此外，以开发者模式运行的另一大好处是，如果你更改了 CSS，应用会尝试重新渲染，而无需重启程序。如何编写单元测试为你全新开发的 Textual 应用编写 单元测试，应该如何操作呢？在 官方文档 展示了几种用于测试我们应用的方式。我将采用 unittest 进行测试。为了处理异步例程，我们会需要特别的类 unittest.IsolatedAsyncioTestCase ：import unittest from textual.widgets import Log, Button from kodegeek_textualize.log_scroller import OsApp class LogScrollerTestCase(unittest.IsolatedAsyncioTestCase): async def test_log_scroller(self): app = OsApp() self.assertIsNotNone(app) async with app.run_test() as pilot: # Execute the default commands await pilot.click(Button) await pilot.pause() event_log = app.screen.query(Log).first() # We pushed the screen, query nodes from there self.assertTrue(event_log.lines) await pilot.click("#close") # Close the new screen, pop the original one await pilot.press("q") # Quit the app by pressing q if __name__ == '__main__': unittest.main() 现在让我们详细看看 test_log_scroller 方法中的操作步骤：通过 app.run_test() 获取一个 Pilot 实例。然后点击主按钮，运行包含默认指令的查询，随后等待所有事件的处理。从我们新推送出的屏幕中获取 Log，确保我们已获得几行返回的内容，即它并非空的。关闭新屏幕并重新呈现旧屏幕。最后，按下 q，退出应用。可以测试表格吗？import unittest from textual.widgets import DataTable, MarkdownViewer from kodegeek_textualize.table_with_detail_screen import CompetitorsApp class TableWithDetailTestCase(unittest.IsolatedAsyncioTestCase): async def test_app(self): app = CompetitorsApp() self.assertIsNotNone(app) async with app.run_test() as pilot: """ Test the command palette """ await pilot.press("ctrl+\\") for char in "manuela".split(): await pilot.press(char) await pilot.press("enter") markdown_viewer = app.screen.query(MarkdownViewer).first() self.assertTrue(markdown_viewer.document) await pilot.click("#close") # Close the new screen, pop the original one """ Test the table """ table = app.screen.query(DataTable).first() coordinate = table.cursor_coordinate self.assertTrue(table.is_valid_coordinate(coordinate)) await pilot.press("enter") await pilot.pause() markdown_viewer = app.screen.query(MarkdownViewer).first() self.assertTrue(markdown_viewer) # Quit the app by pressing q await pilot.press("q") if __name__ == '__main__': unittest.main() 如果你运行所有的测试，你将看到如下类似的输出：(Textualize) [josevnz@dmaf5 Textualize]$ python -m unittest tests/*.py .. ---------------------------------------------------------------------- Ran 2 tests in 2.065s OK 这是测试 TUI 的一个不错的方式，对吧？打包 Textual 应用打包 Textual 应用与打包常规 Python 应用并没有太大区别。你需要记住，需要包含那些控制应用外观的 CSS 文件：. ~/virtualenv/Textualize/bin/activate python -m build pip install dist/KodegeekTextualize-*-py3-none-any.whl 这个教程的 pyproject.toml 文件是一个打包应用的良好起点，告诉你需要做什么。[build-system] requires = [ "setuptools >= 67.8.0", "wheel>=0.42.0", "build>=1.0.3", "twine>=4.0.2", "textual-dev>=1.2.1" ] build-backend = "setuptools.build_meta" [project] name = "KodegeekTextualize" version = "0.0.3" authors = [ {name = "Jose Vicente Nunez", email = "kodegeek.com@protonmail.com"}, ] description = "Collection of scripts that show how to use several features of textualize" readme = "README.md" requires-python = ">=3.9" keywords = ["running", "race"] classifiers = [ "Environment :: Console", "Development Status :: 4 - Beta", "Programming Language :: Python :: 3", "Intended Audience :: End Users/Desktop", "Topic :: Utilities" ] dynamic = ["dependencies"] [project.scripts] log_scroller = "kodegeek_textualize.log_scroller:main" table_detail = "kodegeek_textualize.table_with_detail_screen:main" [tool.setuptools] include-package-data = true [tool.setuptools.packages.find] where = ["."] exclude = ["test*"] [tool.setuptools.package-data] kodegeek_textualize = ["*.txt", "*.tcss", "*.csv"] img = ["*.svg"] [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} 未来计划这个简短的教程只覆盖了 Textual 的部分方面。还有很多需要探索和学习的内容：强烈建议你查看 官方教程。有大量的示例和指向参考 API 的链接。Textual 可以使用来自 Rich 项目的组件，这个项目是一切的起源。我认为其中一些甚至可能所有这些组件在某些时候都会合并到 Textual 中。Textual 框架对于使用高级 API 的复杂应用更能胜任，但 Rich 也有很多漂亮的功能。创建你自己的组件！同样，在设计 TUI 时，拿一张纸，画出你希望这些组件如何布局的，这会为你后期省去很多时间和麻烦。调试 Python 应用可能会有点复杂。有时你可能需要 混合使用不同的工具 来找出应用的问题所在。异步 IO 是一个复杂的话题，你应该 阅读开发者文档 来了解更多可能的选择。Textual 被其他项目所使用。其中一个非常易于使用的项目是 Trogon。它会让你的 CLI 可以自我发现。Textual-web 是个很有前景的项目，能让你在浏览器上运行 Textual 应用。尽管它不如 Textual 成熟，但它的进化速度非常快。最后，查看这些外部项目。在项目组合中有许多有用的开源应用。（题图：DA/f11b0eb0-8e16-4cbe-986a-0fe978f6732a）via: https://fedoramagazine.org/crash-course-on-using-textual/作者：Jose Nunez 选题：lujun9972 译者：ChatGPT 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Vely 可让你在网络应用程序中利用 C 语言的强大功能。Vely 将 C 语言的高性能和低内存占用与 PHP 等语言的易用性和安全性相结合。作为自由开源软件，它以 GPLv3 和 LGPL 3 授权，所以你甚至可以用它来构建商业软件。利用 Vely 构建 SaaS你可以使用 Vely 创建一个多租户网络应用程序，它可以作为软件即服务模式（SaaS）在互联网上运行。每个用户都有一个完全独立的数据空间。在这个网络应用程序示例中，用户可以注册一个笔记本服务来创建笔记，然后查看和删除它们。它仅用了 7 个源文件，310 行代码，就展示了如何集成多项技术:MariaDB网络浏览器ApacheUnix 套接字运作原理以下是从用户的角度来看应用程序是如何工作的。下图是代码演示。该应用允许用户通过指定电子邮件地址和密码创建新的登录名。你可以用任何你喜欢的方式设置它们，例如运用 CSS：验证用户的电子邮件:每个用户使用自己独有的用户名和密码登录：一旦登录，用户就可以添加笔记：用户可以获取笔记列表：删除笔记之前，应用会申请确认信息：用户确认后，笔记被删除：设置先决条件遵照 Vely.dev 上的安装指示。这是一个使用 DNF、APT、Pacman 或者 Zypper 等标准工具包的快速流程。由于它们都是这个范例的一部分，你必须安装 Apache 作为网络服务器，安装 MariaDB 作为数据库。安装 Vely 后，如使用 Vim，打开里面的“语法高亮显示”：vv -m 获取源代码这个演示 SaaS 应用程序的源代码是 Vely 安装的一部分。为每个应用程序创建一个单独的源代码目录不失为一个好主意（而且你可以按自己喜好命名）。在这种情况下，解包源代码会帮你完成这些工作:$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz $ cd multitenant_SaaS 默认情况下，该应用程序以 multitenant_SaaS 命名，但你可以将其命名为任何内容（如果这么做，其他每个地方你都需要改一下）。创建应用程序第一步是创建一个应用程序。使用 Vely 的 vf 工具就可以轻松完成：$ sudo vf -i-u $(whoami) multitenant_SaaS 这个命令创建了一个新的应用程序主目录（/var/lib/vv/multitenant_SaaS），并帮你执行应用程序设置。通常，这意味着在该主目录中创建各种子目录并分配权限。在这种情况下，只有当前用户（whoami 的结果）拥有目录，具有 0700 权限，这确保了其他人没有访问文件的权限。创建数据库在你键入任何代码之前，你需要一个能够存储该应用程序所用信息的空间。首先，创建一个名为 db_multitenant_SaaS 的 MariaDB 数据库，由用户名为 vely 的用户所有，密码为 your_password 。你可以修改刚才提到的任何值，但得记住，在这个示例里，你需要将包含这些内容的每个地方都得修改一遍。在 MySQL 中以 root 身份登录：create database if not exists db_multitenant_SaaS; create user if not exists vely identified by 'your_password'; grant create,alter,drop,select,insert,delete,update on db_multitenant_SaaS.* to vely; 然后在数据库内创建数据库对象（表，记录等等）：use db_multitenant_SaaS; source setup.sql; exit 将 Vely 连接至数据库为了让 Vely 知晓你数据库的位置以及如何登录进去，创建一个名为 db_multitenant_SaaS 的数据库配置文件。（该名称用于在源代码中的数据库声明，所以如果你改了它，确保在它存在的每个地方都改一遍。）Vely 使用原生的 MariaDB 数据库连接，因此你可以指定给定的数据库所能允许的任何选项:$ echo '[client] user=vely password=your_password database=db_multitenant_SaaS protocol=TCP host=127.0.0.1 port=3306' > db_multitenant_SaaS 构建应用程序使用 vv 工具构建应用程序，利用 --db 选项指定 MariaDB 数据库和数据库配置文件：$ vv -q--db=mariadb:db_multitenant_SaaS 启用应用服务器启动你的网络应用程序的服务器，需要使用 vf FastCGI 进程管理器。应用程序服务器使用 Unix 套接字与网络服务器（创建反向代理）通信：$ vf -w3 multitenant_SaaS 这么做会启用三个守护进程，为接收到的请求提供服务。你也可以启动一个自适应服务器，它会增加进程的数量从而服务更多的请求，并在不需要他们时减少进程的数量：$ vf multitenant_SaaS 请参阅 vf 了解更多选项，以帮助你实现最佳性能。当你需要停止你的应用程序服务器，使用 -m quit 选项：$ vf -m quit multitenant_SaaS 创建网络服务器这是一个网络应用程序，那么应用程序就得需要一个网络服务器。该示例通过一个 Unix 套接字监听器使用 Apache。1、设置 Apache将 Apache 配置为一个反向代理，并将你的应用程序与之连接，你需要启用 FastCGI 代理支持，这通常使用 proxy 和 proxy_fcgi 模块。对于 Fedora 系统（或者其它的，比如 Arch）来说，通过在 Apache 配置文件 /etc/httpd/conf/httpd.conf 中添加（或取消注释）适当的 LoadModule 指令，就可启用 proxy 和 proxy_fcgi 模块。以下指令适用于 Debian，Ubuntu 以及类似的系统，启用 proxy 和 proxy_fcgi 模块：$ sudo a2enmod proxy $ sudo a2enmod proxy_fcgi 以下指令适用于 OpenSUSE，将这几行添加在 /etc/apache2/httpd.conf 结尾处：LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so 2、配置 Apache现在你必须将代理信息添加在 Apache 的配置文件中：ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS 你的配置文件的位置可能会有所不同，这取决于不同的 Linux 发行版：Fedora、CentOS、Mageia 和 Arch： /etc/httpd/conf/httpd.confDebian、Ubuntu、Mint： /etc/apache2/apache2.confOpenSUSE：/etc/apache2/httpd.conf3、重新启动最后，重启 Apache。在 Fedora 和类似系统，还有 Arch Linux 是如下指令：$ sudo systemctl restart httpd 在 Debian 和基于 Debian 的系统，还有 OpenSUSE 是如下指令：$ sudo systemctl restart apache2 设置本地邮箱这个示例中，电子邮件是其功能的一部分。如果你的服务器已经可以发送电子邮件了，你可以跳过这一条。此外，你可以使用本地邮箱（myuser@localhost）来测试它。要做到这一点，需安装 Sendmail。在 Fedora 和类似系统中是如下指令：$ sudo dnf installsendmail $ sudo systemctl start sendmail 而在 Debian 和类似系统（如 Ubuntu）：$ sudo apt installsendmail $ sudo systemctl start sendmail 当应用程序向本地用户发送电子邮件，比如说 OS_user@localhost，你就可以通过查看 /var/mail/ 处（即所谓“邮件池”）来确认电子邮件是否被发送。从浏览器访问应用服务器假设你在本地运行该应用，可以通过使用 http://127.0.0.1/multitenant_SaaS?req=notes&action=begin 域名从你的网络服务器访问你的应用服务器。如果你在互联网上的在线服务器运行该程序，你可能就需要调整防火墙设置以允许 HTTP 通信。源代码该应用程序示例包含 7 个源文件。你可以自行回顾代码（记住，这些文件只有 310 行代码），下面是每个文件的概述。SQL 设置（setup.sql）创建的两个表：users：每个用户的信息。在 users 表中，每个用户都有自己唯一的 ID （userId 列），以及其他信息，如电子邮件地址和该地址是否通过了验证。还有一个哈希密码。实际的密码永远不会存储在纯文本（或其他形式）中，单向哈希用于检查密码。notes：用户输入的笔记。notes 表包含了所有的笔记，每个笔记都有一个 userId 列，表示哪个用户拥有它们。userId 列的值与 users 表中的同名列匹配。这样，每个笔记显然都属于单个用户。该文件内容如下：create table if not exists notes (dateOf datetime, noteId bigint auto_increment primary key, userId bigint, note varchar(1000)); create table if not exists users (userId bigint auto_increment primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100)); create unique index if not exists users1 on users (email); 运行时数据（login.h）为了正确地显示登录、注册和注销链接，你需要一些在应用程序中任何地方都可以使用的标志。此外，应用程序使用 cookie 来维护会话，因此它需要在任何地方都可用，例如，验证会话是否有效。发送到应用程序的每个请求都以这种方式进行确认。只有带有可验证 cookie 的请求是允许的。所以要做到这种效果，你需要有一个 global_request_data 类型的 reqdata（请求数据），其中包含 sess_userId（用户的 ID）以及 sess_id（用户目前的会话 ID）。此外，还有一些不言自明的标志，可以帮助渲染页面：#ifndef _VV_LOGIN #define _VV_LOGIN typedef struct s_reqdata { bool displayed_logout; // true 则显示登出连接 bool is_logged_in; // true 则会话已验证登录 char *sess_userId; // 目前会话的用户 ID char *sess_id; // 会话 ID } reqdata; void login_or_signup (); #endif 会话检查和会话数据（\_before.vely）Vely 里有一个 请求前处理程序 before_request handler 的概念。你写的代码会在其它处理请求的代码之前执行的。要达到这个目的，你只需要将这样的代码写在名为 _before.vely 的文件中，然后剩余的部分将会自动处理。SaaS 应用程序所作的任何事情，例如处理发送至应用程序的请求，必须验证其安全性。这样，应用程序就能知晓调用方是否有执行操作所需要的权限。在这里，通过请求前处理程序进行权限检查。这样，无论其他代码如何处理请求，都已经掌握了会话信息。为保持会话数据（比如会话 ID 和用户 ID）在你代码中的任何地方都可用，你可以使用 global_request_data。它只是一个指向内存的通用指针（void*），任何处理请求的代码都可以访问它。这非常适合处理会话，如下所示:#include "vely.h" #include "login.h" // _before() 是一个请求前处理程序。 // 它总是在处理请求的其他代码之前执行。 // 对于任何类型的请求范围设置或数据初始化，它都是一个很好的位置。 void _before() { // 输出 HTTP 请求头 out-header default reqdata *rd; // 这是全局请求数据，见 login.h // 为全局请求数据分配内存， // 将在请求结束时自动释放 new-mem rd size sizeof(reqdata) // 初始化标志 rd->displayed_logout = false; rd->is_logged_in = false; // 将我们创建的数据设置为全局请求数据， // 可以从任何处理请求的代码中访问 set-req data rd // 检查会话是否存在（基于来自客户端的 cookie） // 这在任何其他请求处理代码之前执行， // 使其更容易准备好会话信息 _check_session (); } 检查会话是否有效（\_check\_session.vely）多租户 SaaS 应用程序中最重要的任务之一就是通过检查用户是否登录来（尽快）检查会话是否有效。这是通过从客户端（例如网络浏览器）获取会话 ID 和用户 ID 的 cookie，并将它们与存储会话的数据库进行比较来实现的：#include "vely.h" #include "login.h" // 检查会话是否有效 void _check_session () { // 获取全局请求数据 reqdata *rd; get-req data to rd // 自用户浏览器获取 cookies get-cookie rd->sess_userId="sess_userId" get-cookie rd->sess_id="sess_id" if (rd->sess_id[0] != 0) { // 检查给定用户 ID 下的会话 ID 是否正确 char *email; run-query @db_multitenant_SaaS = "select email from users where userId='%s' and session='%s'" output email : rd->sess_userId, rd->sess_id row-count define rcount query-result email to email end-query if (rcount == 1) { // 如果正确，设置登录标志 rd->is_logged_in = true; // 如果登出链接不显示，则显示它 if (rd->displayed_logout == false) { @Hi <<p-out email>>! Logout rd->displayed_logout = true; } } else rd->is_logged_in = false; } } 注册、登录、登出（login.vely）任何多租户系统的基础便是具有用户注册\登录和登出的功能。通常情况下，注册包括验证电子邮件地址；不止于此，同一电子邮件地址会作为一个用户名。这里就是这种情况。这里实现了几个执行该功能所必须的子请求：注册新用户时，显示 HTML 表单以收集信息。它的 URL 请求签名是 req=login&action=newuser。作为对注册表单的响应，创建一个新用户。URL 请求的签名是 req=login&action=createuser。输入参数（input-param）信号获取 email 和 pwd 的 POST 表单字段。密码值是单向散列，电子邮件验证令牌是一个随机的 5 位数字。这些被插入到 users 表中，创建一个新用户。系统会发送一封验证邮件，并提示用户阅读邮件并输入代码。通过输入发送到该电子邮件的验证码来验证电子邮件。URL 请求的签名是 req=login&action=verify。显示一个登录表单，让用户登录。URL 请求的签名是 req=login（例如，action 为空）。通过验证电子邮件地址（用户名）和密码登录。URL 请求的签名是 req=login&action=login。应用户要求登出。URL 请求的签名是 req=login&action=logout。应用程序的登录页。URL 请求的签名是 req=login&action=begin。如果用户当前已登录，转到应用程序的登录页面。可以看看下面这些例子：#include "vely.h" #include "login.h" // 处理云端多租户应用程序的会话维护、登录、注销、会话验证 void login () { // 获取 URL 的输入参数 `action` input-param action // 获取全局请求数据，我们在其中记录会话信息，所以它很方便 reqdata *rd; get-req data to rd // 如果会话已经建立，我们不会 // 继续到应用程序主页的唯一原因是我们正在登出 if (rd->is_logged_in) { if (strcmp(action, "logout")) { _show_home(); exit-request } } // 应用程序页面启动。显示登录或注册的链接， // 并显示适当的主屏幕 if (!strcmp (action, "begin")) { _show_home(); exit-request // 开始创建新用户。询问电子邮件和密码， // 然后提交此表单时创建用户。 } else if (!strcmp (action, "newuser")) { @Create New User @ @ @ @ @ @ // 验证用户发送到电子邮件的代码。代码必须匹配，从而验证电子邮件地址 } else if (!strcmp (action, "verify")) { input-param code input-param email // 获取基于电子邮件的验证令牌 run-query @db_multitenant_SaaS = "select verify_token from users where email='%s'" output db_verify : email query-result db_verify to define db_verify // 将数据库中记录的令牌与用户提供的令牌进行比较 if (!strcmp (code, db_verify)) { @Your email has been verifed. Please Login. // 如果匹配，更新用户信息以表明已验证。 run-query @db_multitenant_SaaS no-loop = "update users set verified=1 where email='%s'" : email exit-request } end-query @Could not verify the code. Please try again. exit-request // 创建用户 —— 当用户使用电子邮件和密码提交表单以创建用户时运行 } else if (!strcmp (action, "createuser")) { input-param email input-param pwd // 创建散列（单向）密码 hash-string pwd to define hashed_pwd // 生成随机的 5 位数字字符串验证代码 random-string to define verify length 5 number // 创建用户：插入电子邮件、哈希密码、验证令牌。当前验证状态为 0，或未验证 begin-transaction @db_multitenant_SaaS run-query @db_multitenant_SaaS no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue if (strcmp (err, "0") || arows != 1) { // 如果不能添加用户，则可能该用户不存在。不管怎样，我们都无法继续。 login_or_signup(); @User with this email already exists. rollback-transaction @db_multitenant_SaaS } else { // 创建带有验证码的电子邮件并将其发送给用户 write-string define msg @From: vely@vely.dev @To: <<p-out email>> @Subject: verify your account @ @Your verification code is: <<p-out verify>> end-write-string exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st if (st != 0) { @Could not send email to <<p-out email>>, code is <<p-out verify>> rollback-transaction @db_multitenant_SaaS exit-request } commit-transaction @db_multitenant_SaaS // 通知用户查看邮件并输入验证码 @Please check your email and enter verification code here: @ @ @<input name="email" type="hidden" value="<<p-out email>>"> @ @ Verify @ } // 这里在登录用户登出时运行 } else if (!strcmp (action, "logout")) { // 更新用户表以清除会话，即没有该用户登录 if (rd->is_logged_in) { run-query @db_multitenant_SaaS = "update users set session='' where userId='%s'" : rd->sess_userId no-loop affected-rows define arows if (arows == 1) { rd->is_logged_in = false; // 提示用户未登录 @You have been logged out. } } _show_home(); // 登录：当用户输入用户名和密码时运行 } else if (!strcmp (action, "login")) { input-param pwd input-param email // 创建单向散列，目的是与用户表进行比较 —— 密码**永远不会**被记录 hash-string pwd to define hashed_pwd // 为会话 ID 创建一个随机的 30 位长的字符串 random-string to rd->sess_id length 30 // 检查用户名和哈希密码是否匹配 run-query @db_multitenant_SaaS = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_userId : email, hashed_pwd query-result sess_userId to rd->sess_userId // 如果匹配，使用会话 ID 更新用户表 run-query @db_multitenant_SaaS no-loop = "update users set session='%s' where userId='%s'" : rd->sess_id, rd->sess_userId affected-rows define arows if (arows != 1) { @Could not create a session. Please try again. <<.login_or_signup();>> exit-request } // 设置“用户 ID”和“会话 ID”为 cookie。用户的浏览器将在每个请求中返回这些信息 set-cookie "sess_userId" = rd->sess_userId set-cookie "sess_id" = rd->sess_id // 显示主页，确保会话是正确的，并设置标志 _check_session(); _show_home(); exit-request end-query @Email or password are not correct. <<.login_or_signup();>> // 登录界面，要求用户输入用户名和密码 } else if (!strcmp (action, "")) { login_or_signup(); @Please Login: @ @ @ @ @ Go @ } } // 显示登录或注册链接 void login_or_signup() { @Login & & Sign Up } 通用应用程序（\_show\_home.vely）借助本教程，你可以创建你想要的任何多租户 SaaS 应用程序。上面的多租户处理模块（login.vely）调用 _show_home() 函数，它可以容纳你的任何代码。这个示例代码展示了笔记应用程序，但它可以是任何内容。_show_home() 函数可以调用你想要的任何代码，它是一个通用的多租户应用程序插件：#include "vely.h" void _show_home() { notes(); exit-request } 笔记应用程序（notes.vely）该应用程序能够添加、列举以及删除任何给定的笔记：#include "vely.h" #include "login.h" // 多租户云中的笔记应用程序 void notes () { // 获取全局请求数据 reqdata *rd; get-req data to rd // 如果会话有效，显示登录或注册 if (!rd->is_logged_in) { login_or_signup(); } // 问候用户 @ Welcome to Notes! // 如果没有登出，退出 —— 这里确保对用户身份的安全验证 if (!rd->is_logged_in) { exit-request } // 获取 URL 参数，告诉笔记要做什么 input-param subreq // 显示笔记能够做什么操作（添加或列举笔记） @Add Note List Notes // 列举该用户的所有笔记 if (!strcmp (subreq, "list")) { // **只**选取该用户的笔记 run-query @db_multitenant_SaaS = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, note, noteId query-result dateOf to define dateOf query-result note to define note query-result noteId to define noteId // 使用快速缓存正则表达式将新行更改为 match-regex "\n" in note replace-with " \n" result define with_breaks status define st cache if (st == 0) with_breaks = note; // 什么都没有发现/替换，只用原来的 // 显示笔记 @Date: <<p-out dateOf>> (delete note) @Note: <<p-out with_breaks>> @ end-query } // 要求删除笔记 else if (!strcmp (subreq, "delete_note_ask")) { input-param note_id @Are you sure you want to delete a note? Use Back button to go back, or delete note now. } // 删除笔记 else if (!strcmp (subreq, "delete_note")) { input-param note_id // 删除笔记 run-query @db_multitenant_SaaS = "delete from notes where noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows define arows no-loop error define errnote // 告知用户状态 if (arows == 1) { @Note deleted } else { @Could not delete note (<<p-out errnote>>) } } // 添加笔记 else if (!strcmp (subreq, "add_note")) { // 从 note 表单中获取 URL POST 数据 input-param note // 在该用户的 ID 下插入笔记 run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : rd->sess_userId, note affected-rows define arows no-loop error define errnote // 告知用户状态 if (arows == 1) { @Note added } else { @Could not add note (<<p-out errnote>>) } } // 显示一个 HTML 表单来收集笔记，并将其发送回这里（使用 subreq="add_note" URL 参数） else if (!strcmp (subreq, "add")) { @Add New Note @ @ @ @ Create @ } } 具有 C 性能的 SaaSVely 语言使得 C 语言在你的网络应用程序中得到充分利用这件事成为可能。多租户 SaaS 应用程序便是从中受益的一个典型用例。看一看参考代码示例，写一写代码，然后试试 Vely。（题图：DA/126624c8-1a47-481b-b149-92273e8e0f4f）via: https://opensource.com/article/22/11/build-your-own-saas-vely作者：Sergio Mijatovic 选题：lkxed 译者：Drwhooooo 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
我正在一步步解释 Git 的方方面面。在使用 Git 近 15 年后，我已经非常习惯于 Git 的特性，很容易忘记它令人困惑的地方。因此，我在 Mastodon 上进行了调查：你有觉得哪些 Git 术语很让人困惑吗？我计划写篇博客，来解读 Git 中一些奇怪的术语，如：“分离的 HEAD 状态”，“快速前移”，“索引/暂存区/已暂存”，“比 origin/main 提前 1 个提交”等等。我收到了许多有洞见的答案，我在这里试图概述其中的一部分。下面是这些术语的列表：HEAD 和 “heads”“分离的 HEAD 状态”在合并或变基时的 “ours” 和 “theirs”“你的分支已经与 'origin/main' 同步”HEAD^、HEAD~、HEAD^^、HEAD~~、HEAD^2、HEAD~2.. 和 ...“可以快速前移”“引用”、“符号引用”refspecs“tree-ish”“索引”、“暂存的”、“已缓存的”“重置”、“还原”、“恢复”“未跟踪的文件”、“追踪远程分支”、“跟踪远程分支”检出reflog合并、变基和遴选rebase –onto提交更多复杂的术语我已经尽力讲解了这些术语，但它们几乎覆盖了 Git 的每一个主要特性，这对一篇博客而言显然过于繁重，所以在某些地方可能会有一些粗糙。HEAD 和 “heads”有些人表示他们对 HEAD 和 refs/heads/main 这些术语感到困惑，因为听起来像是一些复杂的技术内部实现。以下是一个快速概述：“heads” 就是 “分支”。在 Git 内部，分支存储在一个名为 .git/refs/heads 的目录中。（从技术上讲，官方 Git 术语表 中明确表示分支是所有的提交，而 head 只是最近的提交，但这只是同一事物的两种不同思考方式）HEAD 是当前的分支，它被存储在 .git/HEAD 中。我认为，“head 是一个分支，HEAD 是当前的分支” 或许是 Git 中最奇怪的术语选择，但已经设定好了，想要更清晰的命名方案已经为时已晚，我们继续。“HEAD 是当前的分支” 有一些重要的例外情况，我们将在下面讨论。“分离的 HEAD 状态”你可能已经看到过这条信息：$ git checkout v0.1 You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. [...] （消息译文：你处于 “分离 HEAD” 的状态。你可以四处看看，进行试验性的更改并提交，你可以通过切换回一个分支来丢弃这个状态下做出的任何提交。）这条信息的实质是：在 Git 中，通常你有一个已经检出的 “当前分支”，例如 main。存放当前分支的地方被称为 HEAD。你做出的任何新提交都会被添加到你的当前分支，如果你运行 git merge other_branch，这也会影响你的当前分支。但是，HEAD 不一定必须是一个分支！它也可以是一个提交 ID。Git 会称这种状态（HEAD 是提交 ID 而不是分支）为 “分离的 HEAD 状态”例如，你可以通过检出一个标签来进入分离的 HEAD 状态，因为标签不是分支如果你没有当前分支，一系列事情就断链了：git pull 根本就无法工作（因为它的全部目的就是更新你的当前分支）除非以特殊方式使用 git push，否则它也无法工作git commit、git merge、git rebase 和 git cherry-pick 仍然可以工作，但它们会留下“孤儿”提交，这些提交没有连接到任何分支，因此找到这些提交会很困难你可以通过创建一个新的分支或切换到一个现有的分支来退出分离的 HEAD 状态在合并或变基中的 “ours” 和 “theirs”遇到合并冲突时，你可以运行 git checkout --ours file.txt 来选择 “ours” 版本中的 file.txt。但问题是，什么是 “ours”，什么是 “theirs” 呢？我总感觉此类术语混淆不清，也因此从未用过 git checkout --ours，但我还是查找相关资料试图理清。在合并的过程中，这是如何运作的：当前分支是 “ours”，你要合并进来的分支是 “theirs”，这样看来似乎很合理。$ git checkout merge-into-ours # 当前分支是 “ours” $ git merge from-theirs # 我们正要合并的分支是 “theirs” 而在变基的过程中就刚好相反 —— 当前分支是 “theirs”，我们正在变基到的目标分支是 “ours”，如下：$ git checkout theirs # 当前分支是 “theirs” $ git rebase ours # 我们正在变基到的目标分支是 “ours” 我以为之所以会如此，因为在操作过程中，git rebase main 其实是将当前分支合并到 main （它类似于 git checkout main; git merge current_branch），尽管如此我仍然觉得此类术语会造成混淆。这个精巧的小网站 对 “ours” 和 “theirs” 的术语进行了解释。人们也提到，VSCode 将 “ours”/“theirs” 称作 “当前的更改”/“收到的更改”，同样会引起混淆。“你的分支已经与 origin/main 同步”此信息貌似很直白 —— 你的 main 分支已经与源端同步！但它实际上有些误导。可能会让你以为这意味着你的 main 分支已经是最新的，其实不然。它真正的含义是 —— 如果你最后一次运行 git fetch 或 git pull 是五天前，那么你的 main 分支就是与五天前的所有更改同步。因此，如果你没有意识到这一点，它对你的安全感其实是一种误导。我认为 Git 理论上可以给出一个更有用的信息，像是“与五天前上一次获取的源端 main 是同步的”，因为最新一次获取的时间是在 reflog 中记录的，但它没有这么做。HEAD^、HEAD~、HEAD^^、HEAD~~、HEAD^2、HEAD~2我早就清楚 HEAD^ 代表前一次提交，但我很长一段时间都困惑于 HEAD~ 和 HEAD^ 之间的区别。我查询资料，得到了如下的对应关系：HEAD^ 和 HEAD~ 是同一件事情（指向前 1 个提交）HEAD^^^、HEAD~~~ 和 HEAD~3 是同一件事情（指向前 3 个提交）HEAD^3 指向提交的第三个父提交，它与 HEAD~3 是不同的这看起来有些奇怪，为什么 HEAD~ 和 HEAD^ 是同一个概念？以及，“第三个父提交”是什么？难道就是父提交的父提交的父提交？（剧透：并非如此）让我们一起深入探讨一下！大部分提交只有一个父提交。但是合并提交有多个父提交 - 因为它们合并了两个或更多的提交。在 Git 中，HEAD^ 意味着 “HEAD 提交的父提交”。但是如果 HEAD 是一个合并提交，那 HEAD^ 又代表怎么回事呢？答案是，HEAD^ 指向的是合并提交的第一个父提交，HEAD^2 是第二个父提交，HEAD^3 是第三个父提交，等等。但我猜他们也需要一个方式来表示“前三个提交”，所以 HEAD^3 是当前提交的第三个父提交（如果当前提交是一个合并提交，可能会有很多父提交），而 HEAD~3 是父提交的父提交的父提交。我想，从我们之前对合并提交 “ours”/“theirs” 的讨论来看，HEAD^ 是 “ours”，HEAD^2 是 “theirs”。.. 和 ...这是两个命令：git log main..testgit log main...test我从没用过 .. 和 ... 这两个命令，所以我得查一下 man git-range-diff。我的理解是比如这样一个情况：A - B main \ C - D test main..test 对应的是提交 C 和 Dtest..main 对应的是提交 Bmain...test 对应的是提交 B，C，和 D更有挑战的是，git diff 显然也支持 .. 和 ...，但它们在 git log 中的意思完全不同？我的理解如下：git log test..main 显示在 main 而不在 test 的更改，但是 git log test...main 则会显示 两边 的改动。git diff test..main 显示 test 变动 和 main 变动（它比较 B 和 D），而 git diff test...main 会比较 A 和 D（它只会给你显示一边的差异）。有关这个的更多讨论可以参考 这篇博客文章。“可以快速前移”在 git status 中，我们会经常遇到如下的信息：$ git status On branch main Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded. (use "git pull" to update your local branch) （消息译文：你现在处于 main 分支上。你的分支比 origin/main 分支落后了 2 个提交，可以进行快速前进。 (使用 git pull 命令可以更新你的本地分支)）但“快速前移” 到底是何意？本质上，它在告诉我们这两个分支基本如下图所示（最新的提交在右侧）：main: A - B - C origin/main: A - B - C - D - E 或者，从另一个角度理解就是：A - B - C - D - E (origin/main) | main 这里，origin/main 仅仅多出了 2 个 main 不存在的提交，因此我们可以轻松地让 main 更新至最新 —— 我们所需要做的就是添加上那 2 个提交。事实上，这几乎不可能出错 —— 不存在合并冲突。快速前进式合并是个非常棒的事情！这是合并两个分支最简单的方式。运行完 git pull 之后，你会得到如下状态：main: A - B - C - D - E origin/main: A - B - C - D - E 下面这个例子展示了一种不能快速前进的状态。A - B - C - X (main) | - - D - E (origin/main) 此时，main 分支上有一个 origin/main 分支上无的提交（X），所以无法执行快速前移。在此种情况，git status 就会如此显示：$ git status Your branch and 'origin/main' have diverged, and have 1 and 2 different commits each, respectively. （你的分支和 origin/main 分支已经产生了分歧，其中各有 1 个和 2 个不同的提交。）“引用”、“符号引用”在使用 Git 时，“引用” 一词可能会使人混淆。实际上，Git 中被称为 “引用” 的实例至少有三种：分支和标签，例如 main 和 v0.2HEAD，代表当前活跃的分支诸如 HEAD^^^ 这样的表达式，Git 会将其解析成一个提交 ID。确切说，这可能并非 “引用”，我想 Git 将其称作 “版本参数”，但我个人并未使用过这个术语。个人而言，“符号引用” 这个术语颇为奇特，因为我觉得我只使用过 HEAD（即当前分支）作为符号引用。而 HEAD 在 Git 中占据核心位置，多数 Git 核心命令的行为都基于 HEAD 的值，因此我不太确定将其泛化成一个概念的实际意义。refspecs在 .git/config 配置 Git 远程仓库时，你可能会看到这样的代码 +refs/heads/main:refs/remotes/origin/main 。[remote "origin"] url = git@github.com:jvns/pandas-cookbook fetch = +refs/heads/main:refs/remotes/origin/main 我对这段代码的含义并不十分清楚，我通常只是在使用 git clone 或 git remote add 配置远程仓库时采用默认配置，并没有动机去深究或改变。“tree-ish”在 git checkout 的手册页中，我们可以看到：git checkout [-f|--ours|--theirs|-m|--conflict= ] [<tree-ish>] [--] <pathspec>... 那么这里的 tree-ish 是什么意思呢？其实当你执行 git checkout THING . 时，THING 可以是以下的任一种：一个提交 ID（如 182cd3f）对一个提交 ID 的引用（如 main 或 HEAD^^ 或 v0.3.2）一个位于提交内的子目录（如 main:./docs）可能就这些？对我个人来说，“提交内的目录”这个功能我从未使用过，从我的视角看，tree-ish 可以解读为“提交或对提交的引用”。“索引”、“暂存”、“缓存”这些术语都指向的是同一样东西（文件 .git/index，当你执行 git add 时，你的变动会在这里被暂存）：git diff --cachedgit rm --cachedgit diff --staged文件 .git/index尽管它们都是指向同一个文件，但在实际使用中，这些术语的应用方式有所不同：很显然，--index 和 --cached 并不总是表示同一种意思。我自己从未使用 --index，所以具体细节我就不展开讨论了，但是你可以在 Junio Hamano（Git 的主管维护者）的博客文章 中找到详细解释。“索引” 会包含未跟踪的文件（我猜可能是对性能的考虑），但你通常不会把未跟踪的文件考虑在“暂存区”内。“重置”、“还原”、“恢复”许多人提到，“ 重置 reset ”、“ 还原 revert ” 和 “ 恢复 restore ” 这三个词非常相似，易使人混淆。我认为这部分的困惑来自以下原因：git reset --hard 和 git restore . 单独使用时，基本上达到的效果是一样的。然而，git reset --hard COMMIT 和 git restore --source COMMIT . 相互之间是完全不同的。相应的手册页没有给出特别有帮助的描述：git reset: “重置当前 HEAD 到指定的状态”git revert: “还原某些现有的提交”git restore: “恢复工作树文件”虽然这些简短的描述为你详细说明了哪个名词受到了影响（“当前 HEAD”，“某些提交”，“工作树文件”），但它们都预设了你已经知道在这种语境中，“重置”、“还原”和“恢复”的准确含义。以下是对它们各自功能的简要说明：重置 —— git revert COMMIT: 在你当前的分支上，创建一个新的提交，该提交是 COMMIT 的“反向”操作（如果 COMMIT 添加了 3 行，那么新的提交就会删除这 3 行）。还原 —— git reset --hard COMMIT: 强行将当前分支回退到 COMMIT 所在的状态，抹去自 COMMIT 以来的所有更改。这是一个高风险的操作。恢复 —— git restore --source=COMMIT PATH: 将 PATH 中的所有文件回退到 COMMIT 当时的状态，而不扰乱其他文件或提交历史。“未跟踪的文件”、“远程跟踪分支”、“跟踪远程分支”在 Git 中，“跟踪” 这个词以三种相关但不同的方式使用：“ 未跟踪的文件 Untracked files ”：在 git status 命令的输出中可以看到。这里，“未跟踪” 意味着这些文件不受 Git 管理，不会被计入提交。“ 远程跟踪分支 remote tracking branch ” 例如 origin/main。此处的“远程跟踪分支”是一个本地引用，旨在记住上次执行 git pull 或 git fetch 时，远程 origin 上 main 分支的状态。我们经常看到类似 “分支 foo 被设置为跟踪 origin 上的远程分支 bar ”这样的提示。即使“未跟踪的文件”和“远程跟踪分支”都用到了“跟踪”这个词，但是它们所在的上下文完全不同，所以没有太多混淆。但是，对于以下两种方式的“跟踪”使用，我觉得可能会产生些许困扰：main 是一个跟踪远程的分支origin/main 是一个远程跟踪分支然而，在 Git 中，“跟踪远程的分支” 和 “远程跟踪分支” 是不同的事物，理解它们之间的区别非常关键！下面是对这两者区别的一个简单概述：main 是一个分支。你可以在它上面做提交，进行合并等操作。在 .git/config 中，它通常被配置为 “追踪” 远程的 main 分支，这样你就可以用 git pull 和 git push 来同步和上传更改。origin/main 则并不是一个分支，而是一个“远程跟踪分支”，这并不是一种真正的分支（这有些抱歉）。你不能在此基础上做提交。只有通过运行 git pull 或 git fetch 获取远程 main 的最新状态，才能更新它。我以前没有深入思考过这种模糊的地方，但我认为很容易看出为什么它会让人感到困惑。签出签出做了两个完全无关的事情：git checkout BRANCH 用于切换分支git checkout file.txt 用于撤销对 file.txt 的未暂存修改这是众所周知的混淆点，因此 Git 实际上已经将这两个功能分离到了 git switch 和 git restore（尽管你还是可以使用 checkout，就像我一样，在不愿丢弃 15 年对 git checkout 肌肉记忆的情况下）。再者，即使用了 15 年，我仍然记不住 git checkout main file.txt 用于从 main 分支恢复 file.txt 版本的命令参数。我觉得有时你可能需要在 checkout 命令后面加上--，帮助区分哪个参数是分支名，哪个是路径，但我并未这么使用过，也不确定何时需要这样做。参考日志（reflog）有很多人把 reflog 读作 re-flog，而不是 ref-log。由于本文已经足够长，我这里不会深入讨论参考日志，但值得注意的是：在 Git 中，“参考” 是一个泛指分支、标签和 HEAD 的术语参考日志（“reflog”）则为你提供了一个参考历次记录的历史追踪它是从一些极端困境中拯救出来的利器，比如说你不小心删除了重要的分支我觉得参考日志是 Git 用户界面中最难懂的部分，我总是试图避免使用它。合并 vs 变基 vs 遴选有许多人提及他们常常对于合并和变基的区别感到迷惑，并且不理解变基中的“ 基 base ”指的是什么。我会在这里尽量简要的进行描述，但是这些一句话的解释最终可能并不那么明了，因为每个人使用合并和变基创建工作流程时的方式差别挺大，要真正理解合并和变基，你必须理解工作流程。此外，有图示会更好理解。不过这个话题可能需要一篇独立的博客文章来完整讨论，所以我不打算深入这个问题。合并会创建一个新的提交，用来融合两个分支变基则会逐个地把当前分支上的提交复制到目标分支遴选跟变基类似，但是语法完全不同（一个显著的差异是变基是从当前分支复制提交，而遴选则会把提交复制到当前分支）rebase --onto在 git rebase 中，存在一个被称为 --onto 的选项。这一直让我感到困惑，因为 git rebase main 的核心功能就是将当前分支变基到 main 运行上。那么，额外的 --onto 参数又是怎么回事呢？我进行了一番查找，--onto 显然解决了一个我几乎没有或者说从未遇到过的问题，但我还是会记录下我对它的理解。A - B - C (main) \ D - E - F - G (mybranch) | otherbranch 设想一下，出于某种原因，我只想把提交 F 和 G 变基到 main 上。我相信这应该是某些 Git 工作流中会经常遇到的场景。显然，你可以运行 git rebase --onto main otherbranch mybranch 来完成这个操作。对我来说，在这个语法中记住 3 个不同的分支名顺序似乎是不可能的（三个分支名，对我来说实在太多了），但由于我从很多人那里听说过，我想它一定有它的用途。提交有人提到他们对 Git 中的提交作为一词双义（既作为动词也作为名词）的用法感到困惑。例如：动词：“别忘了经常提交”名词：“main 分支上最新的提交”我觉得大多数人应该能很快适应这个双关的用法，但是在 SQL 数据库中的“提交”用法与 Git 是有所不同，我认为在 SQL 数据库中，“提交”只是作为一个动词（你使用 COMMIT 来结束一个事务），并不作为名词。此外，在 Git 中，你可以从以下三个不同的角度去考虑一个 Git 提交：表示当前每个文件状态的快照与父提交的差异记录所有先前提交的历史这些理解都是不错的：不同的命令在所有的这些情况下都会使用提交。例如，git show 将提交视为一个差异，git log 把提交看作是历史，git restore 则将提交理解为一个快照。然而，Git 的术语并无太多助于你理解一个给定的命令正在如何使用提交。更多令人困惑的术语以下是更多让人觉得混淆的术语。我对许多这些术语的意思并不十分清楚。我自己也不是很理解的东西：git pickaxe （也许这是 git log -S 和 git log -G，它们用于搜索以前提交的差异？）子模块（我知道的全部就是它们并不以我想要的方向工作）Git 稀疏检出中的 “cone mode” （没有任何关于这个的概念，但有人提到过）人们提及觉得混淆，但我在这篇已经 3000 字的文章中略过的东西：blob、tree“合并” 的方向“origin”、“upstream”，“downstream”push 和 pull 并不是对立面fetch 和 pull 的关系（pull = fetch + merge）git porcelain子树工作树暂存“master” 或者 “main” （听起来它在 Git 内部有特殊含义，但其实并没有）何时需要使用 origin main（如 git push origin main）vs origin/main人们提及感到困惑的 Github 术语：“ 拉取请求 pull request ” （与 Gitlab 中的 “ 合并请求 merge request ” 相比，人们似乎认为后者更清晰）“压扁并合并” 和 “变基并合并” 的作用 （在昨天我从未听说过 git merge --squash，我一直以为 “压扁并合并” 是 Github 的特殊功能）确实是 “每个 Git 术语”我惊讶地发现，几乎 Git 的每个其他核心特性都被至少一人提及为某种方式中的困惑。我对听到更多我错过的混淆的 Git 术语的例子也有兴趣。关于这个，有另一篇很棒的 2012 年的文章叫做《最困惑的 Git 术语》。它更多的讨论的是 Git 术语与 CVS 和 Subversion 术语的关联。如果我要选出我觉得最令人困惑的 3 个 Git 术语，我现在会选：head 是一个分支，HEAD 是当前分支“远程跟踪分支” 和 “跟踪远程的分支” 是不同的事物“索引”、“暂存的”、“已缓存的” 全部指的同一件事就这样了！在写这些的过程中，我学到了不少东西。我了解到了一些新的关于Git的事实，但更重要的是，现在我对于别人说Git的所有功能和特性都引起困惑有了更深的理解。许多问题我之前根本没考虑过，比如我从来没有意识到，在讨论分支时，“跟踪”这个词的用法是多么地特别。另外，尽管我已经尽力做到准确无误，但由于我涉猎到了一些我从未深入探讨过的Git的角落，所以可能还是出现了一些错误。（题图：DALL-E/A/e1e5b964-5f32-41bb-811e-8978fb8556d4）via: https://jvns.ca/blog/2023/11/01/confusing-git-terminology/作者：Julia Evans 选题：lujun9972 译者：ChatGPT 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在本系列的 上一篇文章 中，我们回顾了人工智能的历史，然后详细地讨论了矩阵。在本系列的第三篇文章中，我们将了解更多的矩阵操作，同时再介绍几个人工智能 Python 库。在进入主题之前，我们先讨论几个人工智能和机器学习中常用的重要术语。 人工神经网络 artificial neural network （通常简称为 神经网络 neural network ，NN）是机器学习和深度学习的核心。顾名思义，它是受人脑的生物神经网络启发而设计的计算模型。本文中我没有插入神经网络模型的图片，因为在互联网上很容易找到它们。我相信任何对人工智能感兴趣的人应该都见过它们，左边是输入层，中间是一个或多个隐藏层，右边是输出层。各层之间的边上的 权重 weight 会随着训练不断变化。它是机器学习和深度学习应用成功的关键。 监督学习 supervised learning 和 无监督学习 unsupervised learning 是两个重要的机器学习模型。从长远来看，任何立志于从事人工智能或机器学习领域工作的人都需要学习它们，并了解实现它们的各种技术。这里我认为有必要简单说明两种模型之间的区别了。假设有两个人分别叫 A 和 B，他们要把苹果和橘子分成两组。他们从未见过苹果或橘子。他们都通过 100 张苹果和橘子的图片来学习这两种水果的特征（这个过程称为模型的训练）。不过 A 还有照片中哪些是苹果哪些是橘子的额外信息（这个额外信息称为标签）。这里 A 就像是一个监督学习模型，B 就像是无监督学习模型。你认为在是识别苹果和橘子的任务上，谁的效果更好呢？大多数人可能会认为 A 的效果更好。但是根据机器学习的理论，情况并非总是如此。如果这 100 张照片中只有 5 张是苹果，其它都是橘子呢？那么 A 可能根本就不熟悉苹果的特征。或者如果部分标签是错误的呢？在这些情况下，B 的表现可能比 A 更好。在实际的机器学习应用中会发生这样的情况吗？是的！训练模型用的数据集可能是不充分的或者不完整的。这是两种模型都仍然在人工智能和机器学习领域蓬勃发展的众多原因之一。在后续文章中，我们将更正式地讨论它们。下面我们开始学习使用 JupyterLab，它是一个用于开发人工智能程序的强大工具。JupyterLab 入门在本系列的前几篇文章中，为了简单起见，我们一直使用 Linux 终端运行 Python 代码。现在要介绍另一个强大的人工智能工具——JupyterLab。在本系列的第一篇文章中，我们对比了几个候选项，最终决定使用 JupyterLab。它比 Jupyter Notebook 功能更强大，为我们预装了许多库和包，并且易于团队协作。还有一些其它原因，我们将在后续适时探讨它们。在本系列的第一篇文章中，我们已经学习了如何安装 JupyterLab。假设你已经按文中的步骤安装好了 JupyterLab，使用 jupyter lab 或 jupyter-lab 命令在会默认浏览器（如 Mozilla Firefox、谷歌 Chrome 等）中打开 JupyterLab。（LCTT 译注：没有安装 JupyterLab 也不要紧，你可以先 在线试用 JupyterLab）图 1 是在浏览器中打开的 JupyterLab 启动器的局部截图。JupyterLab 使用一个名为 IPython（交互式 Python）的 Python 控制台。注意，IPython 其实可以独立使用，在 Linux 终端运行 ipython 命令就可以启动它。现阶段我们使用 JupyterLab 中的 Jupyter Notebook 功能。点击图 1 中用绿框标记的按钮，打开 Jupyter Notebook。这时可能会要求你选择内核。如果你按照本系列第一篇的步骤安装 JupyterLab，那么唯一的可选项就是 Python 3（ipykernel）。请注意，你还可以在 JupyterLab 中安装其它编程语言的内核，比如 C++、R、MATLAB、Julia 等。事实上 Jupyter 的内核相当丰富，你可以访问 Jupyter 内核清单 了解更多信息。下面我们快速了解一下 Jupyter Notebook 的使用。图 2 显示的是一个在浏览器中打开的 Jupyter Notebook 窗口。从浏览器标签页的标题可以看出，Jupyter Notebook 打开的文件的扩展名是 .ipynb。在图 2 处可以看到有三个选项，它们表示 Jupyter Notebook 中可以使用的三种类型的单元。“Code”（绿色框） 表示代码单元，它是用来执行代码的。“Markdown” 单元可用于输入说明性的文本。如果你是一名计算机培训师，可以用代码单元和 Markdown 单元来创建交互式代码和解释性文本，然后分享给你的学员。“Raw”（红色框）表示原始数据单元，其中的内容不会被格式化或转换。和在终端中不同，在 Jupyter Notebook 中你可以编辑并重新运行代码，这在处理简单的拼写错误时特别方便。图 3 是在 Jupyter Notebook 中执行 Python 代码的截图。要在执行代码单元中的代码，先选中该单元格，然后点击蓝框标记的按钮。图 3 中用红框标记的是 Markdown 单元，用绿框标记的是代码单元，用黄框标记的执行代码的输出。在这个例子中，Python 代码输出的是 π 的值。前面提到，JupyterLab 默认安装了许多库和包，我们不用自己安装了。你可以使用 import 命令将这些库导入到代码中。使用 !pip freeze 命令可以列出 JupyterLab 中目前可用的所有库和包。如果有库或包没有安装，大多数情况下都可以通过 pip install <全小写的库或者包的名称> 来安装它们。例如安装 TensorFlow 的命令是 pip install tensorflow。如果后面有库的安装命令不遵循这个格式，我会进行特别说明。随着本系列的继续，我们还会看到 Jupyter Notebook 和 JupyterLab 更多强大的功能。复杂的矩阵运算通过下面的代码，我们来了解一些更复杂的矩阵运算或操作。为了节省空间，我没有展示代码的输出。import numpy as np A = np.arr ay([[1,2,3],[4,5,6],[7,8,88]]) B = np.arr ay([[1,2,3],[4,5,6],[4,5,6]]) print(A.T) print(A.T.T) print(np.trace(A)) print(np.linalg.det(A)) C = np.linalg.inv(A) print(C) print(A@C) 下面我逐行来解释这些代码：导入 NumPy 包。创建矩阵 A。创建矩阵 B。打印矩阵 A 的 转置 transpose 。通过比较矩阵 A 与 A 的转置，你用该可以大致理解转置操作到底做了什么。打印 A 的转置的转置。可以看到它和矩阵 A 是相同的。这又提示了转置操作的含义。打印矩阵 A 的 迹 trace 。迹是矩阵的对角线（也称为主对角线）元素的和。矩阵 A 的主对角线元素是 1、5 和 88，所以输出的值是 94。打印 A 的 行列式 determinant 。当执行代码的结果是 -237.00000000000009（在你的电脑中可能略有区别）。因为行列式不为 0，所以称 A 为 非奇异矩阵 non-singular matrix 。将矩阵 A 的 逆 inverse 保存到矩阵 C 中。打印矩阵 C。打印矩阵 A 和 C 的乘积。仔细观察，你会看到乘积是一个 单位矩阵 identity matrix ，也就是一个所有对角线元素都为 1，所有其它元素都为 0 的矩阵。请注意，输出中打印出的不是精确的 1 和 0。在我得到的答案中，有像 -3.81639165e-17 这样的数字。这是浮点数的科学记数法，表示 -3.81639165 × 10 -17， 即小数的 -0.0000000000000000381639165，它非常接近于零。同样输出中的其它数字也会有这种情况。我强烈建议你了解计算机是怎样表示浮点数的，这对你会有很大帮助。根据第一篇文章中的惯例，可以将代码分成基本 Python 代码和人工智能代码。在这个例子中，除了第 1 行和第 9 行之外的所有代码行都可以被看作是人工智能代码。现在将第 4 行到第 10 行的操作应用到矩阵 B 上。从第 4 行到第 6 行代码的输出没有什么特别之处。然而运行第 7 行时，矩阵 B 的行列式为 0，因此它被称为 奇异矩阵 singular matrix 。运行第 8 行代码会给产生一个错误，因为只有非奇异矩阵才存在逆矩阵。你可以尝试对本系列前一篇文章中的 8 个矩阵都应用相同的操作。通过观察输出，你会发现矩阵的行列式和求逆运算只适用于方阵。方阵就是行数和列数相等的矩阵。在上面的例子中我只是展示了对矩阵执行各种操作，并没有解释它们背后的理论。如果你不知道或忘记了矩阵的转置、逆、行列式等知识的话，你最好自己学习它们。同时你也应该了解一下不同类型的矩阵，比如单位矩阵、对角矩阵、三角矩阵、对称矩阵、斜对称矩阵。维基百科上的相关文章是不错的入门。现在让我们来学习 矩阵分解 matrix decomposition ，它是更复杂的矩阵操作。矩阵分解与整数的因子分解类似，就是把一个矩阵被写成其它矩阵的乘积。下面我通过图 4 中整数分解的例子来解释矩阵分解的必要性。代码单元开头的 %time 是 Jupyter Notebook 的 魔法命令 magic command ，它会打印代码运行所花费的时间。** 是 Python 的幂运算符。基本的代数知识告诉我们，变量 a 和 b 的值都等于 (6869 x 7873) 100。但图 4 显示计算变量 b 的速度要快得多。事实上，随着底数和指数的增大，执行时间的减少会越来越明显。在几乎所有的矩阵分解技术技术中，原始矩阵都会被写成更稀疏的矩阵的乘积。 稀疏矩阵 sparse matrix 是指有很多元素值为零的矩阵。在分解后，我们可以处理稀疏矩阵，而不是原始的具有大量非零元素的 密集矩阵 dense matrix 。在本文中将介绍三种矩阵分解技术——LUP 分解、 特征分解 eigen decomposition 和 奇异值分解 singular value decomposition （SVD）。为了执行矩阵分解，我们需要另一个强大的 Python 库，SciPy。SciPy 是基于 NumPy 库的科学计算库，它提供了线性代数、积分、微分、优化等方面的函数。首先，让我们讨论 LUP 分解。任何方阵都能进行 LUP 分解。LUP 分解有一种变体，称为 LU 分解。但并不是所有方阵都能 LU 分解。因此这里我们只讨论 LUP 分解。在 LUP 分解中，矩阵 A 被写成三个矩阵 L、U 和 P 的乘积。其中 L 是一个 下三角矩阵 lower triangular matrix ，它是主对角线以上的所有元素都为零的方阵。U 是一个 上三角矩阵 upper triangular matrix ，它是主对角线以下所有元素为零的方阵。P 是一个 排列矩阵 permutation matrix 。这是一个方阵，它的每一行和每一列中都有一个元素为 1，其它元素的值都是 0。现在看下面的 LUP 分解的代码。import numpy as np import scipy as sp A=np.array([[11,22,33],[44,55,66],[77,88,888]]) P, L, U = sp.linalg.lu(A) print(P) print(L) print(U) print(P@L@U) 图 5 显示了代码的输出。第 1 行和第 2 行导入 NumPy 和 SciPy 包。在第 3 行创建矩阵 A。请记住，我们在本节中会一直使用矩阵 A。第 4 行将矩阵 A 分解为三个矩阵——P、L 和 U。第 5 行到第 7 行打印矩阵 P、L 和 U。从图 5 中可以清楚地看出，P 是一个置换矩阵，L 是一个下三角矩阵，U 是一个上三角矩阵。最后在第 8 行将这三个矩阵相乘并打印乘积矩阵。从图 5 可以看到乘积矩阵 P@L@U 等于原始矩阵 A，满足矩阵分解的性质。此外，图 5 也验证了矩阵 L、U 和 P 比矩阵 A 更稀疏。下面我们讨论特征分解，它是将一个方阵是用它的 特征值 eigenvalue 和 特征向量 eigenvector 来表示。用 Python 计算特征值和特征向量很容易。关于特征值和特征向量的理论解释超出了本文的讨论范围，如果你不知道它们是什么，我建议你通过维基百科等先了解它们，以便对正在执行的操作有一个清晰的概念。图 6 中是特征分解的代码。在图 6 中，第 1 行计算特征值和特征向量。第 2 行和第 3 行输出它们。注意，使用 NumPy 也能获得类似的效果，Lambda, Q = np.linalg.eig(A)。这也告诉我们 NumPy 和 SciPy 的功能之间有一些重叠。第 4 行重建了原始矩阵 A。第 4 行中的代码片段 np.diag(Lambda) 是将特征值转换为对角矩阵（记为 Λ）。对角矩阵是主对角线以外的所有元素都为 0 的矩阵。第 4 行的代码片段 sp.linalg.inv(Q) 是求 Q 的逆矩阵（记为 Q -1）。最后，将三个矩阵 Q、Λ、Q -1 相乘得到原始矩阵 A。也就是在特征分解中 A=QΛQ -1。图 6 还显示了执行的代码的输出。红框标记的是特征值，用绿框标记的是特征向量，重构的矩阵 A 用蓝框标记。你可能会感到奇怪，输出中像 11.+0.j 这样的数字是什么呢？其中的 j 是虚数单位。11.+0.j 其实就是 11.0+0.0j，即整数 11 的复数形式。现在让我们来看奇异值分解（SVD），它是特征分解的推广。图 7 显示了 SVD 的代码和输出。第 1 行将矩阵 A 分解为三个矩阵 U、S 和 V。第 2 行中的代码片段 np.diag(S) 将 S 转换为对角矩阵。最后，将这三个矩阵相乘重建原始矩阵 A。奇异值分解的优点是它可以对角化非方阵。但非方阵的奇异值分解的代码稍微复杂一些，我们暂时不在这里讨论它。其它人工智能和机器学习的 Python 库当谈到人工智能时，普通人最先想到的场景可能就是电影《终结者》里机器人通过视觉识别一个人。 计算机视觉 computer vision 是人工智能和机器学习技术被应用得最广泛的领域之一。下面我将介绍两个计算机视觉相关的库：OpenCV 和 Matplotlib。OpenCV 是一个主要用于实时计算机视觉的库，它由 C 和 C++ 开发。C++ 是 OpenCV 的主要接口，它通过 OpenCV-Python 向用户提供 Python 接口。Matplotlib 是基于 Python 的绘图库。我曾在 OSFY 上的一篇早期 文章 中详细介绍了 Matplotlib 的使用。前面我一直在强调矩阵的重要性，现在我用一个实际的例子来加以说明。图 8 展示了在 Jupyter Notebook 中使用 Matplotlib 读取和显示图像的代码和输出。如果你没有安装 Matplotlib，使用 pip install matplotlib 命令安装 Matplotlib。在图 8 中，第 1 行和第 2 行从 Matplotlib 导入了一些函数。注意你可以从库中导入单个函数或包，而不用导入整个库。这两行是基本的 Python 代码。第 3 行从我的计算机中读取标题为 OSFY-Logo.jpg 的图像。我从 OSFY 门户网站的首页下载了这张图片。此图像高 80 像素，宽 270 像素。第 4 行和第 5 行在 Jupyter Notebook 窗口中显示图像。请注意图像下方用红框标记的两行代码，它的输出告诉我们变量 image 实际上是一个 NumPy 数组。具体来说，它是一个 80 x 270 x 3 的三维数组。数组尺寸中的 80 x 270 就是图片的大小，这一点很容易理解。但是第三维度表示什么呢？这是因计算机像通常用 RGB 颜色模型来存储的彩色图。它有三层，分别用于表示红绿蓝三种原色。我相信你还记得学生时代的实验，把原色混合成不同的颜色。例如，红色和绿色混合在一起会得到黄色。在 RGB 模型中，每种颜色的亮度用 0 到 255 的数字表示。0 表示最暗，255 表示最亮。因此值为 (255,255,255) 的像素表示纯白色。现在，执行代码 print(image)， Jupyter Notebook 会将整个数组的一部分部分打印出来。你可以看到数组的开头有许多 255。这是什么原因呢？如果你仔细看 OSFY 的图标会发现，图标的边缘有很多白色区域，因此一开始就印了很多 255。顺便说一句，你还可以了解一下其他颜色模型，如 CMY、CMYK、HSV 等。现在我们反过来从一个数组创建一幅图像。首先看图 9 中所示的代码。它展示了如何生成两个 3 x 3 的随机矩阵，它的元素是 0 到 255 之间的随机值。注意，虽然相同的代码执行了两次，但生成的结果是不同的。这是通过调用 NumPy 的伪随机数生成器函数 randint 实现的。实际上，我中彩票的几率都比这两个矩阵完全相等的几率大得多。接下来我们要生成一个形状为 512 x 512 x 3 的三维数组，然后将它转换为图像。为此我们将用到 OpenCV。注意，安装 OpenCV 命令是 pip install opencv-python。看下面的代码：import cv2 img = np.random.randint(0, 256, size=(512, 512, 3)) cv2.imwrite('img.jpg', img) 第 1 行导入库 OpenCV。注意导入语句是 import cv2，这与大多数其他包的导入不同。第 3 行将矩阵 img 转换为名为 img.jpg 的图像。图 10 显示了由 OpenCV 生成的图像。在系统中运行这段代码，将图像将被保存在 Jupyter Notebook 的同一目录下。如果你查看这张图片的属性，你会看到它的高度是 512 像素，宽度是 512 像素。通过这些例子，很容易看出，任何处理计算机视觉任务的人工智能和机器学习程序使用了大量的数组、向量、矩阵以及线性代数中的思想。这也是本系列用大量篇幅介绍数组、向量和矩阵的原因。最后，考虑下面显示的代码。image.jpg 输出图像会是什么样子？我给你两个提示。函数 zeros 在第 4 行和第 5 行创建了两个 512 x 512 的数组，其中绿色和蓝色填充了零。第 7 行到第 9 行用来自数组 red、green 和 blue 的值填充三维数组 img1。import numpy as np import cv2 red = np.random.randint(0, 256, size=(512, 512)) green = np.zeros([512, 512], dtype=np.uint8) blue = np.zeros([512, 512], dtype=np.uint8) img1 = np.zeros([512,512,3], dtype=np.uint8) img1[:,:,0] = blue img1[:,:,1] = green img1[:,:,2] = red cv2.imwrite(‘image.jpg’, img1) 本期的内容就到此结束了。在下一篇文章中，我们将开始简单地学习 张量 tensor ，然后安装和使用 TensorFlow。TensorFlow 是人工智能和机器学习领域的重要参与者。之后，我们将暂时放下矩阵、向量和线性代数，开始学习概率论。概率论跟线性代数一样是人工智能的重要基石。（题图：MJ/ec8e9a02-ae13-4924-b6cb-74ef96ab8af9）via: https://www.opensourceforu.com/2023/07/ai-a-few-more-useful-python-libraries/作者：Deepu Benson 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
你好！我一直在投入写作一本关于 Git 的小册，因此我对 Git 分支投入了许多思考。我不断从他人那里听说他们觉得 Git 分支的操作方式违反直觉。这使我开始思考：直觉上的分支概念可能是什么样，以及它如何与 Git 的实际操作方式区别开来？在这篇文章中，我想简洁地讨论以下几点内容：我认为许多人可能有的一个直觉性的思维模型Git 如何在内部实现分支的表示（例如，“分支是对提交的指针”）这种“直觉模型”与实际操作方式之间的紧密关联直觉模型的某些局限性，以及为何它可能引发问题本文无任何突破性内容，我会尽量保持简洁。分支的直观模型当然，人们对分支有许多不同的直觉。我自己认为最符合“苹果树的一个分支”这一物理比喻的可能是下面这个。我猜想许多人可能会这样理解 Git 分支：在下图中，两个红色的提交就代表一个“分支”。我认为在这个示意图中有两点很重要：分支上有两个提交分支有一个“父级”（main），它是这个“父级”的分支虽然这个观点看似合理，但实际上它并不符合 Git 对于分支的定义 — 最重要的是，Git 并没有一个分支的“父级”的概念。那么，Git 又是如何定义分支的呢？在 Git 里，分支是完整的历史在 Git 中，一个分支是每个过去提交的完整历史记录，而不仅仅是那个“分支”提交。因此，在我们上述的示意图中，所有的分支（main 和 branch）都包含了 4 次提交。我创建了一个示例仓库，地址为：https://github.com/jvns/branch-example。它设置的分支方式与前图一样。现在，我们来看看这两个分支：main 分支包含了 4 次提交：$ git log --oneline main 70f727a d f654888 c 3997a46 b a74606f a mybranch 分支也有 4 次提交。最后两次提交在这两个分支里都存在。$ git log --oneline mybranch 13cb960 y 9554dab x 3997a46 b a74606f a 因此，mybranch 中的提交次数为 4，而不仅仅是 2 次“分支”提交，即 13cb960 和 9554dab。你可以用以下方式让 Git 绘制出这两个分支的所有提交：$ git log --all --oneline --graph * 70f727a (HEAD -> main, origin/main) d * f654888 c | * 13cb960 (origin/mybranch, mybranch) y | * 9554dab x |/ * 3997a46 b * a74606f a 分支以提交 ID 的形式存储在 Git 的内部，分支会以一种微小的文本文件的形式存储下来，其中包含了一个提交 ID。这就是我一开始提及到的“技术上正确”的定义。这个提交就是分支上最新的提交。我们来看一下示例仓库中 main 和 mybranch 的文本文件：$ cat .git/refs/heads/main 70f727acbe9ea3e3ed3092605721d2eda8ebb3f4 $ cat .git/refs/heads/mybranch 13cb960ad86c78bfa2a85de21cd54818105692bc 这很好理解：70f727 是 main 上的最新提交，而 13cb96 是 mybranch 上的最新提交。这样做的原因是，每个提交都包含一种指向其父级的指针，所以 Git 可以通过追踪这些指针链来找到分支上所有的提交。正如我前文所述，这里遗漏的一个重要因素是这两个分支间的任何关联关系。从这里能看出，mybranch 是 main 的一个分支——这一点并没有被表明出来。既然我们已经探讨了直观理解的分支概念是如何不成立的，我接下来想讨论的是，为何它在某些重要的方面又是如何成立的。人们的直观感觉通常并非全然错误我发现，告诉人们他们对 Git 的直觉理解是“错误的”的说法颇为流行。我觉得这样的说法有些可笑——总的来说，即使人们关于某个题目的直觉在某些方面在技术上不精确，但他们通常会有完全合理的理由来支持他们的直觉！即使是“不正确的”模型也可能极其有用。现在，我们来讨论三种情况，其中直觉上的“分支”概念与我们实际在操作中如何使用 Git 非常相符。变基操作使用的是“直观”的分支概念现在，让我们回到最初的图片。当你在 main 上对 mybranch 执行 变基 rebase 操作时，它将取出“直观”分支上的提交（只有两个红色的提交）然后将它们应用到 main 上。执行结果就是，只有两次提交（x 和 y）被复制。以下是相关操作的样子：$ git switch mybranch $ git rebase main $ git log --oneline mybranch 952fa64 (HEAD -> mybranch) y 7d50681 x 70f727a (origin/main, main) d f654888 c 3997a46 b a74606f a 在此，git rebase 创建了两个新的提交（952fa64 和 7d50681），这两个提交的信息来自之前的两个 x 和 y 提交。所以直觉上的模型并不完全错误！它很精确地告诉你在变基中发生了什么。但因为 Git 不知道 mybranch 是 main 的一个分叉，你需要显式地告诉它在何处进行变基。合并操作也使用了“直观”的分支概念合并操作并不复制提交，但它们确实需要一个“ 基础 base ”提交：合并的工作原理是查看两组更改（从共享基础开始），然后将它们合并。我们撤销刚才完成的变基操作，然后看看合并基础是什么。$ git switch mybranch $ git reset --hard 13cb960 # 撤销 rebase $ git merge-base main mybranch 3997a466c50d2618f10d435d36ef12d5c6f62f57 这里我们获得了分支分离出来的“基础”提交，也就是 3997a4。这正是你可能会基于我们的直观图片想到的提交。GitHub 的拉取请求也使用了直观的概念如果我们在 GitHub 上创建一个拉取请求，打算将 mybranch 合并到 main，这个请求会展示出两次提交：也就是 x 和 y。这完全符合我们的预期，也和我们对分支的直观认识相符。我想，如果你在 GitLab 上发起一个合并请求，那显示的内容应该会与此类似。直观理解颇为精准，但它有一定局限性这使我们的对分支直观定义看起来相当准确！这个“直观”的概念和合并、变基操作以及 GitHub 拉取请求的工作方式完全吻合。当你在进行合并、变基或创建拉取请求时，你需要明确指定另一个分支（如 git rebase main），因为 Git 不知道你的分支是基于哪个分支的。然而，关于分支的直观理解有一个比较严重的问题：你直觉上认为 main 分支和某个分离的分支有很大的区别，但 Git 并不清楚这点。所以，现在我们要来讨论一下 Git 分支的不同种类。主干和派生分支对于人类来说，main 和 mybranch 有着显著的区别，你可能针对如何使用它们，有着截然不同的意图。通常，我们会将某些分支视为“ 主干 trunk ”分支，同时将其他一些分支看作是“派生”。你甚至可能有派生的派生分支。当然，Git 自身并没有这样的区分（“派生”是我刚刚构造的术语！），但是分支的种类确实会影响你如何处理它。例如：你可能会想将 mybranch 变基到 main，但你大概不会想将 main 变基到 mybranch —— 那就太奇怪了！一般来说，人们在重写“主干”分支的历史时比短期存在的派生分支更为谨慎。Git 允许你进行“反向”的变基我认为人们经常对 Git 感到困惑的一点是 —— 由于 Git 并没有分支是否是另一个分支的“派生”的概念，它不会给你任何关于何时合适将分支 X 变基到分支 Y 的指引。这一切需要你自己去判断。例如，你可以执行以下命令：$ git checkout main $ git rebase mybranch 或者$ git checkout mybranch $ git rebase main Git 将会欣然允许你进行任一操作，尽管在这个案例中 git rebase main 是极其正常的，而 git rebase mybranch 则显得格外奇怪。许多人表示他们对此感到困惑，所以我提供了一个展示两种变基类型的图片以供参考：相似地，你可以进行“反向”的合并，尽管这相较于反向变基要正常得多——将 mybranch 合并到 main 和将 main 合并到 mybranch 都有各自的益处。下面是一个展示你可以进行的两种合并方式的示意图：Git 对于分支之间缺乏层次结构感觉有些奇怪我经常听到 “main 分支没什么特别的” 的表述，而这令我感到困惑——对于我来说，我处理的大部分仓库里，main 无疑是非常特别的！那么人们为何会称其为不特别呢？我觉得，重点在于：尽管分支确实存在彼此间的关系（main 通常是非常特别的！），但 Git 并不知情这些关系。每当你执行如 git rebase 或 git merge 这样的 git 命令时，你都必须明确地告诉 Git 分支间的关系，如果你出错，结果可能会相当混乱。我不知道 Git 在此方面的设计究竟“对”还是“错”（无疑它有利有弊，而我已对无休止的争论感到厌倦），但我认为，这对于许多人来说，原因在于它有些出人意料。Git 关于分支的用户界面也同样怪异假设你只想查看某个分支上的“派生”提交，正如我们之前讨论的，这是完全正常的需求。下面是用 git log 查看我们分支上的两次派生提交的方法：$ git switch mybranch $ git log main..mybranch --oneline 13cb960 (HEAD -> mybranch, origin/mybranch) y 9554dab x 你可以用 git diff 这样查看同样两次提交的合并差异：$ git diff main...mybranch 因此，如果你想使用 git log 查看 x 和 y 这两次提交，你需要用到两个点（..），但查看同样的提交使用 git diff，你却需要用到三个点（...）。我个人从来都记不住 .. 和 ... 的具体用意，所以我通常虽然它们在原则上可能很有用，但我选择尽量避免使用它们。在 GitHub 上，默认分支具有特殊性同样值得一提的是，在 GitHub 上存在一种“特殊的分支”：每一个 GitHub 仓库都有一个“默认分支”（在 Git 术语中，就是 HEAD 所指向的地方），具有以下的特别之处：初次克隆仓库时，默认会检出这个分支它作为拉取请求的默认接收分支GitHub 建议应该保护这个默认分支，防止被强制推送，等等。很可能还有许多我未曾想到的场景。总结这些说法在回顾时看似是显而易见的，但实际上我花费了大量时间去搞清楚一个更“直观”的分支概念，这是因为我已经习惯了技术性的定义，“分支是对某次提交的引用”。同样，我也没有真正去思索过如何在每次执行 git rebase 或 git merge 命令时，让 Git 明确理解你分支之间的层次关系——对我而言，这已经成为第二天性，并没有觉得有何困扰。但当我反思这个问题时，可以明显看出，这很容易导致某些人混淆。（题图：MJ/a5a52832-fac8-4190-b3bd-fec70166aa16）via: https://jvns.ca/blog/2023/11/23/branches-intuition-reality/作者：Julia Evans 选题：lujun9972 译者：ChatGPT 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
为何选择文字用户界面（TUI）？许多人每日都在使用终端，因此， 文字用户界面 Text User Interface （TUI）逐渐显示出其价值。它能减少用户输入命令时的误差，让终端操作更高效，提高生产力。以我的个人使用情况为例：我每日会通过家用电脑远程连接到我使用 Linux 系统的实体 PC。所有的远程网络连接都通过私有 VPN 加密保护。然而，当我需要频繁重复输入命令进行连接时，这种经历实在令人烦躁。于是，我创建了下面这个 Bash 函数，从而有所改进：export REMOTE_RDP_USER="myremoteuser" function remote_machine() { /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_RDP_USER /v:$1 /p:$2 } 但后来，我发现自己还是频繁地执行下面这条命令（在一行中）：remote_pass=(/bin/cat/.mypassfile) remote_machine $remote_machine $remote_pass 这太烦了。更糟糕的是，我的密码被明文存储在我的电脑上（我虽然使用了加密驱动器，但这点依然令人不安）。因此，我决定投入一些时间，编写一个实用的脚本，从而更好地满足我的基本需求。我需要哪些信息才能连接到远程桌面？实际上，要连接到远程桌面，你只需提供少量信息。这些信息需要进行结构化处理，所以一个简单的 JSON 文件就能够满足要求：{"machines": [ { "name": "machine1.domain.com", "description": "Personal-PC" }, { "name": "machine2.domain.com", "description": "Virtual-Machine" } ], "remote_user": "MYUSER@DOMAIN", "title" : "MY COMPANY RDP connection" } 尽管在各种配置文件格式中，JSON 并非最佳选择（例如，它不支持注解），但是 Linux 提供了许多工具通过命令行方式解析 JSON 内容。其中，特别值得一提的工具就是 jq。下面我要向你展示如何利用它来提取机器列表：/usr/bin/jq --compact-output --raw-output '.machines[]| .name' \ $HOME/.config/scripts/kodegeek_rdp.json) \ "machine1.domain.com" "machine2.domain.com" jq 的文档可以在 这里 找到。另外，你也可以直接将你的 JSON 文件复制粘贴到 jq play，试用你的表达式，然后在你的脚本中使用这些表达式。既然已经准备好了连接远程计算机所需的所有信息，那现在就让我们来创建一个美观实用的 TUI 吧。Dialog 的帮助Dialog 是那些你可能希望早些认识的、被低评估的 Linux 工具之一。你可以利用它构建出一个井然有序、简介易懂，并且完美适用于你终端的用户界面。比如，我可以创建一个包含我喜欢的编程语言的简单的复选框列表，且默认选择 Python：dialog --clear --checklist "Favorite programming languages:" 10 30 7\ 1 Python on 2 Java off 3 Bash off 4 Perl off 5 Ruby off 我们通过这条命令向 dialog 下达了几个指令：清除屏幕（所有选项都以 -- 开头）创建一个带有标题的复选框（第一个位置参数）决定窗口尺寸（高度、宽度和列表高度，共 3 个参数）列表中的每条选项都由一个标签和一个值组成。惊人的是，仅仅一行代码，就带来了简洁直观和视觉友好的选择列表。关于 dialog 的详细文档，你可以在 这里 阅读。整合所有元素：使用 Dialog 和 JQ 编写 TUI我编写了一个 TUI，它使用 jq 从我的 JSON 文件中提取配置详细信息，并且使用 dialog 来组织流程。每次运行，我都会要求输入密码，并将其保存在一个临时文件中，脚本使用后便会删除这个临时文件。这个脚本非常基础，但它更安全，也使我能够专注于更重要的任务 ?那么 脚本 看起来是怎样的呢？下面是代码：#!/bin/bash # Author Jose Vicente Nunez # Do not use this script on a public computer. It is not secure... # https://invisible-island.net/dialog/ # Below some constants to make it easier to handle Dialog # return codes : ${DIALOG_OK=0} : ${DIALOG_CANCEL=1} : ${DIALOG_HELP=2} : ${DIALOG_EXTRA=3} : ${DIALOG_ITEM_HELP=4} : ${DIALOG_ESC=255} # Temporary file to store sensitive data. Use a 'trap' to remove # at the end of the script or if it gets interrupted declare tmp_file=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file=/tmp/test$$ trap "/bin/rm -f $tmp_file" QUIT EXIT INT /bin/chmod go-wrx ${tmp_file} > /dev/null 2>&1 :<<DOC Extract details like title, remote user and machines using jq from the JSON file Use a subshell for the machine list DOC declare TITLE=$(/usr/bin/jq --compact-output --raw-output '.title' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100 declare REMOTE_USER=$(/usr/bin/jq --compact-output --raw-output '.remote_user' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100 declare MACHINES=$( declare tmp_file2=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file2=/tmp/test$$ # trap "/bin/rm -f $tmp_file2" 0 1 2 5 15 EXIT INT declare -a MACHINE_INFO=$(/usr/bin/jq --compact-output --raw-output '.machines[]| join(",")' $HOME/.config/scripts/kodegeek_rdp.json > $tmp_file2) declare -i i=0 while read line; do declare machine=$(echo $line| /usr/bin/cut -d',' -f1) declare desc=$(echo $line| /usr/bin/cut -d',' -f2) declare toggle=off if [ $i -eq 0 ]; then toggle=on ((i=i+1)) fi echo $machine $desc $toggle done < $tmp_file2 /bin/cp /dev/null $tmp_file2 ) || exit 100 # Create a dialog with a radio list and let the user select the # remote machine /usr/bin/dialog \ --clear \ --title "$TITLE" \ --radiolist "Which machine do you want to use?" 20 61 2 \ $MACHINES 2> ${tmp_file} return_value=$? # Handle the return codes from the machine selection in the # previous step export remote_machine="" case $return_value in $DIALOG_OK) export remote_machine=$(/bin/cat ${tmp_file}) ;; $DIALOG_CANCEL) echo "Cancel pressed.";; $DIALOG_HELP) echo "Help pressed.";; $DIALOG_EXTRA) echo "Extra button pressed.";; $DIALOG_ITEM_HELP) echo "Item-help button pressed.";; $DIALOG_ESC) if test -s $tmp_file ; then /bin/rm -f $tmp_file else echo "ESC pressed." fi ;; esac # No machine selected? No service ... if [ -z "${remote_machine}" ]; then /usr/bin/dialog \ --clear \ --title "Error, no machine selected?" --clear "$@" \ --msgbox "No machine was selected!. Will exit now..." 15 30 exit 100 fi # Send 4 packets to the remote machine. I assume your network # administration allows ICMP packets # If there is an error show message box /bin/ping -c 4 ${remote_machine} >/dev/null 2>&1 if [ $? -ne 0 ]; then /usr/bin/dialog \ --clear \ --title "VPN issues or machine is off?" --clear "$@" \ --msgbox "Could not ping ${remote_machine}. Time to troubleshoot..." 15 50 exit 100 fi # Remote machine is visible, ask for credentials and handle user # choices (like password with a password box) /bin/rm -f ${tmp_file} /usr/bin/dialog \ --title "$TITLE" \ --clear \ --insecure \ --passwordbox "Please enter your Windows password for ${remote_machine}\n" 16 51 2> $tmp_file return_value=$? case $return_value in $DIALOG_OK) # We have all the information, try to connect using RDP protocol /usr/bin/mkdir -p -v $HOME/logs /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_USER /v:${remote_machine} /p:$(/bin/cat ${tmp_file})| \ /usr/bin/tee $HOME/logs/$(/usr/bin/basename $0)-$remote_machine.log ;; $DIALOG_CANCEL) echo "Cancel pressed.";; $DIALOG_HELP) echo "Help pressed.";; $DIALOG_EXTRA) echo "Extra button pressed.";; $DIALOG_ITEM_HELP) echo "Item-help button pressed.";; $DIALOG_ESC) if test -s $tmp_file ; then /bin/rm -f $tmp_file else echo "ESC pressed." fi ;; esac 你从代码中可以看出，dialog 预期的是位置参数，并且允许你在变量中捕获用户的回应。这实际上使其成为编写文本用户界面的 Bash 扩展。上述的小例子只涵盖了一些部件的使用，其实还有更多的文档在 官方 dialog 网站上。Dialog 和 JQ 是最好的选择吗？实现这个功能可以有很多方法（如 Textual，Gnome 的 Zenity，Python 的 TKinker等）。我只是想向你展示一种高效的方式——仅用 100 行代码就完成了这项任务。确实，它并不完美。更具体地讲，它与 Bash 的深度集成使得代码有些冗长，但仍然保持了易于调试和维护的特性。相比于反复复制粘贴长长的命令，这无疑是一个更好的选择。最后，如果你喜欢在 Bash 中使用 jq 处理 JSON，那么你会对这个 jq 配方的精彩集合 感兴趣的。（题图：MJ/a9b7f60a-02ec-4d3f-88ae-2321f49ac0e1）via: https://fedoramagazine.org/writing-useful-terminal-tui-on-linux-with-dialog-and-jq/作者：Jose Nunez 选题：lujun9972 译者：ChatGPT 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这 12 项基本原则能够帮助团队快速高效地构建高度可扩展的应用程序12-Factor 应用方法论 为在短时间内构建应用程序并使其具有可扩展性提供了指导。它由 Heroku 的开发人员创建，用于软件即服务（SaaS）应用程序、网络应用程序以及可能的通信平台即服务（CPaaS）。在有效组织项目和管理可扩展应用程序方面，12 要素应用程序方法论对开源开发具有强大的优势。12-Factor 应用方法论的原则12-Factor 应用方法论的规则非常严格，也是开发和部署 SaaS 应用程序的基石，并且不受任何编程语言或数据库的限制。1：一份基准代码，多份部署每个应用程序都应该有一个具有多个不同环境/部署的代码库。开发人员不应仅仅为了在不同环境中设置而开发另一个代码库。不同的环境代表不同的状态，但这些不同的环境应该共享同一个代码库。在许多开源项目都存储在 GitLab 这样的版本控制系统中的情况下，一个环境可以被视为一个分支。例如，你可以在任何中央版本控制系统中为名为 VoIP-app 的云 VoIP 应用程序创建一个单独的存储库，然后创建两个分支：开发分支（development）和暂存分支（staging），并将主分支（master）作为发布分支。2：明确声明和隔离依赖关系应声明所有依赖关系。你的应用程序可能会依赖外部系统工具或库，但不应对系统工具或库有任何 隐含的 依赖。你的应用程序必须始终明确声明所有依赖关系及其正确版本。在代码库中包含依赖关系可能会产生问题，特别是在开源项目中，外部库的更改可能会将错误引入代码库。例如，代码库可能会使用一个外部库，但没有明确声明该依赖关系或版本。如果外部库更新到更新的、未经测试的版本，这可能会与你的代码产生兼容性问题。如果明确声明了依赖关系及其正确版本，你的代码库就不会出现这种问题。根据技术栈的不同，最好使用软件包管理器，通过读取代表依赖库名称和版本的依赖库声明清单，在各自的系统上下载依赖库。3：在环境中存储配置当需要支持多个环境或客户端时，配置就成了应用程序的重要组成部分。不同部署之间的配置应存储在环境变量中。这样就可以在部署之间轻松更改配置，而无需更改代码。对于闭源应用程序来说，这一原则是有益的，因为你不会希望数据库连接信息或其他秘密数据等敏感信息被公开。然而，在开放源代码开发中，这些细节都是公开的。在这种情况下，好处是你不需要反复修改代码。你只需这样设置变量，只需改变环境，就能让代码完美运行。4：把后端服务当作附加资源所有后备服务（如数据库、外部存储或消息队列）都被视为附加资源，由执行环境附加或分离。根据这一原则，如果这些服务的位置或连接细节发生变化，仍无需更改代码。这些细节可以在配置中找到。备份服务可以从部署中快速附加或分离。例如，如果基于云的电子表格的数据库无法正常工作，开发人员应该能够创建一个从最近备份恢复的新数据库服务器，而无需对代码库进行任何更改。5：严格分离构建和运行12-Factor 应用方法论要求严格区分构建、发布和运行阶段。第一阶段是 构建 阶段。在这一阶段，源代码被组装或编译成可执行文件，同时加载依赖关系并创建资产。每次需要部署新代码时，构建阶段就会开始。第二阶段是 发布 阶段。在此阶段，构建阶段生成的代码与部署的当前配置相结合。最终发布的版本包含构建和配置，可在执行环境中立即执行。第三个阶段是 运行 阶段，也是最后阶段：应用程序在执行环境中运行。该阶段不应被其他任何阶段打断。通过严格区分这些阶段，我们可以避免代码中断，使系统维护更加易于管理。6：以一个或多个无状态进程运行应用应用程序作为一个或多个进程的集合在执行环境中执行。这些进程是无状态的，其持久化数据存储在数据库等后台服务中。这对开源非常有用，因为使用某版本应用程序的开发人员可以在其云平台上创建多节点部署，以实现可扩展性。数据不会在其中持久化，因为如果其中任何一个节点崩溃，数据就会丢失。7：通过端口绑定提供服务你的应用程序应作为独立的服务，独立于其他应用程序。它它应该能通过URL供其他服务访问，以服务形式存在。这样，你的应用程序就可以在需要时作为其他应用程序的资源。利用这一概念，你可以构建 REST API。8：通过进程模型进行扩展该原则也称为并发原则，它表明应用程序中的每个进程都应能够自我扩展、重启或克隆。开发人员可以创建多个进程，并将应用程序的负载分配给这些进程，而不是将一个进程变大。通过这种方法，你可以将每种工作负载分配给一个进程类型，从而构建能处理不同工作负载的应用程序。9：快速启动和优雅终止以增强健壮性你的应用应当基于简单的进程构建，因此开发者可以放大进程的同时还能在发生问题时重启它们。这使得应用的进程易于丢弃。根据这一原则构建应用程序意味着代码的快速部署、快速弹性扩展、更灵活的发布流程以及稳健的生产部署。所有这些在开源开发环境中都非常有用。10：尽可能的保持开发、预发布、生产环境相同同一项目的团队应使用相同的操作系统、支持服务和依赖关系。这样可以降低出现错误的可能性，减少开发所需的时间。由于开源项目的开发人员分散在各地，他们可能无法就所使用的系统、服务和依赖关系进行 沟通 ，因此将这一原则付诸实践对于开源项目来说可能是一个挑战。减少这些差异的一种可能性是制定开发指南，建议使用何种操作系统、服务和依赖关系。11：把日志当作事件流日志对于排除生产问题或了解用户行为至关重要。但是，12-Factor 应用方法论并不适合处理日志的管理。相反，应将日志条目作为事件流，写入标准输出，并将其发送到单独的服务进行分析和存档。机器人流程自动化（RPA）技术可作为处理和分析日志的第三方服务。执行环境将决定如何处理该数据流。这为反省应用程序的行为提供了更大的灵活性和能力。12：后台管理任务当作一次性进程运行这一原则实际上与开发无关，而是与应用程序管理有关。管理进程应在与应用程序常规长期运行进程相同的环境中运行。在本地部署中，开发人员可以直接使用应用程序签出目录内的 Shell 命令来执行一次性管理进程。结论使用 12-Factor 应用方法论开发应用程序，可以提高效率，加快发布速度。在开源开发中，偏离某些指导原则可能是有意义的，但最好还是尽可能严格遵守这些指导原则。开源的 12-Factor 应用是可能的。一个很好的例子是 Jitsi, （一个开源视频会议平台）, 在疫情期间扩展了 100 倍的规模，取得了巨大成功，它就是采用 12-Factor 应用方法论构建的。（题图：MJ/4bc8b463-49d0-4702-8ad3-6a07a718d5d9）via: https://opensource.com/article/21/11/open-source-12-factor-app-methodology作者：Richard Conn 选题：lujun9972 译者：trisbestever 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出