{"id":19283926,"url":"https://github.com/tencentblueking/best-practices","last_synced_at":"2025-08-28T02:43:22.389Z","repository":{"id":92205695,"uuid":"532800956","full_name":"TencentBlueKing/best-practices","owner":"TencentBlueKing","description":"腾讯蓝鲸团队 多年的编程最佳实践总结，包括 Python \\ Golang 等多个语言及其相关领域","archived":false,"fork":false,"pushed_at":"2023-06-27T02:32:55.000Z","size":64,"stargazers_count":132,"open_issues_count":2,"forks_count":31,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-22T04:16:16.634Z","etag":null,"topics":["best-practices","django","python"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TencentBlueKing.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"License.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-05T08:00:48.000Z","updated_at":"2025-03-31T02:46:55.000Z","dependencies_parsed_at":"2024-11-09T21:35:56.422Z","dependency_job_id":null,"html_url":"https://github.com/TencentBlueKing/best-practices","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TencentBlueKing/best-practices","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TencentBlueKing%2Fbest-practices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TencentBlueKing%2Fbest-practices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TencentBlueKing%2Fbest-practices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TencentBlueKing%2Fbest-practices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TencentBlueKing","download_url":"https://codeload.github.com/TencentBlueKing/best-practices/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TencentBlueKing%2Fbest-practices/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272424369,"owners_count":24932893,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-28T02:00:10.768Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["best-practices","django","python"],"created_at":"2024-11-09T21:35:49.848Z","updated_at":"2025-08-28T02:43:22.362Z","avatar_url":"https://github.com/TencentBlueKing.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# 蓝鲸最佳实践 \u003c!-- omit in toc --\u003e\n\n该文档为 **[腾讯蓝鲸团队](https://bk.tencent.com/)** 多年的编程最佳实践总结，包括 Python \\ Golang 等多个语言及其相关领域。内容将跟随项目发展与语言/框架的更新不断改进。\n\n为了更方便地索引最佳实践，我们建立了一个简单的标号机制 `BBP`，你可以阅读 [BBP-0000](BBP-0000.md) 了解更多。\n\n# 目录 \u003c!-- omit in toc --\u003e\n- [Python](#python)\n  - [内置数据结构](#内置数据结构)\n    - [BBP-1001 避免魔术数字](#bbp-1001-避免魔术数字)\n    - [BBP-1002 不要预计算字面量表达式](#bbp-1002-不要预计算字面量表达式)\n    - [BBP-1003 优先使用列表推导或内联函数](#bbp-1003-优先使用列表推导或内联函数)\n  - [内置模块](#内置模块)\n    - [BBP-1004 使用 operator 模块替代简单 lambda 函数](#bbp-1004-使用-operator-模块替代简单-lambda-函数)\n      - [替代相乘函数](#替代相乘函数)\n      - [替代索引获取函数](#替代索引获取函数)\n      - [替代属性获取函数](#替代属性获取函数)\n    - [BBP-1005 logging 模块：尽量使用参数，而不是直接拼接字符串](#bbp-1005-logging-模块尽量使用参数而不是直接拼接字符串)\n    - [BBP-1006 使用 `timedelta.total_seconds()` 代替 `timedelta.seconds()` 获取相差总秒数](#bbp-1006-使用-timedeltatotal_seconds-代替-timedeltaseconds-获取相差总秒数)\n    - [BBP-1007 在协程中使用 `asyncio.sleep()` 代替 `time.sleep()`](#bbp-1007-在协程中使用-asynciosleep-代替-timesleep)\n    - [BBP-1008 当在**非测试**代码中使用 `assert` 时，妥善添加断言信息](#bbp-1008-当在非测试代码中使用-assert-时妥善添加断言信息)\n  - [生成器与迭代器](#生成器与迭代器)\n    - [BBP-1009 警惕未激活生成器的陷阱](#bbp-1009-警惕未激活生成器的陷阱)\n    - [BBP-1010 使用现代化字符串格式化方法](#bbp-1010-使用现代化字符串格式化方法)\n    - [BBP-1011 在有分支判断时使用yield要记得及时return](#bbp-1011-在有分支判断时使用yield要记得及时return)\n  - [函数](#函数)\n    - [BBP-1012 统一返回值类型](#bbp-1012-统一返回值类型)\n    - [BBP-1013 增加类型注解](#bbp-1013-增加类型注解)\n    - [BBP-1014 不要使用可变类型作为默认参数](#bbp-1014-不要使用可变类型作为默认参数)\n    - [BBP-1015 优先使用异常替代错误编码返回](#bbp-1015-优先使用异常替代错误编码返回)\n  - [面向对象编程](#面向对象编程)\n    - [BBP-1016 使用 dataclass 定义数据类](#bbp-1016-使用-dataclass-定义数据类)\n      - [在数据量较大的场景下，需要在结构的便利性和性能中做平衡](#在数据量较大的场景下需要在结构的便利性和性能中做平衡)\n  - [异常处理](#异常处理)\n    - [BBP-1017 避免含糊不清的异常捕获](#bbp-1017-避免含糊不清的异常捕获)\n  - [工具选择](#工具选择)\n    - [BBP-1018 使用 PyMySQL 连接 MySQL 数据库](#bbp-1018-使用-pymysql-连接-mysql-数据库)\n    - [BBP-1019 使用 dogpile.cache 做缓存](#bbp-1019-使用-dogpilecache-做缓存)\n    - [BBP-1020 使用Arrow来处理时间相关转换](#bbp-1020-使用arrow来处理时间相关转换)\n  - [风格建议](#风格建议)\n    - [BBP-1021 对条件判断，在不需要提前返回的情况下，尽量推荐使用正判断](#bbp-1021-对条件判断在不需要提前返回的情况下尽量推荐使用正判断)\n- [Django](#django)\n  - [DB 建模](#db-建模)\n    - [BBP-2001 如果字段的取值是一个有限集合，应使用 `choices` 选项声明枚举值](#bbp-2001-如果字段的取值是一个有限集合应使用-choices-选项声明枚举值)\n    - [BBP-2002 如果某个字段或某组字段被频繁用于过滤或排序查询，建议建立单字段索引或联合索引](#bbp-2002-如果某个字段或某组字段被频繁用于过滤或排序查询建议建立单字段索引或联合索引)\n    - [BBP-2003 变更数据表时，新增字段尽量使用 `null=True` 而不是 `default`](#bbp-2003-变更数据表时新增字段尽量使用-nulltrue-而不是-default)\n  - [DB 查询](#db-查询)\n    - [BBP-2004 使用 .exists() 判断数据是否存在](#bbp-2004-使用-exists-判断数据是否存在)\n    - [BBP-2005 使用 .count() 查询数据条目数](#bbp-2005-使用-count-查询数据条目数)\n    - [BBP-2006 避免 N + 1 查询](#bbp-2006-避免-n--1-查询)\n    - [BBP-2007 如果仅查询外键 ID，则无需进行连表操作。使用 `外键名_id` 可直接获取](#bbp-2007-如果仅查询外键-id则无需进行连表操作使用-外键名_id-可直接获取)\n    - [BBP-2008 避免查询全部字段](#bbp-2008-避免查询全部字段)\n    - [BBP-2009 避免在循环中进行数据库操作](#bbp-2009-避免在循环中进行数据库操作)\n    - [BBP-2010 避免隐式的子查询](#bbp-2010-避免隐式的子查询)\n    - [BBP-2011 `update_or_create` 与 `get_or_create` 通过 defaults 参数避免全表查询](#bbp-2011-update_or_create-与-get_or_create-通过-defaults-参数避免全表查询)\n    - [BBP-2012 `update_or_create` 与 `get_or_create` 查询条件的字段必须要有唯一性约束](#bbp-2012-update_or_create-与-get_or_create-查询条件的字段必须要有唯一性约束)\n    - [BBP-2013 如果查询集只用于单次循环，建议使用 `iterator()` 保持连接查询](#bbp-2013-如果查询集只用于单次循环建议使用-iterator-保持连接查询)\n    - [BBP-2014 针对数据库字段更新尽量使用 `update_fields`](#bbp-2014-针对数据库字段更新尽量使用-update_fields)\n    - [BBP-2015 使用 Django Extra 查询时，需要使用内置的字符串表达](#bbp-2015-使用-django-extra-查询时需要使用内置的字符串表达)\n    - [BBP-2016 善用 bulk\\_create/bulk\\_update 减少批量数据库操作耗时](#bbp-2016-善用-bulk_createbulk_update-减少批量数据库操作耗时)\n    - [BBP-2017 当 MySQL 版本较低时（\\\u003c5.7)，谨慎使用 DateTimeField 进行排序](#bbp-2017-当-mysql-版本较低时57谨慎使用-datetimefield-进行排序)\n- [Golang](#golang)\n    - [BBP-3001 channel空间设定为1或者阻塞](#bbp-3001-channel空间设定为1或者阻塞)\n    - [BBP-3002 除for循环以外，不要在代码块初始化中使用:=](#bbp-3002-除for循环以外不要在代码块初始化中使用)\n    - [BBP-3003 channel接受使用两段式](#bbp-3003-channel接受使用两段式)\n    - [BBP-3004 不能通过取出来的值来判断 key 是不是在 map 中](#bbp-3004-不能通过取出来的值来判断-key-是不是在-map-中)\n    - [BBP-3005 接口类型转换应使用两段式](#bbp-3005-接口类型转换应使用两段式)\n    - [BBP-3006 定义常量时，使用自增的方式定义](#bbp-3006-定义常量时使用自增的方式定义)\n- [DRF](#drf)\n    - [BBP-4001 在数据量较大的场景下，避免使用 Model Serializer](#bbp-4001-在数据量较大的场景下避免使用-model-serializer)\n- [Redis](#redis)\n    - [BBP-5001 善用pipeline和redis新特性](#bbp-5001-善用pipeline和redis新特性)\n    - [BBP-5002 善用 lua 脚本保证多个 Redis 操作的原子性](#bbp-5002-善用-lua-脚本保证多个-redis-操作的原子性)\n\n# Python\n\nPython 🐍 最佳实践、优化思路、工具选择。\n\n\n## 内置数据结构\n\n### BBP-1001 避免魔术数字\n\n不要在代码中出现 [Magic Number](https://en.wikipedia.org/wiki/Magic_number_(programming))，常量应该使用 Enum 模块来替代。\n\n```python\n# BAD\nif cluster_type == 1:\n    pass\n\n\n# GOOD\nfrom enum import Enum\n\nClass BCSType(Enum):\n    K8S = 1\n    Mesos = 2\n\nif cluster_type == BCSType.K8S.value:\n    pass\n```\n\n\n### BBP-1002 不要预计算字面量表达式\n\n如果某个变量是通过简单算式得到的，应该保留算式内容。不要直接使用计算后的结果。\n\n```python\n# BAD\nif delta_seconds \u003e 950400:\n    return\n\n# GOOD\nif delta_seconds \u003e 11 * 24 * 3600:\n    return\n```\n\n### BBP-1003 优先使用列表推导或内联函数\n\n使用列表推导或内联函数能够清晰知道要生成一个列表，并且更简洁\n\n```python\n# BAD\nlist_two = []\nfor v in list_one:\n    if v[0]:\n        new_list.append(v[1])\n\n# GOOD one\nlist_two = [v[1] for v in list_one if v[0]]\n\n# GOOD two\nlist_two = list(filter(lambda x: x[0], list_one))\n```\n\n## 内置模块\n\n### BBP-1004 使用 operator 模块替代简单 lambda 函数\n\n在很多场景下，`lambda` 函数都可以用 `operator` 模块来替代，后者效率更高。\n\n#### 替代相乘函数\n\n```python\n# BAD\nproduct = reduce(lambda x, y: x * y, numbers, 1)\n\n# GOOD\nfrom operator import mul\nproduct = reduce(mul, numbers, 1)\n```\n\n#### 替代索引获取函数\n\n```python\n# BAD\nrows_sorted_by_city = sorted(rows, key=lambda row: row['city'])\n\n# GOOD\nfrom operator import itemgetter\nrows_sorted_by_city = sorted(rows, key=itemgetter('city'))\n```\n\n#### 替代属性获取函数\n\n```python\n# BAD\nproducts_by_quantity = sorted(products, key=lambda p: p.quantity)\n\n# GOOD\nfrom operator import attrgetter\nproducts_by_quantity = sorted(products, key=attrgetter('quantity'))\n```\n\n### BBP-1005 logging 模块：尽量使用参数，而不是直接拼接字符串\n\n在使用 `logging` 模块打印日志时，请尽量 **不要** 在第一个参数内拼接好日志内容（不论是使用何种方式）。正确的做法是只在第一个参数提供模板，参数由后面传入。\n\n在大规模循环打印日志时，这样做效率更高。\n\n参考：https://docs.python.org/3/howto/logging.html#optimization\n\n```python\n# BAD\nlogging.warning(\"To iterate is %s, to recurse %s\" % (\"human\", \"divine\"))\n\n# BAD，但并非不可接受\nlogging.warning(f\"To iterate is {human}, to recurse {divine}\")\n\n# GOOD\nlogging.warning(\"To iterate is %s, to recurse %s\", \"human\", \"divine\")\n```\n\n### BBP-1006 使用 `timedelta.total_seconds()` 代替 `timedelta.seconds()` 获取相差总秒数\n\n```python\nfrom datetime import datetime\ndt1 = datetime.now()\ndt2 = datetime.now()\n\n# BAD\nprint((dt2 - dt1).seconds)\n\n# GOOD\nprint((dt2 - dt1).total_seconds())\n```\n\n在源码中，seconds 的计算方式为：days, seconds = divmod(seconds, 24*3600)\n\n表达式右侧 seconds 是总秒数，被一天的总秒数取模得到 seconds\n\n```python\n@property\ndef seconds(self):\n    \"\"\"seconds\"\"\"\n    return self._seconds\n\n# in the `__new__`, you can find the `seconds` is modulo by the total number of seconds in a day\ndef __new__(cls, days=0, seconds=0, microseconds=0,\n            milliseconds=0, minutes=0, hours=0, weeks=0):\n    seconds += minutes*60 + hours*3600\n    # ...\n    if isinstance(microseconds, float):\n        microseconds = round(microseconds + usdouble)\n        seconds, microseconds = divmod(microseconds, 1000000)\n        # ! 👇\n        days, seconds = divmod(seconds, 24*3600)\n        d += days\n        s += seconds\n    else:\n        microseconds = int(microseconds)\n        seconds, microseconds = divmod(microseconds, 1000000)\n        # ! 👇\n        days, seconds = divmod(seconds, 24*3600)\n        d += days\n        s += seconds\n        microseconds = round(microseconds + usdouble)\n    # ...\n```\n\n`total_seconds` 可以得到一个准确的差值：\n```python\ndef total_seconds(self):\n    \"\"\"Total seconds in the duration.\"\"\"\n    return ((self.days * 86400 + self.seconds) * 10**6 +\n        self.microseconds) / 10**6\n```\n\n### BBP-1007 在协程中使用 `asyncio.sleep()` 代替 `time.sleep()`\n\n`time.sleep()` 是阻塞的，协程执行到此会导致整体事件循环卡住\n\n`asyncio.sleep()` 非阻塞，事件循环将运行其他逻辑\n\n```python\nimport time\nimport asyncio\n\n# BAD\nasync def execute_task(task_id: int):\n    print(f\"task[{task_id}] hello\")\n    time.sleep(1)\n    print(f\"task[{task_id}] world\")\n\n# GOOD\nasync def execute_task(task_id: int):\n    print(f\"task[{task_id}] hello\")\n    await asyncio.sleep(1)\n    print(f\"task[{task_id}] world\")\n```\n\n上述例子将通过以下代码执行：\n\n```python\nimport asyncio\nasync def main():\n    await asyncio.gather(task(1), task(2))\nawait main()\n```\n\n`BAD` 将输出以下内容，`task[1]` 执行完 `hello`  后被 `time.sleep()` 阻塞\n```\ntask[1] hello\ntask[1] world\ntask[2] hello\ntask[2] world\n```\n\n`GOOD` 将输出以下内容，`task[1]` 执行完 `hello` 后，`await asyncio.sleep(1)` 将挂起 `task[1]`，开始执行 `task[2]`\n```\ntask[1] hello\ntask[2] hello\ntask[1] world\ntask[2] world\n```\n\n### BBP-1008 当在**非测试**代码中使用 `assert` 时，妥善添加断言信息\n\n```python\n\u003e\u003e\u003e assert 1 == 0\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\nAssertionError\n```\n\n在大段的日志中，单独存在的 `AssertionError` 不利于日志检索和问题定位，所以添加上可读的断言信息是更推荐的做法。\n\n```python\n# BAD\nassert \"hello\" == \"world\"\n\n# GOOD\nassert \"hello\" == \"world\", \"Hello is not equal to world\"\n```\n\n## 生成器与迭代器\n\n### BBP-1009 警惕未激活生成器的陷阱\n\n调用生成器函数后，拿到的对象是处于“未激活”状态的生成器对象。比如下面的 `get_something()` 函数，当你调用它时并不会抛出 `ZeroDivisionError` 异常：\n\n```python\n\u003e\u003e\u003e def get_something():\n...     yield 1 / 0\n...\n\u003e\u003e\u003e get_something()\n\u003cgenerator object get_something at 0x10d2301b0\u003e\n```\n\n如果对该对象使用布尔判断，将永远返回 `True`。\n\n```python\n\u003e\u003e\u003e bool(get_something())\nTrue\n```\n\n激活生成器可以使用下面这些方式：\n\n```python\n# 使用 list 内建函数\n\u003e\u003e\u003e list(get_something())\nTraceback (most recent call last):\n... ...\nZeroDivisionError: division by zero\n\n# 使用 next 内建函数\n\u003e\u003e\u003e next(get_something())\nTraceback (most recent call last):\n... ...\nZeroDivisionError: division by zero\n\n# 使用 for 循环\n\u003e\u003e\u003e for i in get_something(): pass\n...\nTraceback (most recent call last):\n... ...\nZeroDivisionError: division by zero\n```\n\n### BBP-1010 使用现代化字符串格式化方法\n\n在需要格式化字符串时，请使用 [str.format()](https://docs.python.org/3/library/stdtypes.html#str.format) 或 [f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings)。\n\n```python\n# BAD\nmetric_name = '%s_clsuter_id' % data_id\n\n# GOOD\nmetric_name = '{}_cluster_id'.format(data_id)\n\n# BEST\nmetric_name = f'{data_id}_cluster_id'\n```\n\n当需要重复键入格式化变量名时，使用 `f-strings`。\n\n```python\n# BAD\n\"{a}-{b}-{c}-{d}\".format(a=a, b=b, c=c, d=d)\n\n# GOOD\nf\"{a}-{b}-{c}-{d}\"\n```\n\n\n### BBP-1011 在有分支判断时使用yield要记得及时return\n\n有时我们常会把 yield 类同于 return，为了减少一些循环次数，我们常会把 return 直接替换成 yield，然而这时候就很容易出现 bug，尤其是当一个函数中有多个条件分支时。\n\n```python\n# BAD\ndef test(a: int):\n    if a \u003e 1:\n        yield \"a\"\n    yield \"b\"\n\nlist(test(2)) # 预期是 [\"a\"]， 实际是 [\"a\", \"b\"]，因为 yield 仅是让度 CPU 而非结束当前函数\n\n# GOOD\ndef test(a: int):\n    if a \u003e 1:\n        yield \"a\"\n        return # 控制好函数的生命周期，以达到预期效果\n    yield \"b\"\n```\n\n## 函数\n\n### BBP-1012 统一返回值类型\n\n单个函数应该总是返回同一类数据。\n\n```python\n# BAD\n# 可能返回 bool 或者 None\ndef is_odd(num):\n    if num % 2 \u003e 0:\n        return True\n\n\n# GOOD\ndef is_odd(num):\n    if num % 2 \u003e 0:\n        return True\n    return False\n```\n\n### BBP-1013 增加类型注解\n\n对变量和函数的参数返回值类型做注解，有助于通过静态检查减少类型方面的错误。\n\n```python\n# BAD\ndef greeting(name):\n    return 'Hello ' + name\n\n# GOOD\ndef greeting(name: str) -\u003e str:\n    return 'Hello ' + name\n```\n\n### BBP-1014 不要使用可变类型作为默认参数\n\n函数作为对象定义的时候就被执行，默认参数是函数的属性，它的值可能会随着函数被调用而改变。\n\n```python\n# BAD\ndef foo(li: list = []):\n    li.append(1)\n    print(li)\n\n# GOOD\ndef foo(li : Optional[list] = None):\n    li = li or []\n    li.append(1)\n    print(li)\n```\n调用两次foo函数后，不同的输出结果：\n```python\n# BAD\n[1]\n[1,1]\n\n# GOOD\n[1]\n[1]\n```\n\n### BBP-1015 优先使用异常替代错误编码返回\n\n当函数需要返回错误信息时，以抛出异常优先。\n\n```python\n# BAD\ndef disable_agent(ip):\n    if not IP_DATA.get(ip):\n        return {\"code\": ErrorCode.UnknownError, \"message\": \"没有查询到 IP 对应主机\"}\n\n\n# GOOD\ndef disable_agent(ip):\n    \"\"\"\n    :raises: 当找不到对应信息时，抛出 UNABLE_TO_DISABLE_AGENT_NO_MATCH_HOST\n    \"\"\"\n    if not IP_DATA.get(ip):\n        raise ErrorCode.UNABLE_TO_DISABLE_AGENT_NO_MATCH_HOST\n```\n\n## 面向对象编程\n\n\n### BBP-1016 使用 dataclass 定义数据类\n\n对于需要在初始化阶段设置很多属性的数据类，应该使用 dataclass 来简化代码。但同时注意不要滥用，比如一些实例化参数少、没有太多“数据”属性的类仍然应该使用传统 `__init__` 方法。\n\n```python\n# BAD\nclass BcsInfoProvider:\n    def __init__(self, project_id, cluster_id, access_token , namespace_id, context):\n        self.project_id = project_id\n        self.cluster_id = cluster_id\n        self.namespace_id = namespace_id\n        self.context = context\n\n# GOOD\nfrom dataclasses import dataclass\n\n@dataclass\nclass BcsInfoProvider:\n    project_id: str\n    cluster_id: str\n    access_token: str\n    namespace_id: int\n    context: dict\n```\n#### 在数据量较大的场景下，需要在结构的便利性和性能中做平衡\n\n很多场景下，我们会得到比较大的原始数据（比如数万个嵌套的 `dict`），为了更便利地操作这些数据，往往会选择通过 `class` 进行实例化，但基于 Python 孱弱的 CPU 计算性能，这一操作可能会耗时过于久。\n\n所以需要在两方面做平衡：\n- 保持原始数据，获得最好的性能，但是不方便操作\n- 使用 `NamedTuple` 类似的结构，获得一定的结构便利性，但相较于原始数据，会牺牲一定性能\n- 使用 `dataclass` 或者 `class` 等方式，保证最大的结构便利性，但会非常影响性能\n\n\n## 异常处理\n\n### BBP-1017 避免含糊不清的异常捕获\n\n不要捕获过于基础的异常类，比如 Exception / BaseException，捕获这些会扩大处理的异常范围，容易隐藏其他本来不应该被捕获的问题。\n\n尽量捕获预期内可能出现的异常。\n\n```python\n# BAD\ntry\n    func_code()\nexcept:\n    # your code\n\n# GOOD\ntry:\n    func_code()\nexcept CustomError as error:\n    # your code\nexcept Exception as error:\n    logger.exception(\"some error: %s\", error)\n```\n\n## 工具选择\n\n### BBP-1018 使用 PyMySQL 连接 MySQL 数据库\n\n建议使用纯 Python 实现的 [PyMySQL](https://github.com/PyMySQL/PyMySQL) 模块来连接 MySQL 数据库。如果需要在项目中完全替代 MySQL-python 模块，可以使用模块提供的猴子补丁功能：\n\n```python\nimport pymysql\npymysql.install_as_MySQLdb()\n\n# 如果版本号无法通过框架检查，可以在导入模块后动态修改\nsetattr(pymysql, 'version_info', (1, 2, 6, \"final\", 0))\n```\n\n### BBP-1019 使用 dogpile.cache 做缓存\n\n`dogpile.caches` 扩展性强，提供了一套可以对接多中后端存储的缓存 API，推荐作为项目中缓存的基础库。\n\n样例代码：\n\n```python\nfrom dogpile.cache import make_region\n\nregion = make_region().configure('dogpile.cache.redis')  # 其他参数官方文档\n\n\n@region.cache_on_arguments(expiration_time=3600)\ndef get_application(username):\n    # your code\n\n\n@region.cache_on_arguments(expiration_time=3600, function_key_generator=ignore_access_token) # function_key_generator 参考官方文档\ndef get_application(access_token, username):\n    # your code\n```\n\n### BBP-1020 使用Arrow来处理时间相关转换\n\n如果想要转换一个时间，可以将任意对象扔给arrow，然后转成对应的函数格式\n\n常见的时间格式:\n\n  - object: datetime对象(`datetime.datetime.now()`)，区分时区\n  - int: 整数timestamp(`int(time.time())`)，不区分时区\n  - string: 代表时间的字符串(`2022-11-08 22:57:22`)(`str(datetime.datetime.now())`)，区分时区\n\n这些对象统一都可以扔给arrow.get(任意对象)，得到一个arrow对象`arr`\n\n  - 转成datetime对象: arr.\n\n    ```python\n    # 带时区\n    In [18]: arr.datetime\n    Out[18]: datetime.datetime(2022, 11, 8, 22, 57, 22, 171057, tzinfo=tzlocal())\n\n    # 不带时区\n    In [19]: arr.naive\n    Out[19]: datetime.datetime(2022, 11, 8, 22, 57, 22, 171057)\n    ```    \n\n  - 转成timestamp整数: \n  \n    ```python\n    In [20]: arr.timestamp\n    Out[20]: 1667919442\n    ```\n\n  - 转成字符串: \n\n    ```python\n    In [21]: arr.strftime(\"%Y-%m-%d %H:%M:%S\")\n    Out[21]: '2022-11-08 22:57:22'\n    ```\n\n最佳实践（普通时间 → Unix时间戳(Unix timestamp)）\n\n```python\n# BAD\nIn [22]:  int(time.mktime(time.strptime('2022-11-08 22:57:22+0800', '%Y-%m-%d %H:%M:%S%z')))\nOut[22]: 1667919442\n\n# GOOD\nIn [23]: arrow.get('2022-11-08 22:57:22+0800').timestamp\nOut[23]: 1667919442\n```\n\n无须自己指定时间格式，直接转换即可\n\n\n更多请查看:\n- 文档：https://arrow.readthedocs.io/en/latest/\n- 仓库：https://github.com/arrow-py/arrow\n\n## 风格建议\n\n### BBP-1021 对条件判断，在不需要提前返回的情况下，尽量推荐使用正判断\n\n在判断结果前加否，代码可读性变差，让人的理解成本增加，后续维护也不方便\n\n```python\n# BAD\nif not validated_data[\"data_type\"] == \"cleaned\":\n    kafka_config = data_id_info[\"mq_config\"]\nelse:\n    kafka_config = data_id_info[\"result_table_list\"][0][\"shipper_list\"][0]\n\n# GOOD\nif validated_data[\"data_type\"] == \"cleaned\":\n     kafka_config = data_id_info[\"result_table_list\"][0][\"shipper_list\"][0]\nelse:\n    kafka_config = data_id_info[\"mq_config\"]  \n```\n\n# Django\n\nDjango最佳实践、优化思路。\n\n\n## DB 建模\n\n### BBP-2001 如果字段的取值是一个有限集合，应使用 `choices` 选项声明枚举值\n\n```python\nclass Students(models.Model):\n    class Gender(object):\n        MALE = 'MALE'\n        FEMALE = 'FEMALE'\n\n    GENDER_CHOICES = (\n        (Gender.MALE, \"男\"),\n        (Gender.FEMALE, \"女\"),\n    )\n\n    gender = models.IntegerField(\"性别\", choices=GENDER_CHOICES)\n```\n\n### BBP-2002 如果某个字段或某组字段被频繁用于过滤或排序查询，建议建立单字段索引或联合索引\n\n```python\n# 字段索引：使用 db_index=True 添加索引\ntitle = models.CharField(max_length=255, db_index=True)\n\n# 联合索引：将多个字段组合在一起建立索引\nclass Meta:\n    index_together = ['field_name_1', 'field_name_2']\n\n# 联合唯一索引：将多个组合在一起的索引，并且字段的组合值唯一\nclass Meta:\n    unique_together = ('field_name_1', 'field_name_2')\n```\n\n### BBP-2003 变更数据表时，新增字段尽量使用 `null=True` 而不是 `default`\n\n```python\n# BAD\nnew_field = models.CharField(default=\"foo\")\n\n# GOOD\nnew_field = models.CharField(null=True)\n```\n\n前者将会在 `migrate` 操作时对已存在的数据批量刷新，对现有数据库带来不必要的影响。\n\n参考：https://pankrat.github.io/2015/django-migrations-without-downtimes/\n\n\n## DB 查询\n\n### BBP-2004 使用 .exists() 判断数据是否存在\n\n如果要查询记录是否存在，建议使用 `.exists()` 方法。该方法将会往数据库发起一条设置了 `LIMIT 1` 的查询语句，效率最佳。\n\n```python\n# BAD\n# 将会查询表中所有结果，效率低\nif Foo.objects.filter(name='test'):\n    # Do something\n\n# GOOD\nif Foo.objects.filter(name='test').exists():\n    # Do something\n```\n\n### BBP-2005 使用 .count() 查询数据条目数\n\n如果要统计数据条目数，建议使用使用 `.count()` 方法。该方法将会往数据库发起一条 `SELECT count(*)` 查询语句。\n\n```python\n# BAD\n# 将查询表中所有内容，耗费大量内存和 CPU\ncount = len(Foo.objects.all())\n\n# GOOD\ncount = Foo.objects.count()\n```\n\n### BBP-2006 避免 N + 1 查询\n\n可使用`select_related`提前将关联表进行 join，一次性获取相关数据，many-to-many 的外键则使用`prefetch_related`\n\n```python\n# select_related\n\n# Bad\n# 由于 ORM 的懒加载特性，在执行 filter 操作时，并不会将外键关联表的字段取出，而是在使用时，实时查询。这样会产生大量的数据库查询操作\nstudents = Student.objects.all()\nstudent_in_class = {student.name: student.cls.name for student in students}\n\n# Good\n# 使用 select_related 可以避免 N + 1 查询，一次性将外键字段取出\nstudents = Student.objects.select_related('cls').all()\nstudent_in_class = {student.name: student.cls.name for student in students}\n# prefetch_related\n\n# Bad\narticles = Article.objects.filter(id__in=(1,2))\nfor item in articles:\n    # 会产生新的数据库查询操作\n    item.tags.all()\n\n# Good\narticles = Article.objects.prefetch_related(\"tags\").filter(id__in=(1,2))\nfor item in articles:\n    # 不会产生新的数据库查询操作\n    item.tags.all()\n```\n\n### BBP-2007 如果仅查询外键 ID，则无需进行连表操作。使用 `外键名_id` 可直接获取\n\n```python\n# 获取学生的班级ID\nstudent = Student.objects.first()\n\n# Bad: 会产生一次关联查询\ncls_id = student.cls.id\n\n# Good: 不产生新的查询\ncls_id = student.cls_id\n```\n\n### BBP-2008 避免查询全部字段\n可使用`values`, `values_list`, `only`, `defer`等方法进行过滤出需要使用的字段。\n\n```python\n# 仅获取学生姓名的列表\n\n# Bad\nstudents = Student.objects.all()\nstudent_names = [student.name for student in students]\n\n# Good\nstudents = Student.objects.all().values_list('name', flat=True)\n```\n\n### BBP-2009 避免在循环中进行数据库操作\n\n尽量使用 ORM 提供的批量方法，防止在数据量变大的时候产生大量数据库连接导致请求变慢\n\n```python\n# 批量创建项目\nproject_names = ['ProjectA', 'ProjectB', 'ProjectC']\n\n# Bad\nfor project_name in project_names:\n    Project.objects.create(name=project_name)\n\n# Good\nprojects = []\nfor project_name in project_names:\n    project = Project(name=project_name)\n    projects.append(project)\nProject.objects.bulk_create(projects)\n# 批量查询项目\nproject_names = ['ProjectA', 'ProjectB', 'ProjectC']\n\n# Bad: 每次循环都产生一次新的查询\nprojects = []\nfor project_name in project_names:\n    project = Project.objects.get(name=project_name)\n    projects.append(project)\n\n# Good：使用 in，只需一次数据库查询\nprojects = Project.objects.filter(name__in=project_names)\n# 批量更新项目\nproject_names = ['ProjectA', 'ProjectB', 'ProjectC']\nprojects = Project.objects.filter(name__in=project_names)\n\n# Bad: 每次循环都产生一次新的查询\nfor project in projects:\n    project.enable = True\n    project.save()\n\n# Good：批量更新，只需一次数据库查询\nprojects.update(enable=True)\n```\n\n### BBP-2010 避免隐式的子查询\n\n```python\n# 查询符合条件的组别中的人员\n\n# Bad: 将查询集作为下一个查询的过滤条件，因此产生了子查询。IN 语句中的子查询在外层查询的每一行中都会被执行一次，复杂度为 O(n^2)\ngroups = Group.objects.filter(type=\"typeA\")\nmembers = Member.objects.filter(group__in=groups)\n\n# Good: 以确定的数据作为过滤条件，避免子查询\ngroup_ids = Group.objects.filter(type=\"typeA\").values_list('id', flat=True)\nmembers = Member.objects.filter(group__id__in=list(group_ids))\n```\n\n### BBP-2011 `update_or_create` 与 `get_or_create` 通过 defaults 参数避免全表查询\n\n使用 `update_or_create` 与 `get_or_create` 时，需要将 **查询字段** 和 **更新字段** 做区分：\n\n- 前者放在方法参数中，会被 Django 当作查询条件判断是否已有记录\n- 后者应该被放入 `defaults` 参数中，否则将会被当作查询条件，容易触发全表查询\n\n\n```python\n# BAD\nModelA.objects.update_or_create(\n    field_1=\"field_1\",\n    field_2=\"field_2\",\n    field_3=\"field_3\",\n)\n\n# GOOD\nModelA.objects.update_or_create(\n    field_1=\"field_1\",\n    defaults={\n        \"field_2\": \"field_2\",\n        \"field_3\": \"field_3\",\n    }\n```\n\n### BBP-2012 `update_or_create` 与 `get_or_create` 查询条件的字段必须要有唯一性约束\n\n在并发请求的情况下，`get_or_create` 并不能保证记录的唯一性，会存在重复创建的情况。因此使用此方法前，需要确定用于存在性查询的字段是否设置了DB级别的唯一性约束。\n\n```python\n# BAD\n# models.py\nclass Topic(models.Model):\n    \"\"\"\n    模型定义\n    \"\"\"\n    username = models.CharField(max_length=32)\n    title = models.CharField(max_length=128)\n\n\n# views.py\ndef view_func(request):\n    # 并发请求场景下可能会出现重复记录\n    Topic.objects.get_or_create(username=\"foo\", title=\"bar\")\n\n\n# GOOD\n# models.py\nclass Topic(models.Model):\n    \"\"\"\n    模型定义\n    \"\"\"\n    username = models.CharField(max_length=32)\n    title = models.CharField(max_length=128)\n\n    class Meta:\n        # 增加 username 和 title 字段联合唯一性约束\n        unique_together = (\"username\", \"title\")\n\n\n# views.py\ndef view_func(request):\n    # 存在DB级别的唯一性约束，能够保证不会创建重复记录\n    Topic.objects.get_or_create(username=\"foo\", title=\"bar\")\n\n```\n\n### BBP-2013 如果查询集只用于单次循环，建议使用 `iterator()` 保持连接查询\n\n当查询结果有很多对象时，QuerySet 的缓存行为会导致使用大量内存。如果你需要对查询结果进行好几次循环，这种缓存是有意义的，但是对于 QuerySet 只循环一次的情况，缓存就没什么意义了。在这种情况下，`iterator()`可能是更好的选择。\n\n```python\n# Bad\nfor task in Task.objects.all():\n    # do something\n\n# Good\nfor task in Task.objects.all().iterator():\n    # do something\n```\n\n### BBP-2014 针对数据库字段更新尽量使用 `update_fields`  \n\n如果要对数据库字段进行更新，使用 `update_fields` 避免并行 `save()` 产生数据冲突\n\n```python\n# BAD\nfoo_instance.bar_field = other_value\nfoo_instance.save()\n\n# GOOD\nfoo_instance.bar_field = other_value\nfoo_instance.save(update_fields=[\"bar_field\"])\n```\n\n同时需要注意的是，如果 `Model` 中包含 `auto_now` 字段时，需要在 `update_fields` 的列表中添加该字段，保证同时更新。\n\n### BBP-2015 使用 Django Extra 查询时，需要使用内置的字符串表达\n\n```python\n# BAD\n# 有注入风险, username 不会被转义，可以直接注入\nEntry.objects.extra(where=[f\"headline='{username}'\"])\n\n# GOOD\n# 安全，Django 会将 username 内容转义\nEntry.objects.extra(where=['headline=%s'], params=[username])\n```\n\n### BBP-2016 善用 bulk_create/bulk_update 减少批量数据库操作耗时\n\n```python\n# BAD\n## 每次都执行commit，整体耗时较长(大约25s左右)\nfor num in range(10000):\n    Record.objects.create(num=num)\n\n# GOOD\n## 统一提交数据库，耗时很短(1s以内)\ninserted_list = []\nfor num in range(10000):\n    inserted_list.append(Demo(num=num))\n\nRecord.objects.bulk_create(inserted_list)\n```\n\n同理，当 Django 版本 \u003e 2.x 时，`bulk_update` 也可以加快批量修改。\n\n```python\ntasks = [\n    Task.objects.create(name='task1', status='start', cost=1),\n    Task.objects.create(name='task2', status='start', cost=1),\n    ...\n]\n\n# BAD\nfor task in tasks:\n    task.name = f'{task.pk}-{task.name}'\n    task.save()\n\n# GOOD\nfor task in tasks:\n    task.name = f'{task.pk}-{task.name}'\nTask.objects.bulk_update(tasks, ['name'])\n```\n\n同时还有一些需要额外注意: \n- bulk_create 方法只执行一次数据库交互，这样相当于创建时间一样，并且自定字段不会在返回数据中\n- 当单次提交的对象可能过多时，可通过 `batch_size` 控制\n\n### BBP-2017 当 MySQL 版本较低时（\u003c5.7)，谨慎使用 DateTimeField 进行排序\n\n当 MySQL 版本较低时，DATETIME 类型默认是不支持 milliseconds 的，当批量创建对象时，会导致大量记录的 `auto_now_add` 字段都在同一秒，此时根据该字段是无法获得稳定的排序结果的。\n\n```python\n# BAD\nclass Foo(models.Model):\n    ...\n    foo = models.DateTimeField(auto_now_add=True)\n    ...\n\n    class Meta:\n        ordering = [\"foo\"]\n\n\n\n# GOOD\nclass Foo(models.Model):\n    ...\n    foo = models.DateTimeField(auto_now_add=True)\n    ...\n\n    class Meta:\n        # 使用自增 ID 或者其他能准确表明顺序的字段\n        ordering = [\"id\"]\n```\n\n参考：\n- https://stackoverflow.com/questions/13344994/mysql-5-6-datetime-doesnt-accept-milliseconds-microseconds\n\n\n# Golang \n\n蓝鲸监控团队的Golang实践，持续补充中...\n\n### BBP-3001 channel空间设定为1或者阻塞\n\n如果改为其他长度的channel，都需要很详细的评估设计，因此建议默认考虑长度为1或阻塞的channel\n\n```go\n// BAD\nc := make(chan int, 100)\n\n// GOOD\nc := make(chan int)\n```\n\n### BBP-3002 除for循环以外，不要在代码块初始化中使用:=\n\n如果在代码块中使用了新建变量，容易导致覆盖上层的变量而不会发现，容易引发bug\n\n```go\n// BAD \nif _, err := openFile(\"/path\") {\n   // do something\n}\n\n// GOOD \nvar err error\nif _, err = openFile(\"/path\") {\n   // do something\n}\n```\n\n### BBP-3003 channel接受使用两段式\n\n由于读取已关闭的channel会导致panic，因此要求在读取channel的代码都使用二段式，可以避免channel已关闭的导致panic\n\n```go\n// BAD\nvalue := \u003c- ch\n\n// GOOD\nvar (\n\tok bool\n)\nif _, ok = \u003c- ch; !ok {\n\t// do something when channel is closed.\n}\n```\n\n### BBP-3004 不能通过取出来的值来判断 key 是不是在 map 中\n\ngo 会返回元素对应数据类型的零值，取值操作总有值返回，不能通过取出来的值来判断 key 是不是在 map 中\n\n```go\n// BAD\nx := map[string]string{\"demo1\": \"1\", \"demo2\": \"2\"}\nif v := x[\"demo3\"]; v == \"\" {\n  fmt.Println(\"demo3 is not exist\")\n}\n\n\n// GOOD\nx := map[string]string{\"demo1\": \"1\", \"demo2\": \"2\"}\nif _, ok := x[\"demo3\"]; !ok {\n    fmt.Println(\"demo3 is not exist\")\n}\n```\n\n### BBP-3005 接口类型转换应使用两段式\n\n由于当接口(interface)类型转换为实际类型时，如果类型不正确或接口为nil，会导致panic。因此应该使用二段式或switch的方式来避免panic\n\n```go\nvar (\n  a  interface{}\n  b  int\n  ok bool\n)\n\n// BAD\nb = a.(int)\n\n// GOOD\nb, ok = a.(int)\n// or\nswitch a.(type) {\ncase int:\n    // do something when type is int\ncase float64:\n    // do something when type is float64\ndefault:\n    // Ooops, trans failed.\n}\n```\n\n### BBP-3006 定义常量时，使用自增的方式定义\n\n定义常量时，应使用`itoa`的方式由编译器协助为各个常量赋值，降低后续维护的成本\n\n```go\n// BAD\nconst (\n   Red = 0\n   Gray  = 1\n)\n\n// GOOD\nconst (\n   Red = iota\n   Gray \n)\n```\n\n# DRF\n\n### BBP-4001 在数据量较大的场景下，避免使用 Model Serializer\n\nDRF 在 3.10 版本以前，`ModelSerializer` 有较大的性能问题，用作渲染大量的数据返回可能会耗时非常久，可以考虑使用 `Serializer` 或者原生数据结构返回。\n```python\n# 当有大量 user 对象需要渲染时\n\n# BAD\nclass UserModelSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = User\n        fields = \"__all__\"\n\n# GOOD\n# 性能有所提升，同时又不会破坏 Serializer 结构\nclass UserSerializer(serializers.Serializer):\n    # 将需要的字段平铺出来\n    username = serializers.CharField()\n    ...\n\n# GOOD\n# 最快！直接返回原始结构体，但是可能需要处理多种对象\ndef serialize_user(user: User) -\u003e Dict[str, Any]:\n    ...\n    return {\n        \"username\": \"foo\",\n        ...\n    }\n```\n\n在 DRF 3.10 版本以后，`ModelSerializer` 性能有一定程度的提升，但依旧会比后两种处理慢，可以根据场景和具体测试数据选择适合的写法。\n\n参考：\n- https://hakibenita.com/django-rest-framework-slow\n\n\n# Redis\n\n### BBP-5001 善用pipeline和redis新特性\n\n在对redis的高频操作中，由于[RTT](https://en.wikipedia.org/wiki/Round-trip_delay)的存在。单点对redis的qps很大程度上受到RTT的限制。当ping响应在1ms时，单点qps最大也不会超过1k/s。\n\n```python\n# BAD\nfor item in item_list:\n    client.lpush(key, item)\n\n# GOOD （节省了RTT）\npipeline = client.pipeline()\nfor item in item_list:\n    pipeline.lpush(key, item)\npipeline.execute()\n\n# BETTER（redis2.4及更高版本）\nclient.lpush(key, *item_list)\n```\n\n### BBP-5002 善用 lua 脚本保证多个 Redis 操作的原子性\n\n如果对 Redis 的同一个 `key` 有多次操作并且希望保证操作的原子性，除了加锁的复杂操作外，可以通过将多条 Redis 操作指令封装为 lua 脚本，再通过执行 lua 脚本的方式实现。\n\n业务场景：并发场景下，检查 \"best-practices:identifier\" 的值是否为 `abc`，是的话删除。\n\n```python\n# BAD\nif redis_client.get(\"best-practices:identifier\") == \"abc\":\n    return redis_client.del(\"best-practices:identifier\")\n```\n\n上述实现的问题：并发场景下，`best-practices:identifier` 对应的值可能被修改，如果修改是在 `get` `del` 操作间隙发生，那么会导致值不为 `abc` 的 `best-practices:identifier` 被误删。\n\n通过 lua 脚本，可以将 `get` `del` 封装成原子性操作，避免上述问题的发生。\n\n```python\n# GOOD\n# lua 脚本：满足期望值将 key 删除，否则返回 0\ndel_script = \"\"\"\nif redis.call(\"get\",KEYS[1]) == ARGV[1] then\n    return redis.call(\"del\",KEYS[1])\nelse\n    return 0\nend\n\"\"\"\n\ndel_script_func = redis_client.register_script(del_script)\nreturn del_script_func(keys=[\"best-practices:identifier\"], args=[\"abc\"])\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftencentblueking%2Fbest-practices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftencentblueking%2Fbest-practices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftencentblueking%2Fbest-practices/lists"}