让每一行 Python 代码都散发优雅的气息
在软件开发中,代码格式化工具是一种自动调整源代码布局的工具,使其遵循特定的风格规范。这些工具处理的是代码的视觉呈现,而不是其逻辑功能。
传统手写代码的问题:
# 不同开发者可能写出完全不同的格式defcalculate(x,y,z):return x+y*z # 紧凑风格defcalculate(x, y, z):return x + y * z # 标准风格defcalculate( x, y, z):return x + y * z # 垂直风格代码格式化工具的作用:
Black 的核心哲学是 **"不妥协"**(No Compromises)。这个名字来源于亨利·福特的著名言论:"你可以拥有任何你喜欢的颜色,只要是黑色。"("You can have any color you want, as long as it's black.")
核心设计原则:
最小化可配置性
一致性优先于个人偏好
# 无论谁运行 Black,输出都完全一致# 输入(任何风格)my_list=[1,2,3,4]# 输出(统一风格)my_list = [1, 2, 3, 4]自动且无痛
Black 的设计目标:
Black vs autopep8:
# autopep8 主要修复 PEP 8 违规,但保留原有结构# 输入deffoo():return42# autopep8 输出deffoo():return42# Black 输出(可能重新格式化更多内容)deffoo():return42Black vs yapf:
# yapf 允许高度自定义,Black 风格固定# 相同的输入,不同的 yapf 配置会产生不同的输出# yapf (google 风格)defvery_long_function_name(argument1, argument2, argument3):return argument1 + argument2 + argument3# yapf (pep8 风格)defvery_long_function_name( argument1, argument2, argument3):return argument1 + argument2 + argument3# Black 风格(唯一)defvery_long_function_name( argument1, argument2, argument3):return argument1 + argument2 + argument3Black vs isort:
零配置体验
提高生产力
# 不再需要手动调整格式# 保存即格式化,节省大量时间减少代码审查负担
强大的社区支持
确定性输出
缺乏灵活性
# 某些团队可能有特殊的风格要求# Black 不会妥协,必须接受其风格有时产生不理想的格式
# Black 可能会将代码格式化为# 你认为不如原始版本的方式result = some_function( argument1, argument2, argument3, argument4, argument5)# 而不是你可能想要的更紧凑的形式处理复杂代码时的挑战
迁移成本
学习曲线
# 基本安装pip install black# 安装特定版本pip install black==23.7.0# 升级到最新版本pip install --upgrade blackpipx 专门用于安装 Python 命令行工具,将其隔离在独立环境中:
# 安装 pipxpip install pipxpipx ensurepath# 使用 pipx 安装 blackpipx install black# 验证安装black --versionpipx 的优势:
macOS (Homebrew):
brew install blackUbuntu/Debian:
sudo apt updatesudo apt install blackConda (跨平台):
conda install -c conda-forge black在项目中使用 requirements.txt 或 pyproject.toml:
# requirements.txtblack==23.7.0或使用 Poetry:
poetry add --dev black或使用 pipenv:
pipenv install --dev black# 检查版本black --version# 输出示例: black, version 23.7.0# 查看帮助black --help# 快速测试echo"print('hello')" | black - # 使用 stdin方法 1:使用 Python 扩展
settings.json:{// 设置 Black 为默认格式化工具"[python]": {"editor.defaultFormatter": "ms-python.black-formatter","editor.formatOnSave": true },// 可选:自定义 Black 参数"black-formatter.args": ["--line-length=100" ],// 如果使用 black 直接"python.formatting.provider": "black","python.formatting.blackArgs": ["--line-length=100"],"editor.formatOnSave": true}方法 2:使用 Black 扩展
{"editor.formatOnSave": true,"[python]": {"editor.defaultFormatter": "ms-python.black-formatter" }}安装 Black:
pip install black配置外部工具:
Black$PyInterpreterDirectory$/black 或直接 black$FilePath$$ProjectFileDir$设置快捷键:
保存时自动格式化(可选):
使用 vim-black 插件:
" 使用 vim-plug 安装Plug 'psf/black'" 配置自动格式化autocmd BufWritePre *.py execute ':Black'" 或设置快捷键nnoremap <C-b> :Black<CR>使用 ALE 插件:
" 安装 ALEPlug 'dense-analysis/ale'" 配置 ALE 使用 blacklet g:ale_python_black_executable = 'black'let g:ale_python_black_options = '--line-length 100'let g:ale_fixers = {\ 'python': ['black'],\}原生 LSP 配置(Neovim 0.5+):
-- 使用 null-ls 配置local null_ls = require("null-ls")null_ls.setup({ sources = { null_ls.builtins.formatting.black, },})-- 保存时自动格式化vim.api.nvim_create_autocmd("BufWritePre", { pattern = "*.py", callback = function() vim.lsp.buf.format({ async = false })end,})安装 Package Control
安装 Black 插件:
配置快捷键:
// Preferences → Key Bindings{"keys": ["ctrl+alt+b"],"command": "black"}保存时自动格式化:
// 在用户设置中添加{"black_on_save": true}# 格式化单个文件black my_script.py# 格式化多个文件black file1.py file2.py file3.py# 格式化整个目录(递归)black src/black . # 当前目录# 检查文件是否需要格式化(不修改)black --check my_script.py# 如果文件已经格式化,退出码为0# 如果需要格式化,退出码为1并输出未格式化的文件列表# 显示需要修改的差异(不修改)black --diff my_script.py# 详细输出black --verbose my_script.py# 静默模式(只输出错误)black --quiet my_script.py示例 1:格式化前
# messy_code.pydefadd_numbers(a,b,c):return a+b+cresult=add_numbers(1,2,3)print(f"The result is {result}")classMyClass:pass运行 Black:
black messy_code.py格式化后:
# messy_code.py (格式化后)defadd_numbers(a, b, c):return a + b + cresult = add_numbers(1, 2, 3)print(f"The result is {result}")classMyClass:pass# 默认 88 个字符black script.py# 自定义行长度black --line-length 100 script.py效果对比:
# 默认 88 字符defvery_long_function_name_with_many_parameters( parameter_one, parameter_two, parameter_three, parameter_four):pass# 120 字符(更宽松)defvery_long_function_name_with_many_parameters(parameter_one, parameter_two, parameter_three, parameter_four):pass# 指定 Python 版本black --target-version py310 script.pyblack --target-version py39 script.py# 支持多个版本black --target-version py39 --target-version py310 script.py支持的版本:
py37, py38, py39, py310, py311, py312作用: Black 会根据目标版本决定是否使用某些语法特性。
# 默认:规范字符串引号(使用双引号)black script.py# 跳过字符串规范化,保留原始引号black --skip-string-normalization script.py效果对比:
# 输入name = 'John'message = "Hello"# 默认输出(规范化为双引号)name = "John"message = "Hello"# 跳过规范化(保留原始引号)name = 'John'message = "Hello"# 安全模式(默认)- 运行所有安全检查black --safe script.py# 快速模式 - 跳过安全检查,速度更快black --fast script.py注意:--fast 模式可能会生成无法运行的代码,谨慎使用。
# 只格式化匹配特定模式的文件black --include '\.pyi?$' src/# 排除特定目录black --exclude '/(\.venv|build|dist)/' .创建文件 calculator.py:
# calculator.py - 故意格式混乱的代码import math, sysfrom typing import List,UnionclassCalculator:defadd(self,a,b):return a+bdefsubtract(self,a,b):return a-bdefmultiply(self,a,b):return a*bdefdivide(self,a,b):if b==0:raise ValueError("Cannot divide by zero")return a/bdefpower(self,base,exponent):return math.pow(base,exponent)defsqrt(self,x):if x<0:raise ValueError("Cannot take sqrt of negative")return math.sqrt(x)defcalculate_average(numbers:List[Union[int,float]])->float:ifnot numbers:return0.0return sum(numbers)/len(numbers)defmain(): calc=Calculator() print(calc.add(5,3)) print(calc.subtract(10,4)) print(calc.multiply(6,7)) print(calc.divide(15,3)) print(calc.power(2,8)) print(calc.sqrt(16)) avg=calculate_average([1,2,3,4,5]) print(f"Average: {avg}")if __name__=="__main__":main()# 1. 先检查是否需要格式化black --check calculator.py# 2. 查看将要做的更改black --diff calculator.py# 3. 实际格式化black calculator.py# 4. 再次检查确认black --check calculator.py# 使用自定义行长度black --line-length 120 calculator.py# 跳过字符串规范化black --skip-string-normalization calculator.py# 组合多个选项black --line-length 100 --target-version py310 calculator.py# 创建测试目录结构mkdir -p myproject/{src,tests}touch myproject/src/{module1,module2,utils}.pytouch myproject/tests/{test_module1,test_module2}.py# 格式化整个项目black myproject/# 只格式化源代码目录black myproject/src/# 排除测试目录black myproject/ --exclude '/tests/'创建 .git/hooks/pre-commit 脚本:
#!/bin/bash# 简单的 Git 预提交钩子# 获取所有将要提交的 Python 文件FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')if [ -n "$FILES" ]; thenecho"Running Black on Python files..." black $FILES# 重新添加格式化后的文件 git add $FILESecho"Black formatting applied."fi使脚本可执行:
chmod +x .git/hooks/pre-commit# format_script.pyimport blackimport sysdefformat_code(source_code):"""使用 Black 格式化代码字符串"""try: mode = black.Mode(line_length=88) formatted = black.format_str(source_code, mode=mode)return formattedexcept Exception as e: print(f"格式化错误: {e}")return source_code# 从文件读取并格式化defformat_file(filename):with open(filename, 'r', encoding='utf-8') as f: content = f.read() formatted = format_code(content)with open(filename, 'w', encoding='utf-8') as f: f.write(formatted) print(f"已格式化: {filename}")if __name__ == "__main__":if len(sys.argv) > 1: format_file(sys.argv[1])else: print("请提供文件名")Linux/macOS (.bashrc 或 .zshrc):
alias bf='black --line-length 100'alias bfc='black --check'alias bfd='black --diff'Windows (PowerShell profile):
function bf { black --line-length 100 $args }function bfc { black --check $args }function bfd { black --diff $args }完成这些练习后,你应该能够:
Black 使用 pyproject.toml 作为配置文件,这是 PEP 518 和 PEP 621 定义的标准 Python 项目配置文件。
# pyproject.toml[tool.black]# 基础配置line-length = 88target-version = ['py310', 'py311']include = '\.pyi?$'extend-exclude = '''/( # 排除的目录 \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist | \.pytest_cache | \.vscode | __pycache__)/'''# 字符串规范化(可选)skip-string-normalization = false# 魔法逗号处理(可选)skip-magic-trailing-comma = false# 预览模式(使用新特性)preview = false# 详细输出verbose = falseline-length (默认: 88)
[tool.black]line-length = 100 # 更宽松的行长度target-version (默认: 当前 Python 版本)
[tool.black]target-version = ['py37', 'py38', 'py39', 'py310']include 和 exclude
[tool.black]# 包含的模式(正则表达式)include = '\.pyi?$' # 匹配 .py 和 .pyi 文件# 排除的模式exclude = '''/( \.eggs | \.git | \.venv | build | dist)/'''skip-string-normalization (默认: false)
[tool.black]skip-string-normalization = true # 保留原始引号skip-magic-trailing-comma (默认: false)
[tool.black]skip-magic-trailing-comma = true # 禁用魔法逗号特性preview (默认: false)
[tool.black]preview = true # 启用预览模式的新特性required-version (指定所需版本)
[tool.black]required-version = "23.7.0" # 如果版本不匹配会警告对于 monorepo,可以在不同子目录使用不同的配置:
# 项目根目录 pyproject.toml[tool.black]line-length = 88target-version = ['py310']# 子目录 app/legacy/pyproject.toml(覆盖配置)[tool.black]line-length = 100target-version = ['py37'] # 旧代码使用旧版本Black 的配置优先级(从高到低):
# pyproject.toml 中设置 line-length = 88# 命令行参数会覆盖black --line-length 120 script.py # 使用 120# 多个命令行参数black --line-length 100 --skip-string-normalization script.py# 查看当前使用的配置black --verbose script.py# 输出示例:# Using configuration from /path/to/pyproject.toml.# line-length: 88# target-versions: ['py310', 'py311']# skip-string-normalization: False[tool.black]# 简单排除(逗号分隔)exclude = '''\.venv| build| dist'''# 复杂排除(使用正则)exclude = '''/( # 排除虚拟环境 \.venv | \.env | venv | env | \.pytest_cache | \.mypy_cache | \.tox | \.eggs | \.git | \.hg | \.svn | __pycache__ # 排除构建目录 | build | dist | wheels # 排除第三方代码 | site-packages | node_modules # 排除特定文件 | migrations | conftest\.py | setup\.py)/'''Black 默认会读取 .gitignore 来排除文件。可以通过配置覆盖:
[tool.black]# 忽略 .gitignore(不推荐)force-exclude = '''/( \.venv)/'''场景 1:排除迁移文件
[tool.black]exclude = '''/migrations/'''场景 2:排除生成的文件
[tool.black]exclude = '''/generated/| \.pb2\.py$ # protobuf 生成的文件| \_pb2\.py$'''场景 3:排除测试文件中的特定模式
[tool.black]exclude = '''/tests/fixtures/| /tests/legacy/'''魔法逗号(magic trailing comma)是 Black 的一个特性,用于控制容器格式。
# 没有魔法逗号 - 单行显示my_list = [1, 2, 3]# 有魔法逗号 - 每行一个元素my_list = [1,2,3,]# 魔法逗号触发垂直格式function_call( arg1, arg2, arg3,)规则 1:容器内的魔法逗号
# 输入data = [1,2,3# 没有尾随逗号]# Black 输出(单行)data = [1, 2, 3]# 输入(有尾随逗号)data = [1,2,3, # 有尾随逗号]# Black 输出(垂直格式)data = [1,2,3,]规则 2:函数参数的魔法逗号
# 输入(无尾随逗号)deflong_function_name( param1, param2, param3):pass# Black 输出(如果行长度允许)deflong_function_name(param1, param2, param3):pass# 输入(有尾随逗号)deflong_function_name( param1, param2, param3,):pass# Black 输出(强制垂直格式)deflong_function_name( param1, param2, param3,):pass规则 3:导入语句的魔法逗号
# 无尾随逗号from module import (func1, func2, func3)# 有尾随逗号(强制垂直)from module import ( func1, func2, func3,)[tool.black]skip-magic-trailing-comma = true禁用后,即使有尾随逗号也不会触发垂直格式:
# skip-magic-trailing-comma = truedata = [1,2,3, # 仍然会被压缩]# 输出:data = [1, 2, 3]使用尾随逗号控制格式
# 当需要垂直显示时,添加尾随逗号config = {'key1': 'value1','key2': 'value2', # 尾随逗号保持垂直格式}Git diff 友好
# 有尾随逗号时,添加新元素只改变一行items = [1,2,3, # 尾随逗号]# 添加新元素时items = [1,2,3,4, # 只添加这一行]避免不必要的魔法逗号
# 不推荐:不必要的尾随逗号single = [1,] # 会触发垂直格式,但只有一个元素# 推荐single = [1]创建一个文件 magic_comma_demo.py:
# 测试魔法逗号效果# 示例 1:列表numbers1 = [1, 2, 3, 4, 5]numbers2 = [1,2,3,4,5,]# 示例 2:字典config1 = {'host': 'localhost', 'port': 8080, 'debug': True}config2 = {'host': 'localhost','port': 8080,'debug': True,}# 示例 3:函数调用result1 = calculate(10, 20, 30, 40)result2 = calculate(10,20,30,40,)# 示例 4:函数定义defprocess_data1(data, transform, validate):passdefprocess_data2( data, transform, validate,):pass运行 Black 观察效果:
# 默认行为black magic_comma_demo.py# 禁用魔法逗号black --skip-magic-trailing-comma magic_comma_demo.pypre-commit 是最流行的 Git 钩子管理工具。
安装 pre-commit:
pip install pre-commit创建 .pre-commit-config.yaml:
# .pre-commit-config.yamlrepos:# Black 代码格式化-repo:https://github.com/psf/blackrev:23.7.0# 使用最新稳定版本hooks:-id:blacklanguage_version:python3.10args:[--line-length=88]# 可选:添加其他钩子-repo:https://github.com/pycqa/isortrev:5.12.0hooks:-id:isortname:isort(python)args:["--profile","black"]-repo:https://github.com/pycqa/flake8rev:6.1.0hooks:-id:flake8args:[--max-line-length=88,--extend-ignore=E203]安装钩子:
# 安装 pre-commit 钩子pre-commit install# 可选:在 CI 中运行所有文件pre-commit run --all-files# 手动运行特定钩子pre-commit run black --all-files钩子行为:
# 提交时自动运行git commit -m "Add new feature"# 输出示例:# black....................................................................Failed# - hook id: black# - files were modified by this hook# # reformatted script.py# All done! ✨ 🍰 ✨# 1 file reformatted.创建 .git/hooks/pre-commit:
#!/bin/bash# 手动配置的 pre-commit 钩子echo"Running Black formatter..."# 获取所有暂存的 Python 文件STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')if [ -n "$STAGED_PYTHON_FILES" ]; then# 运行 Black black $STAGED_PYTHON_FILES# 重新暂存格式化后的文件 git add $STAGED_PYTHON_FILESecho"Black formatting applied to:"echo"$STAGED_PYTHON_FILES"elseecho"No Python files to format."fiexit 0使钩子可执行:
chmod +x .git/hooks/pre-commit创建 .gitattributes:
# 在提交时自动格式化*.py filter=black配置 Git 过滤器:
git config --global filter.black.clean 'black --quiet -'git config --global filter.black.smudge 'cat'settings.json 完整示例:
{// Python 特定配置"[python]": {"editor.defaultFormatter": "ms-python.black-formatter","editor.formatOnSave": true,"editor.codeActionsOnSave": {"source.organizeImports": true// 配合 isort } },// Black 格式化器配置"black-formatter.args": ["--line-length=100","--skip-string-normalization" ],// 全局格式化设置"editor.formatOnSave": true,"editor.formatOnPaste": false,"editor.formatOnType": false,// 多语言支持"files.associations": {"*.py": "python" }}工作区特定配置(.vscode/settings.json):
{// 项目特定配置"python.formatting.provider": "black","python.formatting.blackPath": "${workspaceFolder}/venv/bin/black","python.formatting.blackArgs": ["--config=${workspaceFolder}/pyproject.toml" ],"editor.formatOnSave": true}配置 Black 作为外部工具(增强版):
File → Settings → Tools → External Tools
创建 Black 工具:
Black Formatter$PyInterpreterDirectory$/black$FilePath$ --line-length 100$ProjectFileDir$$FILE_PATH$\:$LINE$\:$COLUMN$\:.*配置 File Watcher(实时格式化):
BlackPythonProject Files$PyInterpreterDirectory$/black$FilePath$$FilePath$$ProjectFileDir$✓保存时自动格式化(Macros):
使用 ALE 的完整配置:
" .vimrc 或 init.vim" 使用 vim-plugcall plug#begin()Plug 'dense-analysis/ale'Plug 'psf/black'call plug#end()" ALE 配置let g:ale_fixers = {\ 'python': ['black', 'isort'],\}let g:ale_fix_on_save = 1let g:ale_python_black_options = '--line-length=100'let g:ale_python_isort_options = '--profile black'" 快捷键nnoremap <leader>f :call Black()<CR>nnoremap <leader>c :ALEFix<CR>" 状态栏显示let g:ale_statusline_format = ['✗ %d', '⚡ %d', '✓ OK']Neovim Lua 配置:
-- init.lualocal null_ls = require("null-ls")null_ls.setup({ sources = { null_ls.builtins.formatting.black.with({ extra_args = { "--line-length", "100" } }), null_ls.builtins.formatting.isort.with({ extra_args = { "--profile", "black" } }) }, on_attach = function(client, bufnr)-- 保存时格式化 vim.api.nvim_create_autocmd("BufWritePre", { buffer = bufnr, callback = function() vim.lsp.buf.format({ async = false })end })end});; .emacs 或 init.el;; 使用 blacken(use-package blacken :ensure t :hook (python-mode . blacken-mode) :config (setq blacken-line-length 100));; 保存时自动格式化(add-hook 'python-mode-hook (lambda () (add-hook 'before-save-hook 'blacken-buffer nil t)))基本检查工作流:
# .github/workflows/lint.ymlname:Linton:push:branches:[main,develop]pull_request:branches:[main]jobs:lint:runs-on:ubuntu-lateststrategy:matrix:python-version:["3.8","3.9","3.10","3.11"]steps:-uses:actions/checkout@v3-name:SetupPython${{matrix.python-version}}uses:actions/setup-python@v4with:python-version:${{matrix.python-version}}-name:Installdependenciesrun:| python -m pip install --upgrade pip pip install black-name:CheckformattingwithBlackrun:| black --check --diff .完整的 CI/CD 工作流:
# .github/workflows/ci.ymlname:CIPipelineon:push:branches:[main,develop]pull_request:branches:[main]jobs:format-check:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v3-name:SetupPythonuses:actions/setup-python@v4with:python-version:'3.10'cache:'pip'-name:InstallBlackrun:pipinstallblack-name:Checkformattingrun:| black --check --diff --verbose .-name:Uploaddiffasartifact(iffailed)if:failure()uses:actions/upload-artifact@v3with:name:black-diffpath:| black_diff.txtretention-days:7auto-format:if:github.event_name=='push'&&github.ref=='refs/heads/main'runs-on:ubuntu-lateststeps:-uses:actions/checkout@v3with:token:${{secrets.GITHUB_TOKEN}}-name:SetupPythonuses:actions/setup-python@v4with:python-version:'3.10'-name:InstallBlackrun:pipinstallblack-name:Formatcoderun:black.-name:Commitchangesuses:stefanzweifel/git-auto-commit-action@v4with:commit_message:"style: apply black formatting"branch:${{github.head_ref}}# .gitlab-ci.ymlstages:-lint-testvariables:PIP_CACHE_DIR:"$CI_PROJECT_DIR/.cache/pip"cache:paths:-.cache/pipbefore_script:-python-V-pipinstallvirtualenv-virtualenvvenv-sourcevenv/bin/activate-pipinstallblackblack-check:stage:lintscript:-black--check--diff.artifacts:when:on_failurepaths:-black_diff.txtexpire_in:1weekblack-format:stage:lintscript:-black.-gitdiff--exit-codeonly:-merge_requestsallow_failure:true// Jenkinsfilepipeline { agent any stages { stage('Checkout') { steps { checkout scm } } stage('Setup') { steps { sh ''' python -m venv venv source venv/bin/activate pip install black ''' } } stage('Black Check') { steps { sh ''' source venv/bin/activate black --check --diff . || true ''' } post { always { script {// 生成报告 sh ''' source venv/bin/activate black --check . > black-report.txt || true '''// 发布报告(可选) publishHTML([ reportDir:'.', reportFiles:'black-report.txt', reportName:'Black Format Check' ]) } } } } stage('Format (Optional)') { when { expression { env.BRANCH_NAME == 'develop' } } steps { sh ''' source venv/bin/activate black . # 提交格式化的代码 git config user.email "ci@example.com" git config user.name "CI Bot" git commit -am "style: apply black formatting" || true git push origin HEAD:$BRANCH_NAME || true ''' } } } post { always { cleanWs() } }}# azure-pipelines.ymltrigger:-main-developpool:vmImage:ubuntu-lateststeps:-task:UsePythonVersion@0inputs:versionSpec:'3.10'addToPath:true-script:| python -m pip install --upgrade pip pip install blackdisplayName:'Install Black'-script:| black --check --diff .displayName:'Check formatting with Black'continueOnError:true-task:PublishBuildArtifacts@1condition:failed()inputs:PathtoPublish:'black_diff.txt'ArtifactName:'black-formatting-diff'# .travis.ymllanguage:pythonpython:-"3.8"-"3.9"-"3.10"-"3.11"install:-pipinstallblackscript:-black--check--diff.after_failure:-black--diff.>black_diff.txt创建完整的 GitHub Actions 工作流:
# .github/workflows/black-ci.ymlname:BlackCI/CDon:push:paths:-'**.py'pull_request:paths:-'**.py'jobs:black:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v3-name:InstallBlackrun:pipinstallblack-name:Checkformattingid:black-checkcontinue-on-error:truerun:| black --check . > black-output.txt 2>&1 echo "status=$?" >> $GITHUB_OUTPUT-name:Generatediffif:steps.black-check.outputs.status!=0run:| black --diff . > black-diff.txt cat black-diff.txt-name:Createcommentif:github.event_name=='pull_request'&&steps.black-check.outputs.status!=0uses:actions/github-script@v6with:script:| const fs = require('fs'); const diff = fs.readFileSync('black-diff.txt', 'utf8'); const comment = `## Black Formatting Issues Found 🖤\n\n\`\`\`diff\n${diff}\n\`\`\``;github.rest.issues.createComment({issue_number:context.issue.number,owner:context.repo.owner,repo:context.repo.repo,body:comment});-name:Failifformattingissuesexistif:steps.black-check.outputs.status!=0run:exit1创建完整的 pre-commit 配置:
# .pre-commit-config.yamlrepos:# 基础钩子-repo:https://github.com/pre-commit/pre-commit-hooksrev:v4.4.0hooks:-id:trailing-whitespace-id:end-of-file-fixer-id:check-yaml-id:check-added-large-files-id:check-merge-conflict-id:debug-statements# Black 格式化-repo:https://github.com/psf/blackrev:23.7.0hooks:-id:blackargs:[--line-length=88]language_version:python3.10# 导入排序-repo:https://github.com/pycqa/isortrev:5.12.0hooks:-id:isortname:isort(python)args:["--profile","black","--filter-files"]# Flake8 检查-repo:https://github.com/pycqa/flake8rev:6.1.0hooks:-id:flake8args:[--max-line-length=88,--extend-ignore=E203,W503]# Mypy 类型检查-repo:https://github.com/pre-commit/mirrors-mypyrev:v1.4.1hooks:-id:mypyadditional_dependencies:[types-all]args:[--ignore-missing-imports]创建一个脚本来测试配置:
#!/bin/bash# test-black-setup.shecho"Testing Black configuration..."# 1. 检查 Black 是否安装if ! command -v black &> /dev/null; thenecho"Black is not installed. Installing..." pip install blackfi# 2. 检查配置文件if [ -f "pyproject.toml" ]; thenecho"✓ pyproject.toml found" grep -A 5 "\[tool.black\]" pyproject.tomlelseecho"✗ pyproject.toml not found"fi# 3. 检查 pre-commit 配置if [ -f ".pre-commit-config.yaml" ]; thenecho"✓ pre-commit config found" grep "black" .pre-commit-config.yamlelseecho"✗ pre-commit config not found"fi# 4. 测试格式化echo"Testing formatting on a sample file..."cat > test_format.py << EOFdef test_func(x,y,z):return x+y+zEOFblack test_format.pyecho"Formatted file:"cat test_format.py# 5. 清理rm test_format.pyecho"Test complete!"创建团队的 Black 使用规范:
# Black 代码格式化规范## 概述我们团队使用 Black 作为统一的 Python 代码格式化工具。## 配置### 项目配置 (pyproject.toml)```toml[tool.black]line-length = 88target-version = ['py310', 'py311']skip-string-normalization = false请确保你的编辑器已配置保存时自动格式化。
使用 pre-commit 确保提交的代码已格式化:
pre-commit install使用 # fmt: off 和 # fmt: on 禁用部分代码的格式化:
# fmt: offdata = [1, 2, 3,4, 5, 6]# fmt: on使用尾随逗号强制垂直格式:
# 强制垂直格式items = [1,2,3,]profile = "black"black --check . 检查问题---## 第6章:处理特殊情况### 6.1 禁用 Black 格式化#### 基本用法使用 `# fmt: off` 和 `# fmt: on` 注释来禁用格式化:```python# fmt: off# 这段代码不会被 Black 格式化data = [ 1, 2, 3, 4, 5, 6, 7, 8, 9]# fmt: on# 后续代码恢复正常格式化result = sum(data)print(f"Sum: {result}")示例 1:复杂的数学表达式
# 禁用格式化以保持数学表达式的可读性# fmt: offresult = ( (x + y) * (a - b) / (c + d) * (e - f))# fmt: on示例 2:数据定义
# 保持数据结构的特定格式# fmt: offCOLORS = {'primary': '#FF0000','secondary': '#00FF00','accent': '#0000FF',}# fmt: on示例 3:表格数据
# fmt: offUSER_TABLE = [ ['ID', 'Name', 'Email', 'Role'], [1, 'Alice', 'alice@example.com', 'Admin'], [2, 'Bob', 'bob@example.com', 'User'], [3, 'Charlie', 'charlie@example.com', 'User'],]# fmt: on对于单行,可以使用 # fmt: skip:
# 跳过这一行的格式化very_long_line = "This is a very long string that I want to keep as is"# fmt: skip# 这一行会被格式化normal_line = "This will be formatted normally"在 .gitignore 或 pyproject.toml 中排除文件:
[tool.black]exclude = '''/legacy_code/| /generated/| /third_party/| \.min\.py$'''Black 会自动处理长字符串:
# 长字符串会被自动换行long_string = "This is a very long string that exceeds the line length limit and will be wrapped automatically by Black"# 格式化后long_string = ("This is a very long string that exceeds the line length limit and will be ""wrapped automatically by Black")使用 # fmt: off 保持长字符串:
# fmt: offlong_string = "This is a very long string that I want to keep as a single line"# fmt: on# 多行字符串会保留原始格式docstring = """This is a docstringthat spans multiple linesand should keep its formatting"""# Black 不会修改 docstring 的内容# 短注释保持原样x = 10# 这是一个短注释# 长注释会被换行# This is a very long comment that goes beyond the line length limit and will be wrapped by Black# 格式化后# This is a very long comment that goes beyond the line length limit and will be# wrapped by Black# 链式调用会被格式化result = ( df.query("age > 30") .groupby("city") .agg({"salary": "mean"}) .sort_values("salary", ascending=False) .reset_index())# 或者单行(如果足够短)result = df.query("age > 30").groupby("city").agg({"salary": "mean"})# 复杂条件if ( condition1and condition2and condition3or condition4and condition5): do_something()# 三元表达式value = ("high"if score > 90else"medium"if score > 70else"low")# 嵌套列表和字典config = {"database": {"host": "localhost","port": 5432,"settings": {"pool_size": 10, "timeout": 30}, },"cache": {"backend": "redis","host": "localhost","port": 6379, },}# 函数类型注解defprocess_data( data: List[Dict[str, Union[int, float, str]]], options: Optional[Dict[str, Any]] = None,) -> Tuple[bool, str]:pass# 格式化后的类型注解会自动换行defprocess_data( data: List[Dict[str, Union[int, float, str]]], options: Optional[Dict[str, Any]] = None,) -> Tuple[bool, str]:passfrom typing import TypeVar, Generic, ListT = TypeVar("T")classContainer(Generic[T]):def__init__(self, items: List[T]) -> None: self.items = itemsdefget_items(self) -> List[T]:return self.items# 泛型限制from typing import Union, TypeVarNumber = TypeVar("Number", int, float, complex)defadd_numbers(a: Number, b: Number) -> Number:return a + bfrom typing import Protocol, TypedDictclassDrawable(Protocol):defdraw(self) -> None: ...classPoint(TypedDict): x: int y: intdefrender(item: Drawable, position: Point) -> None: item.draw()# 类型别名UserId = intUserData = Dict[str, Union[str, int, List[str]]]# 长类型别名会被换行ComplexType = Union[ List[Dict[str, Union[int, float]]], Tuple[str, ...], Callable[[int, str], bool],]创建 mixed_format.py:
# 包含各种需要特殊处理的代码import sys, os, jsonfrom typing import List,Dict,Any,Optional,Union# 长字符串LONG_TEXT = "This is a very long string that I want to keep as a single line for performance reasons"# 复杂数据结构CONFIG = {'database':{'host':'localhost','port':5432},'cache':{'backend':'redis','host':'localhost','port':6379,'options':{'max_connections':10,'timeout':30}}}defprocess_data(data:List[Dict[str,Union[int,float,str]]],options:Optional[Dict[str,Any]]=None)->Dict[str,Any]: result = {}for item in data:if item.get('type') == 'user': result[item['id']]=itemelif item.get('type') == 'product'and item.get('price',0)>100: result[item['id']]=itemreturn result# fmt: off# 这个复杂表达式应该保持原样if condition1 and condition2 and condition3 or condition4 and condition5: do_complex_operation()# fmt: onclassDataProcessor:def__init__(self,items:List[str])->None: self.items=itemsdefprocess(self)->List[str]:return [item.upper()for item in self.items if item]应用 Black 并观察变化:
black mixed_format.py创建 controlled_format.py:
# 演示格式化控制defcalculate_metrics(data):# fmt: off# 保持这个复杂计算的格式 result = ( (data['value1'] * data['factor1'] + data['value2'] * data['factor2']) / (data['total'] if data['total'] > 0else1) )# fmt: on# 这个会被格式化 metrics = {'mean': sum(data['values']) / len(data['values']),'std': (sum((x - mean) ** 2for x in data['values']) / len(data['values'])) ** 0.5 }return result, metrics# fmt: skip 可以跳过单行very_long_line = "This is a very long line that I want to keep as a single line"# fmt: skip# 多行字符串保持不变SQL_QUERY = """SELECT users.id, users.name, orders.totalFROM usersJOIN orders ON users.id = orders.user_idWHERE users.active = trueAND orders.created_at > NOW() - INTERVAL '30 days'"""创建 type_hints.py:
from typing import ( List, Dict, Tuple, Optional, Union, Callable, Any, TypeVar, Generic)from dataclasses import dataclassfrom enum import Enum# 复杂的类型别名JsonValue = Union[str, int, float, bool, None, List['JsonValue'], Dict[str, 'JsonValue']]JsonDict = Dict[str, JsonValue]# 泛型类T = TypeVar('T')K = TypeVar('K')V = TypeVar('V')classCache(Generic[K, V]):def__init__(self, maxsize: int = 100, ttl: Optional[float] = None) -> None: self._cache: Dict[K, V] = {} self._maxsize = maxsize self._ttl = ttldefget(self, key: K, default: Optional[V] = None) -> Optional[V]:return self._cache.get(key, default)defset(self, key: K, value: V) -> None:if len(self._cache) >= self._maxsize: self._cache.pop(next(iter(self._cache))) self._cache[key] = value# ProtocolclassSerializable(Protocol):defserialize(self) -> JsonDict: ... @classmethoddefdeserialize(cls, data: JsonDict) -> 'Serializable': ...# TypedDictclassUserData(TypedDict): id: int name: str email: str roles: List[str] metadata: JsonDict运行 Black 并检查结果:
black type_hints.py创建一个模拟真实项目的文件 real_world.py:
"""实际项目中的特殊场景处理"""import asyncioimport aiohttpfrom typing import List, Dict, Optionalimport logginglogger = logging.getLogger(__name__)classAPIClient:"""API 客户端,演示异步和复杂逻辑"""def__init__(self, base_url: str, timeout: int = 30): self.base_url = base_url.rstrip('/') self.timeout = timeout self.session: Optional[aiohttp.ClientSession] = Noneasyncdef__aenter__(self): self.session = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=self.timeout) )return selfasyncdef__aexit__(self, exc_type, exc_val, exc_tb):if self.session:await self.session.close()# fmt: off# 这个长 URL 构建应该保持可读性def_build_url(self, endpoint: str, params: Dict[str, str]) -> str:return (f"{self.base_url}/{endpoint.lstrip('/')}"f"?{'&'.join(f'{k}={v}'for k, v in params.items())}" )# fmt: onasyncdeffetch(self, endpoint: str, params: Optional[Dict] = None) -> Dict:"""获取数据,演示类型注解和错误处理""" params = params or {} url = self._build_url(endpoint, {k: str(v) for k, v in params.items()})try:asyncwith self.session.get(url) as response: response.raise_for_status()returnawait response.json()except aiohttp.ClientError as e: logger.error(f"Failed to fetch {url}: {e}")raiseexcept asyncio.TimeoutError as e: logger.error(f"Timeout fetching {url}: {e}")raise# fmt: off# 保持正则表达式模式的可读性EMAIL_PATTERN = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'URL_PATTERN = r'^(https?://)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*/?$'# fmt: on# 复杂的条件判断defvalidate_user_data(user_data: Dict) -> List[str]: errors = []ifnot user_data.get('email'): errors.append('Email is required')elifnot EMAIL_PATTERN.match(user_data['email']): errors.append('Invalid email format')ifnot user_data.get('age'): errors.append('Age is required')elif user_data['age'] < 0or user_data['age'] > 150: errors.append('Age must be between 0 and 150')if user_data.get('password'):if len(user_data['password']) < 8: errors.append('Password must be at least 8 characters')elifnot any(c.isupper() for c in user_data['password']): errors.append('Password must contain at least one uppercase letter')elifnot any(c.isdigit() for c in user_data['password']): errors.append('Password must contain at least one digit')return errors这个练习展示了:
作者简介:码上工坊,探索用编程为己赋能,定期分享编程知识和项目实战经验。持续学习、适应变化、记录点滴、复盘反思、成长进步。
重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,欢迎交流讨论。