标签归档:parallel

Numba-使用LLVM的NumPy感知动态Python编译器

Numba

Python中数值函数的实时编译器

Numba是一个开源的、支持NumPy的Python优化编译器,由Anaconda,Inc.赞助。它使用LLVM编译器项目从Python语法生成机器码

Numba可以编译大量以数字为中心的Python,包括许多NumPy函数。此外,Numba还支持循环的自动并行化、GPU加速代码的生成以及ufuncs和C回调的创建

有关Numba的更多信息,请参阅Numba主页:https://numba.pydata.org

支持的平台

  • 操作系统和CPU:
    • Linux:x86(32位)、x86_64、ppc64le(POWER8和9)、ARMv7(32位)、ARMv8(64位)
    • Windows:x86、x86_64
    • MacOS:x86_64(M1/ARM64,仅非官方支持)
    • *BSD:(仅限非官方支持)
  • (可选)加速器和GPU:
    • Linux、Windows、MacOS(<10.14)上通过CUDA驱动程序实现的NVIDIA GPU(开普勒架构或更高版本)

依赖项

  • Python版本:3.7-3.9
  • 伊利莫利石0.37。*
  • NumPy>=1.17(可以使用1.11构建以实现ABI兼容性)

(可选):

  • SciPy>=1.0.0(适用于numpy.linalg支持)

正在安装

安装Numba并获取更新的最简单方法是使用Anaconda发行版:https://www.anaconda.com/download

$ conda install numba

有关更多选项,请参阅安装指南:https://numba.readthedocs.io/en/stable/user/installing.html

文档

https://numba.readthedocs.io/en/stable/index.html

邮件列表

Numba有一个供讨论的话语论坛:

一些旧的邮件列表档案位于:

持续集成

LightGBM-基于决策树算法的快速、分布式、高性能梯度提升框架,用于排序、分类和许多其他机器学习任务

LightGBM是一个使用基于树的学习算法的梯度提升框架。它设计为分布式且高效,具有以下优势:

  • 更快的培训速度和更高的效率
  • 降低内存使用率
  • 更高的精确度
  • 支持并行、分布式和GPU学习
  • 能够处理大规模数据

有关更多详情,请参阅Features

得益于这些优势,LightGBM在许多领域得到了广泛的应用winning solutions机器学习竞赛的

Comparison experiments公开数据集上的数据显示,LightGBM在效率和准确性上都优于现有的Boosting框架,并且内存消耗明显较低。更重要的是,distributed learning experiments展示了LightGBM可以通过在特定设置中使用多台机器进行训练来实现线性加速

入门和文档

我们的主要文档在https://lightgbm.readthedocs.io/并且是从该存储库生成的。如果您是LightGBM的新手,请关注the installation instructions在那个网站上

接下来,您可能想要阅读:

投稿人文档:

新闻

请参阅更改日志,地址为GitHub releases页面

一些旧的更新日志位于Key Events页面

外部(非官方)存储库

FLAML(用于超参数优化的AutoML库):https://github.com/microsoft/FLAML

Optuna(超参数优化框架):https://github.com/optuna/optuna

朱莉娅-套餐:https://github.com/IQVIA-ML/LightGBM.jl

JPMML(Java PMML转换器):https://github.com/jpmml/jpmml-lightgbm

Treite(用于高效部署的模型编译器):https://github.com/dmlc/treelite

lLeaf(基于LLVM的模型编译器,用于高效推理):https://github.com/siboehm/lleaves

Hummingbird(将模型编译器转换为张量计算):https://github.com/microsoft/hummingbird

CuML林推理库(GPU加速推理):https://github.com/rapidsai/cuml

daal4py(英特尔CPU加速推理):https://github.com/IntelPython/daal4py

m2cgen(适用于各种语言的模型应用程序):https://github.com/BayesWitnesses/m2cgen

树叶(GO模型施加器):https://github.com/dmitryikh/leaves

ONNXMLTools(ONNX转换器):https://github.com/onnx/onnxmltools

Shap(模型输出解释器):https://github.com/slundberg/shap

Shapash(模型可视化和解释):https://github.com/MAIF/shapash

dtreeviz(决策树可视化和模型解释):https://github.com/parrt/dtreeviz

MMLSpark(电光上的LightGBM):https://github.com/Azure/mmlspark

Kubeflow光顺(Kubernetes上的LightGBM):https://github.com/kubeflow/fairing

Kubeflow运算符(Kubernetes上的LightGBM):https://github.com/kubeflow/xgboost-operator

ML.NET(.NET/C#-Package):https://github.com/dotnet/machinelearning

LightGBM.NET(.NET/C#-Package):https://github.com/rca22/LightGBM.Net

红宝石:https://github.com/ankane/lightgbm

LightGBM4j(Java高级绑定):https://github.com/metarank/lightgbm4j

lightgbm-rs(铁锈装订):https://github.com/vaaaaanquish/lightgbm-rs

MLflow(实验跟踪、模型监控框架):https://github.com/mlflow/mlflow

{treesnip}(r{parsnip}-兼容接口):https://github.com/curso-r/treesnip

{mlr3learners.lightgbm}(r{mlr3}-兼容接口):https://github.com/mlr3learners/mlr3learners.lightgbm

支持

如何做出贡献

检查CONTRIBUTING页面

Microsoft开放源代码行为准则

本项目采用了Microsoft Open Source Code of Conduct有关更多信息,请参阅Code of Conduct FAQ或联系方式opencode@microsoft.com如有任何其他问题或评论

参考文献

柯国林,齐蒙,托马斯·芬利,王泰峰,魏晨,马卫东,叶启伟,刘铁岩。“LightGBM: A Highly Efficient Gradient Boosting Decision Tree“神经信息处理系统的进展”(NIPS 2017),第3149-3157页。

齐蒙,柯国林,王泰峰,魏晨,叶启伟,马志明,刘铁岩。“A Communication-Efficient Parallel Algorithm for Decision Tree“神经信息处理系统的进展”29(NIPS 2016),第1279-1287页

张欢,四思,谢楚瑞。“GPU Acceleration for Large-scale Tree Boosting“.SysML大会,2018年

注意事项:如果您在GitHub项目中使用LightGBM,请添加lightgbmrequirements.txt

许可证

这个项目是根据麻省理工学院的许可证条款授权的。看见LICENSE有关更多详细信息,请参阅

Ray 一个开放源码框架,为构建分布式应用程序提供简单、通用的API

Ray为构建分布式应用程序提供了简单、通用的API,为构建分布式应用程序提供简单、通用的API。Ray与RLlib(一个可伸缩的强化学习库)和Tune(一个可伸缩的超参数调整库)可以打包在一起。

Ray附带以下库,用于加速机器学习工作负载:

  • Tune:可伸缩的超参数调整
  • RLlib:可扩展强化学习
  • RaySGD:分布式培训包装器
  • Ray Serve:可扩展、可编程的服务

也有很多community integrations和Ray在一起,包括DaskMARSModinHorovodHugging FaceScikit-learn,以及其他。请查看full list of Ray distributed libraries here

使用以下选项安装Ray:pip install ray有关夜间车轮的信息,请参阅Installation page

快速入门

并行执行Python函数

import ray
ray.init()

@ray.remote
def f(x):
    return x * x

futures = [f.remote(i) for i in range(4)]
print(ray.get(futures))

要使用Ray的演员模型,请执行以下操作:

import ray
ray.init()

@ray.remote
class Counter(object):
    def __init__(self):
        self.n = 0

    def increment(self):
        self.n += 1

    def read(self):
        return self.n

counters = [Counter.remote() for i in range(4)]
[c.increment.remote() for c in counters]
futures = [c.read.remote() for c in counters]
print(ray.get(futures))

Ray程序可以在一台计算机上运行,也可以无缝扩展到大型群集。要在云中执行上述Ray脚本,只需下载this configuration file,然后运行:

ray submit [CLUSTER.YAML] example.py --start

阅读有关以下内容的更多信息launching clusters

调整快速入门

Tune是一个用于任何规模的超参数调优的库

要运行此示例,您需要安装以下软件:

$ pip install "ray[tune]"

此示例运行并行格网搜索以优化示例目标函数

from ray import tune


def objective(step, alpha, beta):
    return (0.1 + alpha * step / 100)**(-1) + beta * 0.1


def training_function(config):
    # Hyperparameters
    alpha, beta = config["alpha"], config["beta"]
    for step in range(10):
        # Iterative training function - can be any arbitrary training procedure.
        intermediate_score = objective(step, alpha, beta)
        # Feed the score back back to Tune.
        tune.report(mean_loss=intermediate_score)


analysis = tune.run(
    training_function,
    config={
        "alpha": tune.grid_search([0.001, 0.01, 0.1]),
        "beta": tune.choice([1, 2, 3])
    })

print("Best config: ", analysis.get_best_config(metric="mean_loss", mode="min"))

# Get a dataframe for analyzing trial results.
df = analysis.results_df

如果安装了TensorBoard,则自动可视化所有试验结果:

tensorboard --logdir ~/ray_results

RLlib快速入门

RLlib是构建在Ray之上的用于强化学习的开源库,它为各种应用程序提供了高可伸缩性和统一的API

pip install tensorflow  # or tensorflow-gpu
pip install "ray[rllib]"
import gym
from gym.spaces import Discrete, Box
from ray import tune

class SimpleCorridor(gym.Env):
    def __init__(self, config):
        self.end_pos = config["corridor_length"]
        self.cur_pos = 0
        self.action_space = Discrete(2)
        self.observation_space = Box(0.0, self.end_pos, shape=(1, ))

    def reset(self):
        self.cur_pos = 0
        return [self.cur_pos]

    def step(self, action):
        if action == 0 and self.cur_pos > 0:
            self.cur_pos -= 1
        elif action == 1:
            self.cur_pos += 1
        done = self.cur_pos >= self.end_pos
        return [self.cur_pos], 1 if done else 0, done, {}

tune.run(
    "PPO",
    config={
        "env": SimpleCorridor,
        "num_workers": 4,
        "env_config": {"corridor_length": 5}})

Ray Serve快速入门

Ray Serve是一个构建在Ray之上的可伸缩的模型服务库。它是:

  • 框架不可知性:使用相同的工具包提供各种服务,从使用PyTorch或TensorFlow&Kera等框架构建的深度学习模型到Scikit-Learning模型或任意业务逻辑
  • Python优先:在纯Python中配置声明性服务的模型,不需要YAML或JSON配置
  • 以性能为导向:启用批处理、流水线和GPU加速以提高模型的吞吐量
  • 原生合成:允许您通过将多个模型组合在一起来驱动单个预测来创建“模型管道”
  • 水平可扩展:随着您添加更多的机器,Serve可以线性扩展。使您的ML支持的服务能够处理不断增长的流量

要运行此示例,您需要安装以下软件:

$ pip install scikit-learn
$ pip install "ray[serve]"

此示例Run服务于一个SCRICKIT-LEARN梯度增强分类器

from ray import serve
import pickle
import requests
from sklearn.datasets import load_iris
from sklearn.ensemble import GradientBoostingClassifier

# Train model
iris_dataset = load_iris()
model = GradientBoostingClassifier()
model.fit(iris_dataset["data"], iris_dataset["target"])

# Define Ray Serve model,
class BoostingModel:
    def __init__(self):
        self.model = model
        self.label_list = iris_dataset["target_names"].tolist()

    def __call__(self, flask_request):
        payload = flask_request.json["vector"]
        print("Worker: received flask request with data", payload)

        prediction = self.model.predict([payload])[0]
        human_name = self.label_list[prediction]
        return {"result": human_name}


# Deploy model
client = serve.start()
client.create_backend("iris:v1", BoostingModel)
client.create_endpoint("iris_classifier", backend="iris:v1", route="/iris")

# Query it!
sample_request_input = {"vector": [1.2, 1.0, 1.1, 0.9]}
response = requests.get("http://localhost:8000/iris", json=sample_request_input)
print(response.text)
# Result:
# {
#  "result": "versicolor"
# }

更多信息

较旧的文档:

参与其中

Tqdm-一个用于Python和CLI的快速、可扩展的进度条

TQDM

tqdm派生自阿拉伯语单词塔卡杜姆(تقدّم)可以是“进步”的意思,在西班牙语中是“我非常爱你”的缩写(德马西亚多)

立即使您的循环显示一个智能进度表-只需用tqdm(iterable),你就完了!

from tqdm import tqdm
for i in tqdm(range(10000)):
    ...

76%|████████████████████████        | 7568/10000 [00:33<00:10, 229.00it/s]

trange(N)还可以用作以下操作的便捷快捷方式tqdm(range(N))

它还可以作为带有管道的模块执行:

$ seq 9999999 | tqdm --bytes | wc -l
75.2MB [00:00, 217MB/s]
9999999

$ tar -zcf - docs/ | tqdm --bytes --total `du -sb docs/ | cut -f1` \
    > backup.tgz
 32%|██████████▍                      | 8.89G/27.9G [00:42<01:31, 223MB/s]

开销很低–每次迭代约60 ns(80 ns,tqdm.gui),并针对性能回归进行单元测试。相比之下,久负盛名的ProgressBar具有800 ns/ITER开销

除了开销低之外,tqdm使用智能算法预测剩余时间并跳过不必要的迭代显示,这在大多数情况下可以忽略不计的开销

tqdm可在任何平台(Linux、Windows、Mac、FreeBSD、NetBSD、Solaris/SunOS)、任何控制台或GUI中运行,并且与IPython/Jupyter笔记本电脑也很友好

tqdm不需要任何依赖项(甚至不需要curses!),只有Python和一个支持carriage return \rline feed \n控制字符


Installation

Latest PyPI stable release


pip install tqdm

Latest development release on GitHub

拉入并安装预发行版devel分支机构:

pip install "git+https://github.com/tqdm/tqdm.git@devel#egg=tqdm"

Latest Conda release

conda install -c conda-forge tqdm

Latest Snapcraft release

有3个频道可供选择:

snap install tqdm  # implies --stable, i.e. latest tagged release
snap install tqdm  --candidate  # master branch
snap install tqdm  --edge  # devel branch

请注意,snap二进制文件仅供CLI使用(而不是import-可用),并自动设置bash制表符完成

Latest Docker release

docker pull tqdm/tqdm
docker run -i --rm tqdm/tqdm --help

Other

还有其他(非官方的)地方tqdm可以下载,特别是对于CLI使用:


Changelog

所有更改的列表可在GitHub的版本上获得:,在wiki,或在website

Usage

tqdm是非常多才多艺的,可以有多种用途。下面给出了三个主要的原因

Iterable-based

包装tqdm()围绕任何可迭代:

from tqdm import tqdm
from time import sleep

text = ""
for char in tqdm(["a", "b", "c", "d"]):
    sleep(0.25)
    text = text + char

trange(i)是一个特殊的优化实例tqdm(range(i))

from tqdm import trange

for i in trange(100):
    sleep(0.01)

循环外的实例化允许手动控制tqdm()

pbar = tqdm(["a", "b", "c", "d"])
for char in pbar:
    sleep(0.25)
    pbar.set_description("Processing %s" % char)

Manual

手动控制tqdm()使用with声明:

with tqdm(total=100) as pbar:
    for i in range(10):
        sleep(0.1)
        pbar.update(10)

如果可选变量total(或使用可迭代的len()),则显示预测统计信息

with也是可选的(您可以直接将tqdm()赋给一个变量,但在这种情况下,不要忘记delclose()在结尾处:

pbar = tqdm(total=100)
for i in range(10):
    sleep(0.1)
    pbar.update(10)
pbar.close()

Module

也许最奇妙的用法就是tqdm是在脚本中还是在命令行上。简单地插入tqdm(或python -m tqdm)之间的管道将通过所有stdinstdout将进度打印到时stderr

下面的示例演示了如何计算当前目录中所有Python文件中的行数,其中包括计时信息

$ time find . -name '*.py' -type f -exec cat \{} \; | wc -l
857365

real    0m3.458s
user    0m0.274s
sys     0m3.325s

$ time find . -name '*.py' -type f -exec cat \{} \; | tqdm | wc -l
857366it [00:03, 246471.31it/s]
857365

real    0m3.585s
user    0m0.862s
sys     0m3.358s

请注意,通常的论点是tqdm也可以指定

$ find . -name '*.py' -type f -exec cat \{} \; |
    tqdm --unit loc --unit_scale --total 857366 >> /dev/null
100%|█████████████████████████████████| 857K/857K [00:04<00:00, 246Kloc/s]

备份一个大目录吗?

$ tar -zcf - docs/ | tqdm --bytes --total `du -sb docs/ | cut -f1` \
  > backup.tgz
 44%|██████████████▊                   | 153M/352M [00:14<00:18, 11.0MB/s]

这还可以进一步美化:

$ BYTES="$(du -sb docs/ | cut -f1)"
$ tar -cf - docs/ \
  | tqdm --bytes --total "$BYTES" --desc Processing | gzip \
  | tqdm --bytes --total "$BYTES" --desc Compressed --position 1 \
  > ~/backup.tgz
Processing: 100%|██████████████████████| 352M/352M [00:14<00:00, 30.2MB/s]
Compressed:  42%|█████████▎            | 148M/352M [00:14<00:19, 10.9MB/s]

或使用7-zip在文件级完成:

$ 7z a -bd -r backup.7z docs/ | grep Compressing \
  | tqdm --total $(find docs/ -type f | wc -l) --unit files \
  | grep -v Compressing
100%|██████████████████████████▉| 15327/15327 [01:00<00:00, 712.96files/s]

已经输出基本进度信息的现有CLI程序将受益于tqdm%s--update--update_to标志:

$ seq 3 0.1 5 | tqdm --total 5 --update_to --null
100%|████████████████████████████████████| 5.0/5 [00:00<00:00, 9673.21it/s]
$ seq 10 | tqdm --update --null  # 1 + 2 + ... + 10 = 55 iterations
55it [00:00, 90006.52it/s]

FAQ and Known Issues

最常见的问题与多行输出过多有关,而不是整齐的单行进度条

  • 一般控制台:需要回车支持(CR\r)
  • 嵌套进度条:
    • 一般控制台:需要支持将光标上移到上一行。例如,IDLEConEmuPyCharm(亦请参阅herehere,以及here)缺乏全力支持
    • Windows:另外可能需要Python模块colorama要确保嵌套条位于各自的行内,请执行以下操作
  • Unicode:
    • 报告支持Unicode的环境将具有稳定、平滑的进度条。后备方案是一个ascii-仅限栏
    • Windows控制台通常只部分支持Unicode,因此often require explicit ascii=True(亦请参阅here)。这是因为普通宽度的Unicode字符错误地显示为“宽”,或者某些Unicode字符未呈现
  • 包裹生成器:
    • 生成器包装函数倾向于隐藏可迭代的长度tqdm不会
    • 替换tqdm(enumerate(...))使用enumerate(tqdm(...))tqdm(enumerate(x), total=len(x), ...)同样的道理也适用于numpy.ndenumerate
    • 替换tqdm(zip(a, b))使用zip(tqdm(a), b)甚至是zip(tqdm(a), tqdm(b))
    • 同样的道理也适用于itertools
    • 一些有用的方便函数可以在下面的tqdm.contrib
  • Hanging pipes in python2:使用时tqdm在CLI上,您可能需要使用Python 3.5+才能正确缓冲
  • No intermediate output in docker-compose:使用docker-compose run而不是docker-compose uptty: true

如果您遇到任何其他困难,请浏览并归档

Documentation

(自2016年5月19日起)

class tqdm():
  """
  Decorate an iterable object, returning an iterator which acts exactly
  like the original iterable, but prints a dynamically updating
  progressbar every time a value is requested.
  """

  def __init__(self, iterable=None, desc=None, total=None, leave=True,
               file=None, ncols=None, mininterval=0.1,
               maxinterval=10.0, miniters=None, ascii=None, disable=False,
               unit='it', unit_scale=False, dynamic_ncols=False,
               smoothing=0.3, bar_format=None, initial=0, position=None,
               postfix=None, unit_divisor=1000):

Parameters

  • 可迭代的:可迭代,可选
    可重复使用进度条进行装饰。保留为空可手动管理更新
  • 说明:字符串,可选
    进度栏的前缀
  • 总计:整型或浮点型,可选
    预期迭代次数。如果未指定,则在可能的情况下使用len(可迭代)。如果为FLOAT(“inf”)或作为最后手段,则仅显示基本进度统计信息(无ETA,无进度条)。如果gui为True并且此参数需要后续更新,请指定初始任意大正数,例如9e9
  • 离开吧:布尔值,可选
    如果为[default:true],则在迭代终止时保留进度条的所有痕迹。如果None,只有在以下情况下才会离开position0
  • 文件:io.TextIOWrapperio.StringIO,可选
    指定输出进度消息的位置(默认值:sys.stderr)。用途file.write(str)file.flush()方法。有关编码信息,请参见write_bytes
  • 乙二醇:整型,可选
    整个输出消息的宽度。如果指定,则动态调整进度条的大小以保持在此范围内。如果未指定,则尝试使用环境宽度。后备宽度为10米,对计数器和统计数据没有限制。如果为0,则不打印任何仪表(仅打印统计数据)
  • 最小间隔:浮动,可选
    最小进度显示更新间隔[默认值:0.1]秒
  • 最大间隔:浮动,可选
    最大进度显示更新间隔[默认值:10]秒。自动调整miniters与…相对应mininterval在长时间显示更新延迟之后。仅在以下情况下才有效dynamic_miniters或者启用了监视器线程
  • 小型矿车:整型或浮点型,可选
    最小进度显示更新间隔,以迭代为单位。如果为0且dynamic_miniters,将自动调整为等于mininterval(CPU效率更高,适用于紧密循环)。如果>0,将跳过指定迭代次数的显示。调整此选项,然后mininterval以获得非常有效的循环。如果快速迭代和慢速迭代(网络、跳过项目等)的进度都不稳定,则应该将miniters设置为1
  • ASCII:布尔值或字符串,可选
    如果未指定或为False,则使用Unicode(平滑块)填充仪表。备用方法是使用ASCII码字符“123456789#”
  • 禁用:布尔值,可选
    是否禁用整个进度栏包装[默认值:false]。如果设置为None,则在非TTY上禁用
  • 单位:字符串,可选
    将用于定义每个迭代的单位的字符串[默认值:it]
  • 单位_刻度:Bool、int或Float,可选
    如果为1或True,则迭代次数将自动减少/缩放,并将添加遵循国际单位制标准的公制前缀(千、兆等)[默认值:FALSE]。如果有任何其他非零数,将缩放totaln
  • 动态参数(_N):布尔值,可选
    如果设置,则会不断更改ncolsnrows到环境(允许调整窗口大小)[默认值:FALSE]
  • 平滑:浮动,可选
    速度估计的指数移动平均平滑系数(在GUI模式中忽略)。范围从0(平均速度)到1(当前/瞬时速度)[默认值:0.3]
  • 条形图格式(_F):字符串,可选
    指定自定义条形字符串格式。可能会影响性能。[默认值:‘{l_bar}{bar}{r_bar}’],其中l_bar=‘{desc}:{percentage:3.0f}%|’,r_bar=‘|{n_fmt}/{total_fmt}[{elapsed}<{reaving},’{rate_fmt}{postfix}]‘’可能的变量:l_bar,bar,r_bar,n,n_fmt,total,total_fmt,百分比,已用rate_noinv、rate_noinv_fmt、rate_inv、rate_inv_fmt、后缀、unit_ditor、剩余、剩余_s、eta。请注意,如果{desc}为空,则会在{desc}之后自动删除尾随的“:”
  • 首字母:整型或浮点型,可选
    初始计数器值。重新启动进度条时非常有用[默认值:0]。如果使用浮点,请考虑指定{n:.3f}或类似于bar_format,或指定unit_scale
  • 职位:整型,可选
    如果未指定,请指定自动打印此条形图的行偏移量(从0开始)。一次管理多个条形图非常有用(例如,从线程)
  • 后缀:DICT或*,可选
    指定要在条末尾显示的其他统计信息。呼叫set_postfix(**postfix)如果可能(DICT)
  • 单位_除数:浮动,可选
    [默认值:1000],除非忽略unit_scale是真的吗?
  • 写入字节(_B):布尔值,可选
    如果(默认值:无)和file未指定,则将使用Python 2写入字节。如果True还将写入字节。在所有其他情况下,将默认为Unicode
  • 锁定参数(_A):元组,可选
    已传递给refresh用于中间输出(初始化、迭代和更新)
  • n行:整型,可选
    屏幕高度。如果指定,则隐藏此边界之外的嵌套条。如果未指定,则尝试使用环境高度。退而求其次是20
  • 颜色:字符串,可选
    条形图颜色(例如“绿色”、“#00ff00”)
  • 延迟:浮动,可选
    在经过[默认值:0]秒之前不显示

Extra CLI Options

  • 神志不清:CHR,可选
    分隔字符[默认值:‘n’]。使用“0”表示NULL。注:在Windows系统上,Python将‘n’转换为‘rn’
  • buf_size:整型,可选
    字符串缓冲区大小(以字节为单位)[默认值:256]在以下情况下使用delim已指定
  • 字节数:布尔值,可选
    如果为true,将计算字节数,忽略delim和Defaultunit_scale为了真的,unit_divisor设置为1024,并且unit到“B”
  • T形三通:布尔值,可选
    如果为true,则通过stdin对两个人都是stderrstdout
  • 更新:布尔值,可选
    如果为true,则将输入视为新经过的迭代,即要传递到的数字update()请注意,这很慢(~2e5 it/s),因为每个输入都必须解码为数字
  • 更新目标(_T):布尔值,可选
    如果为true,则将输入视为已用迭代总数,即要分配给的数字self.n请注意,这很慢(~2e5 it/s),因为每个输入都必须解码为数字
  • 空:布尔值,可选
    如果为true,将丢弃输入(无标准输出)
  • 人工路径:字符串,可选
    要安装tqdm手册页的目录
  • 压缩路径:字符串,可选
    放置tqdm完成的目录
  • 日志:字符串,可选
    严重|致命|错误|警告(ING)|[默认值:‘INFO’]|DEBUG|NOTSET

Returns

  • 输出:修饰迭代器
class tqdm():
  def update(self, n=1):
      """
      Manually update the progress bar, useful for streams
      such as reading files.
      E.g.:
      >>> t = tqdm(total=filesize) # Initialise
      >>> for current_buffer in stream:
      ...    ...
      ...    t.update(len(current_buffer))
      >>> t.close()
      The last line is highly recommended, but possibly not necessary if
      ``t.update()`` will be called in such a way that ``filesize`` will be
      exactly reached and printed.

      Parameters
      ----------
      n  : int or float, optional
          Increment to add to the internal counter of iterations
          [default: 1]. If using float, consider specifying ``{n:.3f}``
          or similar in ``bar_format``, or specifying ``unit_scale``.

      Returns
      -------
      out  : bool or None
          True if a ``display()`` was triggered.
      """

  def close(self):
      """Cleanup and (if leave=False) close the progressbar."""

  def clear(self, nomove=False):
      """Clear current bar display."""

  def refresh(self):
      """
      Force refresh the display of this bar.

      Parameters
      ----------
      nolock  : bool, optional
          If ``True``, does not lock.
          If [default: ``False``]: calls ``acquire()`` on internal lock.
      lock_args  : tuple, optional
          Passed to internal lock's ``acquire()``.
          If specified, will only ``display()`` if ``acquire()`` returns ``True``.
      """

  def unpause(self):
      """Restart tqdm timer from last print time."""

  def reset(self, total=None):
      """
      Resets to 0 iterations for repeated use.

      Consider combining with ``leave=True``.

      Parameters
      ----------
      total  : int or float, optional. Total to use for the new bar.
      """

  def set_description(self, desc=None, refresh=True):
      """
      Set/modify description of the progress bar.

      Parameters
      ----------
      desc  : str, optional
      refresh  : bool, optional
          Forces refresh [default: True].
      """

  def set_postfix(self, ordered_dict=None, refresh=True, **tqdm_kwargs):
      """
      Set/modify postfix (additional stats)
      with automatic formatting based on datatype.

      Parameters
      ----------
      ordered_dict  : dict or OrderedDict, optional
      refresh  : bool, optional
          Forces refresh [default: True].
      kwargs  : dict, optional
      """

  @classmethod
  def write(cls, s, file=sys.stdout, end="\n"):
      """Print a message via tqdm (without overlap with bars)."""

  @property
  def format_dict(self):
      """Public API for read-only member access."""

  def display(self, msg=None, pos=None):
      """
      Use ``self.sp`` to display ``msg`` in the specified ``pos``.

      Consider overloading this function when inheriting to use e.g.:
      ``self.some_frontend(**self.format_dict)`` instead of ``self.sp``.

      Parameters
      ----------
      msg  : str, optional. What to display (default: ``repr(self)``).
      pos  : int, optional. Position to ``moveto``
        (default: ``abs(self.pos)``).
      """

  @classmethod
  @contextmanager
  def wrapattr(cls, stream, method, total=None, bytes=True, **tqdm_kwargs):
      """
      stream  : file-like object.
      method  : str, "read" or "write". The result of ``read()`` and
          the first argument of ``write()`` should have a ``len()``.

      >>> with tqdm.wrapattr(file_obj, "read", total=file_obj.size) as fobj:
      ...     while True:
      ...         chunk = fobj.read(chunk_size)
      ...         if not chunk:
      ...             break
      """

  @classmethod
  def pandas(cls, *targs, **tqdm_kwargs):
      """Registers the current `tqdm` class with `pandas`."""

def trange(*args, **tqdm_kwargs):
    """
    A shortcut for `tqdm(xrange(*args), **tqdm_kwargs)`.
    On Python3+, `range` is used instead of `xrange`.
    """

Convenience Functions

def tqdm.contrib.tenumerate(iterable, start=0, total=None,
                            tqdm_class=tqdm.auto.tqdm, **tqdm_kwargs):
    """Equivalent of `numpy.ndenumerate` or builtin `enumerate`."""

def tqdm.contrib.tzip(iter1, *iter2plus, **tqdm_kwargs):
    """Equivalent of builtin `zip`."""

def tqdm.contrib.tmap(function, *sequences, **tqdm_kwargs):
    """Equivalent of builtin `map`."""

Submodules

class tqdm.notebook.tqdm(tqdm.tqdm):
    """IPython/Jupyter Notebook widget."""

class tqdm.auto.tqdm(tqdm.tqdm):
    """Automatically chooses beween `tqdm.notebook` and `tqdm.tqdm`."""

class tqdm.asyncio.tqdm(tqdm.tqdm):
  """Asynchronous version."""
  @classmethod
  def as_completed(cls, fs, *, loop=None, timeout=None, total=None,
                   **tqdm_kwargs):
      """Wrapper for `asyncio.as_completed`."""

class tqdm.gui.tqdm(tqdm.tqdm):
    """Matplotlib GUI version."""

class tqdm.tk.tqdm(tqdm.tqdm):
    """Tkinter GUI version."""

class tqdm.rich.tqdm(tqdm.tqdm):
    """`rich.progress` version."""

class tqdm.keras.TqdmCallback(keras.callbacks.Callback):
    """Keras callback for epoch and batch progress."""

class tqdm.dask.TqdmCallback(dask.callbacks.Callback):
    """Dask callback for task progress."""

contrib

这个tqdm.contrib软件包还包含实验模块:

  • tqdm.contrib.itertools:周围有薄薄的包装纸itertools
  • tqdm.contrib.concurrent:周围有薄薄的包装纸concurrent.futures
  • tqdm.contrib.discord:发布到Discord机器人
  • tqdm.contrib.telegram:发布到Telegram机器人
  • tqdm.contrib.bells:自动启用所有可选功能
    • autopandasdiscordtelegram

Examples and Advanced Usage

Description and additional stats

自定义信息可以在上动态显示和更新tqdm带有条形图的条形图descpostfix参数:

from tqdm import tqdm, trange
from random import random, randint
from time import sleep

with trange(10) as t:
    for i in t:
        # Description will be displayed on the left
        t.set_description('GEN %i' % i)
        # Postfix will be displayed on the right,
        # formatted automatically based on argument's datatype
        t.set_postfix(loss=random(), gen=randint(1,999), str='h',
                      lst=[1, 2])
        sleep(0.1)

with tqdm(total=10, bar_format="{postfix[0]} {postfix[1][value]:>8.2g}",
          postfix=["Batch", dict(value=0)]) as t:
    for i in range(10):
        sleep(0.1)
        t.postfix[1]["value"] = i / 2
        t.update()

使用时要记住的要点{postfix[...]}bar_format字符串:

  • postfix还需要以兼容格式作为初始参数传递,并且
  • postfix将自动转换为字符串(如果它是dict-类物体。要防止此行为,请在键不是字符串的字典中插入额外的项

附加内容bar_format参数也可以通过重写format_dict,并且可以使用以下命令修改栏本身ascii

from tqdm import tqdm
class TqdmExtraFormat(tqdm):
    """Provides a `total_time` format parameter"""
    @property
    def format_dict(self):
        d = super(TqdmExtraFormat, self).format_dict
        total_time = d["elapsed"] * (d["total"] or 0) / max(d["n"], 1)
        d.update(total_time=self.format_interval(total_time) + " in total")
        return d

for i in TqdmExtraFormat(
      range(9), ascii=" .oO0",
      bar_format="{total_time}: {percentage:.0f}%|{bar}{r_bar}"):
    if i == 4:
        break
00:00 in total: 44%|0000.     | 4/9 [00:00<00:00, 962.93it/s]

请注意,{bar}还支持格式说明符[width][type]

  • width
    • 未指定(默认):自动填充ncols
    • int >= 0:固定宽度替代ncols逻辑
    • int < 0:从自动缺省值中减去
  • type
    • a:ASCII(ascii=True覆盖)
    • u:unicode(ascii=False覆盖)
    • b:空白(ascii=" "覆盖)

这意味着可以使用以下方法创建具有右对齐文本的固定栏:bar_format="{l_bar}{bar:10}|{bar:-10b}right-justified"

Nested progress bars

tqdm支持嵌套进度条。下面是一个示例:

from tqdm.auto import trange
from time import sleep

for i in trange(4, desc='1st loop'):
    for j in trange(5, desc='2nd loop'):
        for k in trange(50, desc='3rd loop', leave=False):
            sleep(0.01)

要手动控制定位(例如,用于多处理),您可以指定position=n哪里n=0对于最外面的酒吧,n=1下一个,以此类推。不过,最好还是检查一下tqdm无需手动即可工作position第一

from time import sleep
from tqdm import trange, tqdm
from multiprocessing import Pool, RLock, freeze_support

L = list(range(9))

def progresser(n):
    interval = 0.001 / (n + 2)
    total = 5000
    text = "#{}, est. {:<04.2}s".format(n, interval * total)
    for _ in trange(total, desc=text, position=n):
        sleep(interval)

if __name__ == '__main__':
    freeze_support()  # for Windows support
    tqdm.set_lock(RLock())  # for managing output contention
    p = Pool(initializer=tqdm.set_lock, initargs=(tqdm.get_lock(),))
    p.map(progresser, L)

请注意,在Python3中,tqdm.write是否线程安全:

from time import sleep
from tqdm import tqdm, trange
from concurrent.futures import ThreadPoolExecutor

L = list(range(9))

def progresser(n):
    interval = 0.001 / (n + 2)
    total = 5000
    text = "#{}, est. {:<04.2}s".format(n, interval * total)
    for _ in trange(total, desc=text):
        sleep(interval)
    if n == 6:
        tqdm.write("n == 6 completed.")
        tqdm.write("`tqdm.write()` is thread-safe in py3!")

if __name__ == '__main__':
    with ThreadPoolExecutor() as p:
        p.map(progresser, L)

Hooks and callbacks

tqdm可以轻松支持回调/挂钩和手动更新。下面是一个包含以下内容的示例urllib

“urllib.urlsearche“文档

[.]
如果存在,将调用一次钩子函数
关于网络连接的建立和每次挡路阅读后的一次
之后。将向挂钩传递三个参数;块计数
到目前为止已传输的挡路大小(以字节为单位)和文件的总大小
[.]
import urllib, os
from tqdm import tqdm
urllib = getattr(urllib, 'request', urllib)

class TqdmUpTo(tqdm):
    """Provides `update_to(n)` which uses `tqdm.update(delta_n)`."""
    def update_to(self, b=1, bsize=1, tsize=None):
        """
        b  : int, optional
            Number of blocks transferred so far [default: 1].
        bsize  : int, optional
            Size of each block (in tqdm units) [default: 1].
        tsize  : int, optional
            Total size (in tqdm units). If [default: None] remains unchanged.
        """
        if tsize is not None:
            self.total = tsize
        return self.update(b * bsize - self.n)  # also sets self.n = b * bsize

eg_link = "https://caspersci.uk.to/matryoshka.zip"
with TqdmUpTo(unit='B', unit_scale=True, unit_divisor=1024, miniters=1,
              desc=eg_link.split('/')[-1]) as t:  # all optional kwargs
    urllib.urlretrieve(eg_link, filename=os.devnull,
                       reporthook=t.update_to, data=None)
    t.total = t.n

灵感来自twine#242中的功能替代examples/tqdm_wget.py

建议使用miniters=1每当迭代速度存在潜在较大差异时(例如,通过补丁连接下载文件)

包装读/写方法

要通过类似文件的对象测量吞吐量,请执行以下操作readwrite方法,使用CallbackIOWrapper

from tqdm.auto import tqdm
from tqdm.utils import CallbackIOWrapper

with tqdm(total=file_obj.size,
          unit='B', unit_scale=True, unit_divisor=1024) as t:
    fobj = CallbackIOWrapper(t.update, file_obj, "read")
    while True:
        chunk = fobj.read(chunk_size)
        if not chunk:
            break
    t.reset()
    # ... continue to use `t` for something else

或者,使用更简单的wrapattr便利函数,该函数将压缩两个urllibCallbackIOWrapper下面是示例:

import urllib, os
from tqdm import tqdm

eg_link = "https://caspersci.uk.to/matryoshka.zip"
response = getattr(urllib, 'request', urllib).urlopen(eg_link)
with tqdm.wrapattr(open(os.devnull, "wb"), "write",
                   miniters=1, desc=eg_link.split('/')[-1],
                   total=getattr(response, 'length', None)) as fout:
    for chunk in response:
        fout.write(chunk)

这个requests等价物几乎完全相同:

import requests, os
from tqdm import tqdm

eg_link = "https://caspersci.uk.to/matryoshka.zip"
response = requests.get(eg_link, stream=True)
with tqdm.wrapattr(open(os.devnull, "wb"), "write",
                   miniters=1, desc=eg_link.split('/')[-1],
                   total=int(response.headers.get('content-length', 0))) as fout:
    for chunk in response.iter_content(chunk_size=4096):
        fout.write(chunk)

自定义回调

tqdm以智能地跳过不必要的显示而闻名。要使自定义回调利用这一点,只需使用update()这设置为True如果一个display()已被触发

from tqdm.auto import tqdm as std_tqdm

def external_callback(*args, **kwargs):
    ...

class TqdmExt(std_tqdm):
    def update(self, n=1):
        displayed = super(TqdmExt, self).update(n):
        if displayed:
            external_callback(**self.format_dict)
        return displayed

asyncio

请注意,break当前未被异步迭代器捕获。这意味着tqdm在这种情况下,无法自行清理:

from tqdm.asyncio import tqdm

async for i in tqdm(range(9)):
    if i == 2:
        break

取而代之的是,或者调用pbar.close()手动或使用上下文管理器语法:

from tqdm.asyncio import tqdm

with tqdm(range(9)) as pbar:
    async for i in pbar:
        if i == 2:
            break

Pandas Integration

由于大众的需求,我们增加了对pandas–这里有一个例子DataFrame.progress_applyDataFrameGroupBy.progress_apply

import pandas as pd
import numpy as np
from tqdm import tqdm

df = pd.DataFrame(np.random.randint(0, 100, (100000, 6)))

# Register `pandas.progress_apply` and `pandas.Series.map_apply` with `tqdm`
# (can use `tqdm.gui.tqdm`, `tqdm.notebook.tqdm`, optional kwargs, etc.)
tqdm.pandas(desc="my bar!")

# Now you can use `progress_apply` instead of `apply`
# and `progress_map` instead of `map`
df.progress_apply(lambda x: x**2)
# can also groupby:
# df.groupby(0).progress_apply(lambda x: x**2)

如果您对这是如何工作的(以及如何为您自己的回调修改它)感兴趣,请参阅examples文件夹或导入模块,然后运行help()

Keras Integration

一个keras也可以回调:

from tqdm.keras import TqdmCallback

...

model.fit(..., verbose=0, callbacks=[TqdmCallback()])

Dask Integration

一个dask也可以回调:

from tqdm.dask import TqdmCallback

with TqdmCallback(desc="compute"):
    ...
    arr.compute()

# or use callback globally
cb = TqdmCallback(desc="global")
cb.register()
arr.compute()

IPython/Jupyter Integration

IPython/Jupyter通过tqdm.notebook子模块:

from tqdm.notebook import trange, tqdm
from time import sleep

for i in trange(3, desc='1st loop'):
    for j in tqdm(range(100), desc='2nd loop'):
        sleep(0.01)

除了……之外tqdm功能方面,该子模块提供原生Jupyter小部件(与IPythonv1-v4和Jupyter兼容)、完全工作的嵌套条和颜色提示(蓝色:正常、绿色:已完成、红色:错误/中断、浅蓝色:无ETA);如下所示



这个notebook版本支持总宽度的百分比或像素(例如:ncols='100%'ncols='480px')

也可以让tqdm自动选择控制台或笔记本电脑版本,方法是使用autonotebook子模块:

from tqdm.autonotebook import tqdm
tqdm.pandas()

请注意,这将发出一个TqdmExperimentalWarning如果在笔记本中运行,因为它不能区分jupyter notebookjupyter console使用auto而不是autonotebook要取消显示此警告,请执行以下操作

请注意,笔记本将在创建它的单元格中显示该条。这可能是与使用它的单元格不同的单元格。如果这也不是我们想要的,

  • 将栏的创建延迟到必须显示它的单元格,或者
  • 使用创建条形图display=False,并且在稍后的蜂窝呼叫中display(bar.container)
from tqdm.notebook import tqdm
pbar = tqdm(..., display=False)
# different cell
display(pbar.container)

这个keras回调有一个display()同样可以使用的方法:

from tqdm.keras import TqdmCallback
cbk = TqdmCallback(display=False)
# different cell
cbk.display()
model.fit(..., verbose=0, callbacks=[cbk])

另一种可能是拥有一个不断重复使用的条(靠近笔记本顶部)(使用reset()而不是close())。因此,笔记本版本(与CLI版本不同)不会自动调用close()在此基础上Exception

from tqdm.notebook import tqdm
pbar = tqdm()
# different cell
iterable = range(100)
pbar.reset(total=len(iterable))  # initialise with new `total`
for i in iterable:
    pbar.update()
pbar.refresh()  # force print final status but don't `close()`

Custom Integration

要更改默认参数(如进行dynamic_ncols=True),只需使用内置的Python魔术:

from functools import partial
from tqdm import tqdm as std_tqdm
tqdm = partial(std_tqdm, dynamic_ncols=True)

要进一步自定义,请参见tqdm可以从继承以创建自定义回调(与TqdmUpTo示例above)或用于自定义前端(例如,诸如笔记本或绘图包之类的GUI)。在后一种情况下:

  1. def __init__()要呼叫,请执行以下操作super().__init__(..., gui=True)禁用端子的步骤status_printer创作
  2. 重新定义:close()clear()display()

考虑超载display()使用,例如,使用self.frontend(**self.format_dict)而不是self.sp(repr(self))

继承的一些子模块示例:

Dynamic Monitor/Meter

您可以使用tqdm作为一个不是单调递增的韵律。这可能是因为n减少(例如CPU使用情况监视器)或total变化

递归搜索文件就是一个例子。这个total是到目前为止找到的对象数,而n是属于文件(而不是文件夹)的那些对象的数量:

from tqdm import tqdm
import os.path

def find_files_recursively(path, show_progress=True):
    files = []
    # total=1 assumes `path` is a file
    t = tqdm(total=1, unit="file", disable=not show_progress)
    if not os.path.exists(path):
        raise IOError("Cannot find:" + path)

    def append_found_file(f):
        files.append(f)
        t.update()

    def list_found_dir(path):
        """returns os.listdir(path) assuming os.path.isdir(path)"""
        listing = os.listdir(path)
        # subtract 1 since a "file" we found was actually this directory
        t.total += len(listing) - 1
        # fancy way to give info without forcing a refresh
        t.set_postfix(dir=path[-10:], refresh=False)
        t.update(0)  # may trigger a refresh
        return listing

    def recursively_search(path):
        if os.path.isdir(path):
            for f in list_found_dir(path):
                recursively_search(os.path.join(path, f))
        else:
            append_found_file(path)

    recursively_search(path)
    t.set_postfix(dir=path)
    t.close()
    return files

使用update(0)是一种方便的方式来让tqdm决定何时触发显示刷新以避免控制台垃圾邮件

Writing messages

这是一项正在进行的工作(请参见#737)

因为tqdm使用简单的打印机制显示进度条,您不应该在终端中使用print()当进度条打开时

在终端中写入消息而不与其发生任何冲突tqdm条形图显示,a.write()提供了一种方法:

from tqdm.auto import tqdm, trange
from time import sleep

bar = trange(10)
for i in bar:
    # Print using tqdm class method .write()
    sleep(0.1)
    if not (i % 3):
        tqdm.write("Done task %i" % i)
    # Can also use bar.write()

默认情况下,这将打印到标准输出sys.stdout但是您可以使用file论点。例如,这可用于将写入日志文件或类的消息重定向

Redirecting writing

如果使用可以将消息打印到控制台的库,请通过替换print()使用tqdm.write()可能不是我们想要的。在这种情况下,重定向sys.stdouttqdm.write()是一种选择

要重定向,请执行以下操作sys.stdout,创建一个类似文件的类,该类将把任何输入字符串写入tqdm.write(),并提供参数file=sys.stdout, dynamic_ncols=True

下面给出了一个可重用的规范示例:

from time import sleep
import contextlib
import sys
from tqdm import tqdm
from tqdm.contrib import DummyTqdmFile


@contextlib.contextmanager
def std_out_err_redirect_tqdm():
    orig_out_err = sys.stdout, sys.stderr
    try:
        sys.stdout, sys.stderr = map(DummyTqdmFile, orig_out_err)
        yield orig_out_err[0]
    # Relay exceptions
    except Exception as exc:
        raise exc
    # Always restore sys.stdout/err if necessary
    finally:
        sys.stdout, sys.stderr = orig_out_err

def some_fun(i):
    print("Fee, fi, fo,".split()[i])

# Redirect stdout to tqdm.write() (don't forget the `as save_stdout`)
with std_out_err_redirect_tqdm() as orig_stdout:
    # tqdm needs the original stdout
    # and dynamic_ncols=True to autodetect console width
    for i in tqdm(range(3), file=orig_stdout, dynamic_ncols=True):
        sleep(.5)
        some_fun(i)

# After the `with`, printing is restored
print("Done!")

Redirecting logging

类似于sys.stdout/sys.stderr如上所述,控制台logging也可以重定向到tqdm.write()

警告:如果还重定向sys.stdout/sys.stderr,请务必重定向logging如果需要,先来

中提供了帮助器方法tqdm.contrib.logging例如:

import logging
from tqdm import trange
from tqdm.contrib.logging import logging_redirect_tqdm

LOG = logging.getLogger(__name__)

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    with logging_redirect_tqdm():
        for i in trange(9):
            if i == 4:
                LOG.info("console logging redirected to `tqdm.write()`")
    # logging restored

Monitoring thread, intervals and miniters

tqdm实施一些技巧来提高效率和降低管理费用

  • 避免不必要的频繁条刷新:mininterval定义每次刷新之间等待的时间。tqdm始终在后台更新,但它将仅在mininterval
  • 减少检查系统时钟/时间的调用次数
  • mininterval比起配置更直观miniters巧妙的调整系统dynamic_miniters将自动调整miniters与时间相适应的迭代次数mininterval从本质上讲,tqdm将在不实际检查时间的情况下检查是否到了打印时间。通过手动设置,仍可绕过此行为miniters

但是,请考虑快速迭代和慢速迭代相结合的情况。在几次快速迭代之后,dynamic_miniters将设置miniters变成了一个大数目。当迭代速率随后减慢时,miniters将保持较大,从而降低显示更新频率。要解决此问题,请执行以下操作:

  • maxinterval定义显示刷新之间的最长时间。并发监视线程检查过期的更新,并在必要时强制更新

监视线程不应该有明显的开销,并且默认情况下保证至少每10秒更新一次。该值可以通过设置monitor_interval任何tqdm实例(即t = tqdm.tqdm(...); t.monitor_interval = 2)。可以通过设置在应用程序范围内禁用监视线程tqdm.tqdm.monitor_interval = 0在实例化任何tqdm钢筋

Merch

你可以买到tqdm branded merch现在!

Contributions

所有源代码都托管在GitHub欢迎投稿

请参阅CONTRIBUTING有关详细信息,请参阅文件

做出重大贡献的开发人员,按SLOC(幸存的代码行,git fame-wMC --excl '\.(png|gif|jpg)$'),包括:

名字 ID号 SLOC 注意事项
卡斯珀·达·科斯塔-路易斯 casperdcl ~81% 主要维护人员
斯蒂芬·拉罗克 lrq3000 ~10% 团队成员
马丁·祖格诺尼 martinzugnoni ~3% 他说:
理查德·谢里登 richardsheridan ~1% 他说:
陈广硕 chengs ~1% 他说:
凯尔·阿尔滕多夫 altendky <1% 他说:
马修·史蒂文斯 mjstevens777 <1% 他说:
哈德琳·玛丽 hadim <1% 团队成员
伊万·伊万诺夫 obiwanus <1% 他说:
丹尼尔·潘特莱特 danielpanteleit <1% 他说:
乔纳斯·哈格 jonashaag <1% 他说:
詹姆斯·E·金三世 jeking3 <1% 他说:
诺姆·约拉夫-拉斐尔 noamraph <1% 原作者
米哈伊尔·科罗博夫 kmike <1% 团队成员

Ports to Other Languages

有关列表,请访问this wiki page

LICENCE

开源(OSI批准):

引文信息:

(自2016年5月19日起)