-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 99.4 KB
/
index.json
File metadata and controls
1 lines (1 loc) · 99.4 KB
1
[{"categories":["python-eng"],"content":"Python 工程化:loguru 日志集成","date":"2024-01-04","objectID":"/posts/python-loguru-logging/","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":" 摘要 这篇文章综合考虑标准日志模块的统一接口以及 Loguru 日志框架的简便性,采用仅以标准日志模块作为日志门面,实际日志使用 Loguru 来输出的方案,并提供了方案的具体实现。通过这种方式,项目可以使用如 YAML 等纯文本配置文件进行日志配置,日志打印时只使用标准日志模块接口,使得业务代码和具体日志实现相隔离。 日志在开发中的地位不言而喻,规范的日志一如“书同文,车同轨”一般,不仅能灵活的进行日志搜索和过滤,更能清晰的展示业务流程,成为跨系统调用的“硬通货”。在排查线上问题时,关键日志往往能提供重要线索,帮助从一团乱麻中快速定位问题,甚至能省下数小时时间。更进一步,日志也作为系统的数据资产,从海量日志中可以分析出很多有价值的业务信息。 ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:0:0","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"Python 日志方案 ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:1:0","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"Python 标准日志模块 Python 标准日志模块[1] logging 由 PEP 282 提案[2]引入,其最大的好处是提供了全 Python 环境统一的日志接口,使得所有 Python 模块的日志得以整合,也就是说除了记录应用本身的日志外,第三方模块的日志也可以一同记录,从而为应用提供更全面完备的日志信息。 标准日志模块按照层级结构组织[3],核心包括四类组件:loggers,handlers,filters 和 formatters,它们之间的关系如下图所示: 除了根据日志级别直接进行过滤外,日志输出的主体流程分为四步(详细流程可参考[4]): logger 创建日志记录,首先根据 logger 过滤器规则对日志进行筛选; logger 将筛选出的日志记录传递给对应的日志处理器 handlers 处理; 每个 handler 根据其自身的过滤器规则筛选出需要处理的日志记录; handler 将筛选出的日志记录使用 formatter 格式化后进行输出; 标准日志模块使用示例如下: \u003e\u003e\u003e import logging \u003e\u003e\u003e \u003e\u003e\u003e logging.basicConfig(level=logging.INFO) \u003e\u003e\u003e \u003e\u003e\u003e logger = logging.getLogger(__name__) \u003e\u003e\u003e \u003e\u003e\u003e logger.info(\"Hello World\") INFO:__main__:Hello World ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:1:1","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"Loguru 日志框架 Loguru [5]是最受欢迎的 Python 第三方日志框架,提供开箱即用的日志入口,同时支持彩色日志输出,使用效果如下: 在配置方式上,Loguru 摒弃了标准日志模块的 loggers,handlers 等层级结构,统一采用 add 方法,使得配置非常简单: \u003e\u003e\u003e from loguru import logger \u003e\u003e\u003e \u003e\u003e\u003e logger.add(\"out_{time}.log\", rotation=\"500 MB\") 1 \u003e\u003e\u003e logger.info(\"Hello World\") 2024-01-04 11:06:12.237 | INFO | __main__:\u003cmodule\u003e:1 - Hello World $ cat out_2024-01-04_11-06-00_178249.log 2024-01-04 11:06:12.237 | INFO | __main__:\u003cmodule\u003e:1 - Hello World 另外,Loguru 支持包含运行时变量值的堆栈打印,可以直观的看到调用上下文信息,以除零异常为例: # main.py from loguru import logger def divide(a, b): return a / b def nested(c): try: return divide(5, c) except Exception as e: logger.exception(e) if __name__ == '__main__': nested(0) $ python main.py 2024-01-04 10:44:02.288 | ERROR | __main__:nested:10 - division by zero Traceback (most recent call last): File \"main.py\", line 13, in \u003cmodule\u003e nested(0) └ \u003cfunction nested at 0x1036a1080\u003e \u003e File \"main.py\", line 8, in nested return divide(5, c) │ └ 0 └ \u003cfunction divide at 0x10364a340\u003e File \"main.py\", line 4, in divide return a / b │ └ 0 └ 5 ZeroDivisionError: division by zero 注意 生产环境日志配置应该通过 diagnose=False 参数关闭堆栈详情打印,以避免泄露敏感信息。 ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:1:2","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"日志方案选择 总体来看,Python 标准日志模块提供统一的日志接口且功能强大,但是其配置相对复杂,在日志输出样式以及对异常诊断的帮助上表现不如 Loguru;后者在一定程度上补齐了标准日志模块的短板,然而其使用具有一定的侵入性,一方面使得项目代码跟日志框架耦合,另一方面也不利于整合其他模块日志。 回忆上面介绍的日志输出主体流程,可以将 Loguru 配置为标准日志模块的 Handler。业务代码仍然通过标准日志模块打印日志,实际的日志输出通过 Loguru 执行,从而达到日志实现与业务代码隔离的目的。 这种方式非常类似于 Java 中的 slf4j,也是面向接口编程思想的实践。 ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:1:3","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"Loguru 日志集成 ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:2:0","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"基于字典的日志配置 Python 从 PEP 391 [6] 开始支持基于字典的日志配置,考虑字典是为了提供最大的扩展性,像 JSON 以及 YAML 等格式都可以转换成字典,进而也可以用于进行日志配置。 为了能使用纯文本完成日志配置,PEP 391 定义了一些特殊解析器。 Python 对象访问解析器 格式为 ext://xxx.xxx,当需要使用到 Python 系统路径中的对象时使用,最常用的例子是引用标准控制台输出流,以 YAML 格式为例: handlers: console: class: logging.StreamHandler formatter: brief level: INFO stream: ext://sys.stdout 除开 ext:// 前缀的其他部分将等效使用 import 导入使用。 日志配置访问解析器 格式为 cfg://xxx.xxx,当需要引用当前日志配置中的配置节点时使用,支持通过 . 嵌套访问子元素、通过 [0] 访问数组内元素,使用示例如下: handlers: email: class: logging.handlers.SMTPHandler mailhost: localhost fromaddr: my_app@domain.tld toaddrs: - support_team@domain.tld - dev_team@domain.tld subject: Houston, we have a problem. custom: (): my.package.MyHandler host: 'cfg://handlers.email.mailhost' addr: 'cfg://handlers.email.toaddrs[0]' 用户自定义对象 支持使用类和 callable 对象作为工厂,类工厂使用 class 关键字配置,callable 工厂使用 () 特殊关键字配置。除 formatters、filters 等特殊配置外,同级的其他参数将作为工厂的入参传入。 类工厂的例子可以参考上一小节的 SMTPHandler 配置,解析时会触发其构造函数调用:SMTPHandler(mailhost='localhost', fromaddr='my_app@domain.tld', toaddrs=...。 callable 工厂的例子如下: formatters: custom: (): my.package.customFormatterFactory bar: baz spam: 99.9 answer: 42 以上配置解析时会执行 my.package.customFormatterFactory(bar='baz', spam=99.9, answer=42)。 以上几个解析器已经可以覆盖掉大部分的配置场景了。 ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:2:1","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"Loguru 日志处理器实现 可以通过配置 Loguru 拦截器的方式拦截标准日志模块输出[7],流程示意如下图所示: Loguru 支持通过 logger.configure() 方法完成日志配置,所有配置参数可通过 LoguruInterceptHandler 构造函数获取,而构造函数可以通过标准日志模块字典配置的类工厂引入。 yaml 配置样例 使用类似下面的 YAML 配置初始化标准日志模块,期望将 loguru_config 属性传递给 logger.configure() 完成 loguru 配置,要达到的效果如下: 通过 loguru_format 配置日志输出格式; 配置三个日志输出项,分别为 stdout、stderr 以及日志文件 file.log; 使用 loguru_logging.compact_name_format 函数进行格式化,其作用是压缩日志中的路径,比如 long.module.dir.log 压缩后输出为 l.m.dir.log; 支持 lambda:// 前缀声明 lambda 表达式,达到 stdout 不输出 ERROR 级别的效果; 日志文件 file.log 每天零点压缩归档,压缩格式为 tar.gz; # logging.yml # https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema version: 1 disable_existing_loggers: false root: handlers: - loguru level: INFO handlers: loguru: class: loguru_logging.LoguruInterceptHandler # constructor param starts with loguru_ loguru_format: \"\u003cgreen\u003e{time:YYYY-MM-DD HH:mm:ss.SSS}\u003c/green\u003e | \u003clevel\u003e{level: \u003c8}\u003c/level\u003e | \u003ccyan\u003e{name}\u003c/cyan\u003e:\u003ccyan\u003e{function}\u003c/cyan\u003e:\u003ccyan\u003e{line}\u003c/cyan\u003e - \u003clevel\u003e{message}\u003c/level\u003e\" loguru_config: # https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.configure handlers: # param: https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add - sink: ext://sys.stdout level: INFO format: ext://loguru_logging.compact_name_format filter: 'lambda://record:record[\"level\"].no \u003c logging.ERROR' - sink: ext://sys.stderr level: ERROR format: ext://loguru_logging.compact_name_format - sink: \"file.log\" rotation: \"00:00\" compression: \"tar.gz\" level: INFO format: ext://meta_repository.loguru_logging.compact_name_format 完整的配置支持可参考基于字典的标准日志模块配置[6]以及 logger.configure() API 文档[8]。 拦截器实现 接下来基于示例 YAML 配置文件的声明,进行拦截器实现,主要包括: LoguruInterceptHandler 类构造函数接收配置参数,并在构造函数中完成日志配置; 继承 logging.config.BaseConfigurator 实现 loguru 配置类 LoguruDictConfigurator,以复用路径解析、 ext:// 前缀等功能; 参考 logger.configure() api 文档,在 LoguruDictConfigurator.configure 方法中构造 loguru 配置参数; 扩展 value_converters 支持 lambda:// 前缀表达式; 实现 compact_name_format 动态格式化函数,对 name 进行压缩; 具体实现如下: import inspect import logging import os import re from logging.config import BaseConfigurator from loguru import logger # _global_loguru_format may be changed by LoguruInterceptHandler _global_loguru_format = os.getenv( \"LOGURU_FORMAT\", \"\u003cgreen\u003e{time:YYYY-MM-DD HH:mm:ss.SSS}\u003c/green\u003e | \" \"\u003clevel\u003e{level: \u003c8}\u003c/level\u003e | \" \"\u003ccyan\u003e{name}\u003c/cyan\u003e:\u003ccyan\u003e{function}\u003c/cyan\u003e:\u003ccyan\u003e{line}\u003c/cyan\u003e - \u003clevel\u003e{message}\u003c/level\u003e\", ) def compact_name_format(record) -\u003e str: \"\"\" loguru dynamic formatter :param record: log record :return: \"\"\" compact_name = compact_path(record[\"name\"]) def format_name(match): return match.group().format_map({\"name\": compact_name}) return re.sub(r\"{name(:.*?)?}\", format_name, _global_loguru_format) + \"\\n{exception}\" class LoguruInterceptHandler(logging.Handler): \"\"\"intercept standard logging messages toward loguru https://github.com/Delgan/loguru#entirely-compatible-with-standard-logging \"\"\" def __init__(self, loguru_config: dict = None, loguru_format: str = None): super().__init__() self.loguru_config = loguru_config if loguru_format: global _global_loguru_format _global_loguru_format = loguru_format self.configure_loguru() def emit(self, record: logging.LogRecord) -\u003e None: # Get corresponding Loguru level if it exists. level: str | int try: level = logger.level(record.levelname).name except ValueError: level = record.levelno # Find caller from where originated the logged message. frame, depth = inspect.currentframe(), 0 while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__): frame = frame.f_back depth += 1 logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) def configure_loguru(self): LoguruDictConfigurator(self.loguru_config).configure() class LoguruDictConfigurator(BaseConfigurator): value_converters = BaseConfigurator.value_converters | { \"lambda\": \"lambda_convert\", } # https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.configure def configure(self): loguru_config = {} conf","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:2:2","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"总结 具体实现看起来比较复杂,但实际上只是把复杂的事情做一次,后续简单的事情重复 n 次,所获得的收益还是值得的。 使用这种配置方式,只需要在项目根目录配置好对应的 logging.yml,然后在项目入口完成日志配置,比如: import logging.config import yaml # 初始化日志配置文件 with open('logging.yml', \"r\") as conf: conf_dict = yaml.load(conf, Loader=yaml.FullLoader) logging.config.dictConfig(conf_dict) if __name__ == '__main__': app.run() 其他模块通过标准日志模块打印日志即可,如: import logging logger = logging.getLogger(__name__) def demo_function(): logger.info(\"This is a standard log but will log by loguru.\") ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:3:0","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["python-eng"],"content":"参考资料 [1]. Logging facility for Python, Python Docs [2]. PEP 282 – A Logging System [3]. Advanced Logging Tutorial, Python Docs [4]. Logging Flow, Python Docs [5]. Features of Loguru [6]. PEP 391 – Dictionary-Based Configuration For Logging [7]. loguru compatible with standard logging [8]. logger.configure() api of Loguru ","date":"2024-01-04","objectID":"/posts/python-loguru-logging/:4:0","tags":["Python"],"title":"Python 工程化:Loguru 日志集成","uri":"/posts/python-loguru-logging/"},{"categories":["tools"],"content":"自动化构建工具 go-task 使用介绍。","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":" 摘要 这篇文章介绍自动化构建工具 go-task 的使用,涵盖工具安装、基本语法规则以及进阶使用,另外对在 Windows 平台使用进行了特殊说明。总结部分提供的 Python 虚拟环境自动构建脚本是对全文内容的综合实践,也是我真正应用到项目中,确实有带来生产效率提升的实用脚本,欢迎使用。 Task 是用 Go 语言编写的任务执行/构建工具,对比 GNU make,Task 语法规则[1]更加简单且语义化,学习成本更低,同时 Task 脚本支持跨平台执行,除了 Linux 和 Mac 外,Windows 系统通过使用 GitBash 也能完全兼容执行。使用 Task 编写构建任务、自动化脚本,可以极大提高开发协作效率。 Task 执行文件为 Taskfile,采用 yaml 格式,下面以 cowsay 为例进行快速开始演示: # Taskfile.yml version: '3' tasks: default: desc: \"This is the default task\" silent: true cmds: - cowsay \"Are you ready to go task ?\" $ task ___________________________ \u003c Are you ready to go task ? \u003e --------------------------- \\ ^__^ \\ (oo)\\_______ (__)\\ )\\/\\ ||----w | || || default 任务非必须,是 Task 不带任务参数执行时的默认选项,可以通过 task --list 或者 task -l 查看所有可执行任务。 ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:0:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"工具安装 Task 安装非常方便,参考官方安装文档[2],支持多种包管理工具,比如 Mac / Linux 平台的 Homebrew、Tea,Windows 平台的 Chocolatey、Scoop,Node 的 npm,也可以使用安装包、安装脚本或者直接从源码编译安装。以 brew 为例: $ brew install go-task $ task --version Task version: 3.32.0 ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:1:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"基本使用 Task 有非常完善的使用文档[3],可以先快速浏览,然后在实际使用时作为手册查阅。 ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"命令参数 Task 的核心是任务,对应 cmds 参数,支持多种格式[4],最常用的是执行 shell 命令或者执行其他 task。 version: '3' tasks: example: desc: example task cmds: # 执行 shell 命令 - cmd: echo \"hello world\" # 直接输入字符串与 cmd 等价 - echo \"hello world\" # 多行 shell 脚本 - | cat \u003c\u003c EOF \u003e output.txt hello world EOF # 执行其它 task - task: another another: desc: another example task cmds: - cat output.txt ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:1","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"任务依赖 除了可以使用 cmds.task 直接调用执行其他任务外,还可以通过 deps 声明任务依赖,在当前任务开始前,所有依赖会先执行完成,多个依赖项并行执行。 如下所示: version: '3' tasks: default: deps: [before_1, before_2] cmds: - echo \"after\" before_1: cmds: - echo \"before 1\" before_2: cmds: - echo \"before 2\" $ task before 2 before 1 after ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:2","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"环境变量 通过设置环境变量可以控制命令行为(比如调整 pypi 镜像)。 使用 env 设置全局环境变量,使用 Task.env 设置任务局部环境变量,环境变量使用 $ 符号访问。 如下示例中,install 任务识别全局环境变量使用清华源加速,install-test 任务识别单独配置环境变量使用阿里云源加速: version: '3' env: PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple tasks: install: desc: install requirements.txt cmds: - pip install -r requirements.txt install-test: desc: install test packages env: PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple cmds: - cmd: echo using index $PIP_INDEX_URL silent: true - pip install pytest pytest-cov ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:3","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"变量 变量在 Taskfile 脚本中使用,可以增强任务灵活度,方便任务复用。使用 vars 设置全局变量,使用 Task.vars 设置任务局部变量,变量使用 go 模板如 {{.VAR_NAME}} 访问。 变量取值按优先级从高到低依次为: Task.vars 中定义的任务局部变量值 被其他任务直接调用时在 Command.vars 中定义的变量值 通过命令行参数传入的变量值 通过 includes 导入的外部 Taskfile 中定义的全局变量值 当前 Taskfile 中定义的全局变量值 通过 includes 导入的外部 Taskfile 中定义的全局环境变量值 当前 Taskfile 中定义的全局环境变量值 通过命令行参数传入的环境变量值 系统环境变量值 通过下面的示例来演示变量取值,任务 echo 在控制台输出变量 MESSAGE 的值: # Taskfile.yml version: '3' includes: docs: Taskfile1.yml vars: MESSAGE: \"v5\" env: MESSAGE: \"v7\" tasks: do-echo: internal: true vars: MESSAGE: \"v1\" cmds: - echo {{.MESSAGE}} echo: desc: echo message cmds: - task: do-echo vars: MESSAGE: \"v2\" # Taskfile1.yml version: '3' vars: MESSAGE: \"v4\" env: MESSAGE: \"v6\" 根据顺序执行,{{.MESSAGE}} 取值按照优先级从高到低依次为 v1 到 v9,其中 v3 是命令行参数传入的变量值,v8 是命令行参数传入的环境变量值,v9 是系统环境变量值: $ export MESSAGE=v9 $ MESSAGE=v8 task echo MESSAGE=v3 v1 # 注释掉 v1 后执行 $ MESSAGE=v8 task echo MESSAGE=v3 v2 # 注释掉 v2 后执行 $ MESSAGE=v8 task echo MESSAGE=v3 v3 $ MESSAGE=v8 task echo v4 # 依次注释掉 v4,v5,v6 后执行 $ MESSAGE=v8 task echo # 结果依次为 v5,v6,v7 # 注释掉 v7 后执行 $ MESSAGE=v8 task echo v8 $ task echo v9 ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:4","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"工作目录 默认情况下,所有任务命令会在 Taskfile 所在目录执行,可以通过 dir 指定命令执行目录,比如下面的任务会在 client 目录执行客户端构建: version: '3' tasks: client:build: desc: build client dist file dir: client cmds: - yarn - yarn build ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:5","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"引用其他 Taskfile Taskfile 引用可以在多种场景中发挥作用,比方说引入通用工具类,或者根据功能不同将任务按文件进行分组等。 可以像如下示例中的 docs 一样直接使用路径进行引入,也可以像 dev 一样定义参数引入,需要注意的是,引入后的所有命令默认都在当前 Taskfile 目录下执行,除非通过 dir 参数修改引入脚本的执行目录。 # Taskfile.yml version: '3' includes: docs: docs/Taskfile.yml dev: taskfile: dev/Taskfile.yml vars: REQ_FILE: requirements-dev.txt # dev/Taskfile.yml version: '3' vars: REQ_FILE: requirements.txt tasks: build: desc: build dev environment cmds: - echo \"build with {{.REQ_FILE}}\" # docs/Taskfile.yml version: '3' tasks: build: desc: build docs cmds: - echo \"build docs\" $ task --list task: Available tasks for this project: * dev:build: build dev environment * docs:build: build docs ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:2:6","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"进阶使用 ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:3:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"动态变量 可以通过 shell 脚本在运行时动态设置变量的值。比如下面的任务会获取当前 Git 提交哈希值设置变量 IMAGE_TAG: version: '3' vars: IMAGE_TAG: sh: git rev-parse --short HEAD ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:3:1","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"非必要不执行 对于有中间产物生成的任务,可以通过设置条件判断中间产物是否需要更新,从而减少不必要的任务执行。 通过 task --force 或者 task -f 可以强制执行任务。 文件内容比对 Task 通过 sources 和 generates 指定源文件和中间产物,支持文件路径或者 glob 表达式,提供两种校验方式: checksum: 中间产物生成后,Task 记录源文件的校验和,只有当源文件校验和发生变更时任务才需要重新执行 timestamp:Task 记录最后一次任务执行时间,只有当任一源文件修改时间比最后执行任务时间新时任务才需要重新执行 默认方式为 checksum,两种校验方式可以通过观察项目根目录下的 .task 文件夹内容验证。使用示例如下: version: '3' tasks: build: cmds: - go build . sources: - ./*.go generates: - app{{exeExt}} method: checksum # or timestamp 命令执行比对 可以通过 status 运行命令测试,根据命令执行结果判断中间产物是否需要更新,这种方式具有非常大的灵活度。 比如下面的例子判断只要 directory 目录以及其下的两个文件存在,任务就不需要重复执行: version: '3' tasks: generate-files: cmds: - mkdir directory - touch directory/file1.txt - touch directory/file2.txt # test existence of files status: - test -d directory - test -f directory/file1.txt - test -f directory/file2.txt ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:3:2","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"go 模板引擎 Task 脚本在执行之前会使用 Go 模板引擎[5]进行解析,支持全部 slim-sprig 库函数[6]以及 Task 额外增加的平台相关等函数。 使用示例如下: version: '3' tasks: print-os: cmds: - echo '{{OS}} {{ARCH}}' - echo '{{if eq OS \"windows\"}}windows-command{{else}}unix-command{{end}}' # This will be path/to/file on Unix but path\\to\\file on Windows - echo '{{fromSlash \"path/to/file\"}}' ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:3:3","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"Windows 环境使用 开头介绍 Task 时说到 Windows 完全兼容执行需要依赖 GitBash,实际上 Task 使用 go 原生 sh 解析库 mvdan/sh [7]解析 shell 脚本,其提供非完全跨平台的能力,使用 GitBash 是为了补齐 Windows 不支持的部分内置 shell 命令,如 rm、mv 等,所以这里进行单独说明,Windows 平台安装使用指南如下: # 使用 管理员权限 打开 powershell # 一行命令安装 Chocolatey $ Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) # 验证 Chocolatey $ choco # 使用 Chocolatey 安装 go-task $ choco install go-task # 打开 GitBash 窗口,执行 task 命令 $ task --list ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:4:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"总结 使用 Task 将项目构建过程定义为自动化脚本,在提升工作效率的同时,也是在进行知识沉淀,类似的脚本可以复用,不断丰富自己的工具箱。同时可参考 Taskfile 规范[8],编写高质量的脚本代码。 下面是我在 Python 项目中基本都会使用到的一段脚本,主要功能包括: 根据 conda 虚拟环境目录是否存在,自动执行虚拟环境创建或者虚拟环境更新; 识别 environment.yml,requirements.txt 配置文件修改时间,判断是否需要重新执行依赖安装; 设置 PYTHONPATH 环境变量为项目根目录,遵循 Python 模块路径导入规范; 使用这段脚本后,不管是初次克隆项目还是项目代码更新,只需要简单执行 task 命令然后回车,脚本就会自动处理虚拟环境设置工作,多人协作时大家使用相同的脚本构建可以保证环境一致性。同时因为监听了配置文件修改时间,脚本判断只有在必要的时候才真正执行环境构建,多次重复执行 task 命令也不会有负担。 version: '3' vars: CONDA_PREFIX: ./venv CONDA_ENV_FILE: environment.yml PIP_REQ_FILE: requirements.txt tasks: default: desc: build or update venv cmds: - task: venv:build silent: true venv:build: desc: build conda venv when config file updated cmds: - task: venv:inner-build - task: venv:config-vars silent: true venv:inner-build: internal: true silent: true cmds: - test ! -d {{.CONDA_PREFIX}} || conda env update -f {{.CONDA_ENV_FILE}} -p {{.CONDA_PREFIX}} - test -d {{.CONDA_PREFIX}} || conda env create -f {{.CONDA_ENV_FILE}} -p {{.CONDA_PREFIX}} - touch {{.CONDA_PREFIX}} status: - test {{.CONDA_PREFIX}} -nt {{.CONDA_ENV_FILE}} - test {{.CONDA_PREFIX}} -nt {{.PIP_REQ_FILE}} venv:config-vars: desc: config environment variables of venv cmds: - conda env config vars set PYTHONPATH=\"{{.PYTHONPATH}}\" -p {{.CONDA_PREFIX}} vars: PYTHONPATH: sh: pwd ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:5:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["tools"],"content":"参考资料 [1]. Taskfile 语法规则 [2]. Task 官方安装文档 [3]. Task 官方使用指南 [4]. Task 命令参数 [5]. Task Go 模板引擎 [6]. slim-sprig 库函数 [7]. GitHub:mvdan/sh [8]. Taskfile 编写规范 ","date":"2023-12-14","objectID":"/posts/taskfile-the-alternatives-to-makefile/:6:0","tags":["Taskfile"],"title":"Makefile 平替:跨平台构建脚本 Taskfile","uri":"/posts/taskfile-the-alternatives-to-makefile/"},{"categories":["blog-build"],"content":"给博客首页增加 GitHub 提交记录贪食蛇动画。","date":"2023-11-29","objectID":"/posts/github-contribution-grid-snake/","tags":["GitHub"],"title":"GitHub 提交记录贪食蛇动画","uri":"/posts/github-contribution-grid-snake/"},{"categories":["blog-build"],"content":"网上冲浪看到一个同样使用 FixIt 主题的博客[1],首页的贪食蛇动画一下抓住了我的眼球,看到好东西当然要搬过来,一番 Google 后终于成功,来看下最终效果: 整体思路分为两步: 先通过 GitHub Action Platane/snk [2] 生成 svg 动画并上传到 GitHub 仓库; 自定义博客首页头像 css,将贪食蛇动画 svg 作为首页头像的背景图片; ","date":"2023-11-29","objectID":"/posts/github-contribution-grid-snake/:0:0","tags":["GitHub"],"title":"GitHub 提交记录贪食蛇动画","uri":"/posts/github-contribution-grid-snake/"},{"categories":["blog-build"],"content":"贪食蛇动画生成 找一个公开仓库添加 GitHub Action 工作流,第一次提交后可手动执行,定时任务等效东八区时间每天早上 5:30 和下午 17:30 执行,以保证贪食蛇动画中的提交记录更新。 name: Generate Snake Animation on: workflow_dispatch: schedule: # equal UTC/GMT+8 \"30 5,17 * * *\" - cron: \"30 9,21 * * *\" jobs: generate: permissions: contents: write runs-on: ubuntu-latest timeout-minutes: 10 steps: # https://github.com/Platane/snk - name: generate github-contribution-grid-snake.svg uses: Platane/snk/svg-only@v3 with: github_user_name: ${{ github.repository_owner }} outputs: | dist/github-contribution-grid-snake.svg dist/github-contribution-grid-snake-dark.svg?palette=github-dark # push the content of \u003cbuild_dir\u003e to a branch # the content will be available at https://raw.githubusercontent.com/\u003cgithub_user\u003e/\u003crepository\u003e/\u003ctarget_branch\u003e/\u003cfile\u003e - name: push github-contribution-grid-snake.svg to the output branch uses: crazy-max/ghaction-github-pages@v4 with: target_branch: output build_dir: dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 任务执行后,在仓库的 output 分支可以看到生成的 svg 文件: ","date":"2023-11-29","objectID":"/posts/github-contribution-grid-snake/:1:0","tags":["GitHub"],"title":"GitHub 提交记录贪食蛇动画","uri":"/posts/github-contribution-grid-snake/"},{"categories":["blog-build"],"content":"自定义首页头像 css 参考 FixIt 文档[3],可添加 assets/css/_custom.scss 文件进行样式自定义。 通过浏览器控制台定位首页头像元素: 然后添加对应 css 样式: // assets/css/_custom.scss .home .home-profile .home-avatar { background-size: 100% 100%; padding: 1rem; background-repeat: no-repeat; background-position: center top; background-image: url(https://raw.githubusercontent.com/will4j/blog-resource/output/github-contribution-grid-snake.svg); [data-theme='dark'] \u0026 { background-image: url(https://raw.githubusercontent.com/will4j/blog-resource/output/github-contribution-grid-snake-dark.svg); } } 完成收工。 ","date":"2023-11-29","objectID":"/posts/github-contribution-grid-snake/:2:0","tags":["GitHub"],"title":"GitHub 提交记录贪食蛇动画","uri":"/posts/github-contribution-grid-snake/"},{"categories":["blog-build"],"content":"参考资料 [1]. 个人博客:晴空小筑 [2]. GitHub 仓库:Platane/snk [3]. FixIt 官方文档:自定义样式 ","date":"2023-11-29","objectID":"/posts/github-contribution-grid-snake/:3:0","tags":["GitHub"],"title":"GitHub 提交记录贪食蛇动画","uri":"/posts/github-contribution-grid-snake/"},{"categories":["rust-kata"],"content":"Rust 编程练习:实现猜数字游戏。","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":" Note: Rust 卡塔系列旨在通过具体场景的编程练习学习 Rust 编程语言,结尾是相关的 Rust 知识点概要总结,附上参考资料以作扩展阅读。 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:0:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"问题描述 实现一个猜数字游戏:游戏开始前,从玩家输入的数字范围(如1到100)中随机选取一个数字作为答案;每轮游戏根据玩家的输入缩小数字范围,直到玩家猜中答案时游戏结束,统计玩家猜的总次数。 Note: Rust 官网电子书《Rust 编程语言》第二章[1]也以猜数字游戏作为示例,这个卡塔较之会稍微复杂一些,但用意都在于通过具体场景演示 Rust 基本语法。 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:1:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"测试先行 优雅的代码肯定也是易于测试的,反之,开始编码之前先思考测试场景是从功能层面对问题进行分解,有助于写出职责清晰,松耦合的代码。Rust 测试编写可参考文档[2]。 抽取猜数字游戏的两个主要功能:游戏创建(new_game)以及数字猜测(do_guess),单元测试分为基本功能用例和异常场景用例两组,可以先实现基本功能用例再实现异常场景用例: #[cfg(test)] mod tests { use crate::{do_guess, new_game}; // start of basic use cases #[test] fn new_game_ok() { let (min, max, secret_number) = new_game(\"1 100\"); assert_eq!(min, 1); assert_eq!(max, 100); assert!(secret_number \u003e= 1 \u0026\u0026 secret_number \u003c= 100); } #[test] fn do_guess_bingo() { let guess_result = do_guess(1, 100, 50, \"50\"); assert_eq!(guess_result, Ok((0, 50, 50))); } // end of basic use cases // start of exception use cases #[test] fn do_guess_wrong() { let guess_result = do_guess(1, 100, 37, \"50\"); assert_eq!(guess_result, Ok((1, 1, 49))); let guess_result = do_guess(1, 49, 37, \"25\"); assert_eq!(guess_result, Ok((-1, 26, 49))); } #[test] fn do_guess_failed() { let guess_result = do_guess(1, 100, 37, \"abc\"); assert_eq!(guess_result.unwrap_err(), \"Please input a integer number\"); let guess_result = do_guess(25, 50, 37, \"51\"); assert_eq!(guess_result.unwrap_err(), \"Number should between 25 and 50\"); } // end of exception use cases } /// 创建新的猜数字游戏,返回游戏范围以及范围内的随机数字。 /// /// # Arguments /// /// * `game_str` - 游戏创建字符串,空格分割的数字起始和结束范围。 /// /// # Examples /// ``` /// let (min, max, secret) = new_game(\"1 100\") /// ``` fn new_game(game_str: \u0026str) -\u003e (u32, u32, u32) { (1, 100, 50) } /// 进行一轮猜数字游戏,返回猜测结果以及根据结果调整过后的数字范围。 /// /// 功能包含对输入进行校验。 /// /// # Arguments /// /// * `min` - 数字范围:最小数字 /// * `max` - 数字范围:最大数字 /// * `secret_number` - 猜数字游戏答案 /// * `guess_str` - 本轮游戏输入 fn do_guess(min: u32, max: u32, secret_number: u32, guess_str: \u0026str) -\u003e Result\u003c(i8, u32, u32), String\u003e { Ok((-1, 25, 49)) } ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:2:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"代码实现 use std::cmp::Ordering; use std::io::{self, Write}; use rand::Rng; fn main() -\u003e io::Result\u003c()\u003e { println!(\"Let's Play a Number Guessing Game!\"); print!(\"New Game: \"); io::stdout().flush().unwrap(); let mut input_str = String::new(); io::stdin().read_line(\u0026mut input_str).unwrap(); // 根据用户输入创建新游戏,返回最小值、最大值和随机数字答案 let (mut min, mut max, secret_number) = new_game(\u0026input_str); // 记录猜的次数 let mut count: u32 = 0; loop { print!(\"Guess a Number between {min} and {max}: \"); io::stdout().flush().unwrap(); let mut input_str = String::new(); io::stdin().read_line(\u0026mut input_str).unwrap(); let guess_result = do_guess(min, max, secret_number, \u0026input_str); match guess_result { Ok((result, new_min, new_max)) =\u003e { print!(\"You guess {}, \", input_str.trim()); count += 1; min = new_min; max = new_max; if result == 0 { println!(\"You win with {count} guesses!\"); break; } else if result == -1 { println!(\"Too small!\"); } else { println!(\"Too big!\"); } } Err(err) =\u003e { println!(\"{err}\"); continue; } }; } Ok(()) } fn new_game(game_str: \u0026str) -\u003e (u32, u32, u32) { let range: Vec\u003cu32\u003e = game_str .split_whitespace() .map(|s| s.parse().expect(\"parse error\")) .collect(); let min = range[0]; let max = range[1]; let secret_number = rand::thread_rng().gen_range(min..=max); (min, max, secret_number) } fn do_guess(min: u32, max: u32, secret_number: u32, guess_str: \u0026str) -\u003e Result\u003c(i8, u32, u32), String\u003e { match guess_str.trim().parse::\u003cu32\u003e() { Ok(_num) =\u003e { if _num \u003c min || _num \u003e max { return Err(format!(\"Number should between {min} and {max}\")); } // 比对结果,调整数字范围 match _num.cmp(\u0026secret_number) { Ordering::Less =\u003e Ok((-1, _num + 1, max)), Ordering::Greater =\u003e Ok((1, min, _num - 1)), Ordering::Equal =\u003e Ok((0, _num, _num)), } } Err(_) =\u003e return Err(format!(\"Please input a integer number\")) } } ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:3:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"代码执行 $ cargo run --bin number-guessing-game Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/number-guessing-game` Let's Play a Number Guessing Game! New Game: 1 20 Guess a Number between 1 and 20: 10 You guess 10, Too big! Guess a Number between 1 and 9: 5 You guess 5, Too small! Guess a Number between 6 and 9: 8 You guess 8, You win with 3 guesses! ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:4:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"Rust 知识点 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"Cargo Cargo [3]是 Rust 项目的编译构建和依赖管理工具,对应配置文件 Cargo.toml [4],可通过 Cargo 命令创建项目: $ cargo new rust-kata Created binary (application) `rust-kata` package $ tree rust-kata rust-kata ├── Cargo.toml └── src └── main.rs $ cat rust-kata/Cargo.toml [package] name = \"rust-kata\" version = \"0.1.0\" edition = \"2021\" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 默认情况下,Rust 项目只能有一个 main 函数作为执行入口(如 src/main.rs),通过 Cargo bin 可额外设置。项目依赖声明在 dependencies 配置下,可通过 crates.io [5]搜索三方依赖。 rust-kata 配置猜数字游戏入口,添加随机数库依赖后配置如下: $ cat rust-kata/Cargo.toml [package] name = \"rust-kata\" version = \"0.1.0\" edition = \"2021\" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] rand = \"0.8.5\" [[bin]] name = \"number-guessing-game\" path = \"src/bin/number_guesssing_game.rs\" 程序执行方式: # 执行项目主程序 src/main.rs $ cargo run --bin rust-kata # 执行猜数字游戏程序 $ cargo run --bin number-guessing-game ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:1","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"控制台输出 Rust 标准库[6]中包含控制台输出函数 print 和 println,两者均支持 Rust 字符串格式化语法[7]。 基于性能考虑,print 输出会先放到行缓冲区,不会立即打印到控制台,可通过 io::stdout().flush() 手动触发打印: // 立即输出到控制台,结束后另起一行 println!(\"Let's Play a Number Guessing Game!\"); // 不换行,不会立即输出到控制台,需要手动 flush,或等待下一次 println,或者等待程序运行结束 print!(\"Guess a Number between {min} and {max}: \"); io::stdout().flush().unwrap(); ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:2","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"变量可变性 Rust 通过 let 关键字声明变量,默认不可修改,支持变量修改需要通过 mut 关键字声明。 // x 变量不支持修改 let x = 5; x = 6; ^^^^^ cannot assign twice to immutable variable // mut x 变量支持修改 let mut x = 5; x = 6; ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:3","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"函数声明与调用 Rust 通过 fn 关键字声明函数[8],支持多返回值,可省略 return 语句,以最后一行的变量或者表达式作为函数返回值: fn upper_and_lower(str: \u0026str) -\u003e (String, String) { let upper = src.to_uppercase(); let lower = src.to_lowercase(); (upper, lower) } fn main() { let (upper, lower) = upper_and_lower(\"Rust\"); println!(\"{upper} {lower}\"); // RUST rust } ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:4","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"变量所有权及其租借 Rust 不使用垃圾收集器,而是引入所有权机制[9]来进行内存管理。简单来说,就是保证始终只有一个变量对内存区域具有所有权,所有权变量失效时对应内存即被释放。 所有权 所有权机制主要用于堆内存管理。 对于基础类型变量(如 u32),其长度固定,默认在栈上分配,变量跟随出栈操作释放。栈上变量间传递以复制(Copy)方式进行: let x: u32 = 64; let y = x; println!(\"x={x} y={y}\"); // 输出 x=64 y=64 以上代码中,let y = x; 语句实际上复制 x 创建了一个新的变量 y,x 和 y 都存储在栈上。 对于复杂类型变量(如字符串、数组),其长度不固定,需要在堆上进行分配。要保证同时只有一个变量对堆内存区域拥有所有权,不可避免会发生所有权变更,rust 对所有权变更的处理策略在其他编程语言的开发者看来可能会比较违反直觉: let x = String::from(\"Rusty\"); let y = x; - value moved here println!(\"x={x} y={y}\"); ^^ value borrowed here after move 以上代码中,let y = x; 语句执行后,变量 x 的所有权移交(Move)给了变量 y,区别于复制(Copy)的方式,所有权移交后变量即视为失效,除非重新声明使用,后续所有对于变量 x 的访问在编译时就会报错。 通过观察变量指针的内存地址可以更好的理解所有权移交: let mut x = String::from(\"Rusty\"); println!(\"x={:p} *x={:p}\", \u0026x, \u0026*x); // 输出 x=0x16f0aa940 *x=0x600001e4c040 let y = x; println!(\"y={:p} *y={:p}\", \u0026y, \u0026*y); // 输出 y=0x16f0aa9c0 *y=0x600001e4c040 x = String::from(\"Rusty\"); println!(\"x={:p} *x={:p}\", \u0026x, \u0026*x); // 输出 x=0x16f0aa940 *x=0x600001e4c050 以上代码中,\u0026x 表示变量在栈上的地址,\u0026*x 表示变量指向的堆内存地址。可以看到,所有权变更后,变量 y 指向的堆内存地址 \u0026*y 与之前的 \u0026*x 相同,说明变量 y 确实接管了变量 x 对堆内存的所有权。对变量 x 重新分配后,其栈上内存地址不变,即栈上变量 \u0026x 被复用,但是指向的堆内存地址已经发生了变更。 所有权租借 如果使用所有权变更机制进行函数传参,函数调用后实参变量即失效,将导致后续对该变量的访问报错,这种情况就需要用到所有权租借机制,租借需要依赖变量引用(使用 \u0026 操作符): fn main() { let x = String::from(\"Rusty\"); let length = len(\u0026x); println!(\"{x} length={length}\"); // 输出 Rusty length=5 } fn len(str: \u0026String) -\u003e usize { str.len() } 以上代码 len 函数调用传递的是变量引用 \u0026x,没有发生所有权变更,因此在后续 println 语句中仍然可以访问变量 x。引用也可以对变量进行修改,但是需要通过 \u0026mut 显式声明。 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:5","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"从控制台获取用户输入 获取控制台输入[10]使用可变引用传递的方式,将用户输入保存到字符串变量: let mut input_str = String::new(); io::stdin().read_line(\u0026mut input_str).expect(\"failed to read line\"); ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:6","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"字符串切分和转换 Rust 提供多种字符串 split 方式[11],split 返回的是一个迭代器对象,可根据需要再次进行映射或过滤处理,猜数字游戏实现使用的是按空白字符切分(split_whitespace): let x = String::from(\"a b c\"); let y = x.split_whitespace(); println!(\"{:?}\", y.collect::\u003cVec\u003c_\u003e\u003e()); // 输出 [\"a\", \"b\", \"c\"] 使用 parse 方法[12]可对字符串进行类型转换: let four: u32 = \"4\".parse().unwrap(); assert_eq!(4, four); ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:7","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"Match 流程控制 Rust 通过 Match [13]达到类似 swtich 的效果,但是 Match 功能更加强大。可以处理表达式,也可以处理函数返回值: let x = String::from(\"123\"); let result = match x.parse::\u003cu32\u003e() { Ok(num) =\u003e num, Err(_) =\u003e panic!(\"can't parse to integer\"), }; match 要求分支完备,上面的代码如果没有异常 Err(_) 处理分支,编译时会报错。 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:8","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"异常处理 Rust 将异常分为不可恢复异常(panic)和可恢复异常,后者需要主动处理或者向上传递。通常以 Result [14]作为结果包装容器: enum Result\u003cT, E\u003e { Ok(T), Err(E), } 除了用 match 显式处理异常外,Result 有两种用 panic 处理异常的快捷方式,unwrap 和 expect: let x = String::from(\"123\"); // unwrap 在异常时直接 panic,使用默认错误信息 let result = x.parse::\u003cu32\u003e().unwrap(); // expect 也在异常时直接 panic,但使用自定义错误信息 let result = x.parse::\u003cu32\u003e().expect(\"can't parse to integer\"); 一般在生产环境推荐使用 expect 以提供更准确的上下文信息。 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:5:9","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["rust-kata"],"content":"参考资料 [1]. Programming a Guessing Game. ch02,《Rust 编程语言》 [2]. How to Write Tests. ch11.1,《Rust 编程语言》 [3]. Hello Cargo. ch01.3,《Rust 编程语言》 [4]. The Manifest Format.《Cargo 手册》 [5]. Rust 社区 crate 仓库 [6]. Rust 标准库,宏目录 [7]. Rust 标准模块:format! [8]. Functions ch03.3,《Rust 编程语言》 [9]. What Is Ownership? ch04.1,《Rust 编程语言》 [10]. Rust 标准库:标准输入输出 [11]. Rust 标准模块:str split_whitespace 方法 [12]. Rust 标准模块:str parse 方法 [13]. Match 流程控制. ch06.2,《Rust 编程语言》 [14]. 可恢复异常 Result. ch09.2,《Rust 编程语言》 ","date":"2023-11-28","objectID":"/posts/rust-kata-number-guessing-game/:6:0","tags":["Rust"],"title":"Rust 卡塔:猜数字游戏","uri":"/posts/rust-kata-number-guessing-game/"},{"categories":["blog-build"],"content":"记录使用 hugo 和 FixIt 主题搭建博客的过程。","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"Hugo [1]是一个用 go 语言编写的开源[2]网站构建框架,截止目前 GitHub 星数超过70k,它具有功能强大的模板系统、丰富的主题、完善的文档以及全平台支持的客户端,提供开箱即用的分类系统、评论系统、代码高亮、多语言支持等功能,非常适合用来搭建博客网站。 在本地撰写阶段,借助 Hugo 内置服务器可以做到毫秒级热更新,实现所见即所得。同时,得益于 Hugo 采用静态站点生成[3]的架构,Hugo 站点可以很容易的部署到各种 HTTP 服务器,且通过本地预览即可确认线上部署效果。 基于成本和使用习惯考虑,选择使用 GitHub Pages 进行部署,并且根据功能不同进行仓库拆分。话不多说,来开始吧。 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:0:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"环境安装 Hugo 分为标准和扩展两个版本,扩展版支持 WebP 图像处理以及 Dart Sass,这里我们选择安装扩展版本。 参考官方安装指南[4],前置准备需要安装 Git、Go 和 Dart Sass [5],mac系统可直接使用 homebrew 进行安装,我的电脑上 Git 和 Go 已经安装好,只需要安装 Dart Sass: # 添加 tap 源 $ brew tap sass/sass # 安装 Dart Sass $ brew install sass/sass/sass # 验证 $ sass --version 1.69.5 安装 Hugo: # Hugo 安装 $ brew install hugo # 验证 $ hugo version hugo v0.120.4-f11bca5fec2ebb3a02727fb2a5cfb08da96fd9df+extended darwin/arm64 BuildDate=2023-11-08T11:18:07Z VendorInfo=brew 可以看到 hugo 版本信息中包含 +extended 信息,说明安装的是 Hugo 扩展版。 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:1:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"博客创建 从 Hugo 主题站[6]挑选自己喜欢的主题,大多数主题都有 demo 可以体验,我选的是 FixIt。 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:2:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"GitHub 仓库准备 接下来创建三个 GitHub 仓库: GitHub Pages 仓库:仓库名前缀必须使用用户名,格式为\u003cusername\u003e.github.io,必须为公开仓库,用于博客部署。 博客仓库:可随意命名如 myblog,私有仓库,用于存储博客源代码,包括文章和配置。 主题仓库:从 FixIt 源代码库[7] fork,用于自定义配置主题,另外我 fork 过来后将默认分支从 master 重命名成了 main。 三个仓库的关系如图所示: 博客仓库将主题以 submodule 的形式导入到 themes 文件夹,通过配置文件指定使用的主题;当博客仓库的代码提交到 main 分支时,会触发 GitHub Actions 将 Hugo 构建好的静态站点文件部署到 GitHub Pages 仓库,用户即可通过 GitHub Pages 域名进行博客访问。 三个仓库中,只有 GitHub pages 仓库是必须的,如果不在意博客内容和源代码隐私性,可以去掉 myblog 仓库,将博客源代码存储到 \u003cusername\u003e.github.io 仓库中,GitHub Pages 支持使用特定分支(默认是 gh-pages)部署。如果不准备自定义修改主题,可以去掉 FixIt 仓库,直接使用官方源代码仓库或者通过 Hugo module 进行主题安装。 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:2:1","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"博客站点创建 仓库准备好之后,可以通过 Hugo 命令行工具[8]快速创建站点,Hugo 默认使用 toml 格式配置,同时支持 yaml 和 json,我的博客使用 yaml: $ hugo new site --format yaml myblog # 切换到博客仓库目录 $ cd myblog # 初始化 git 仓库 $ git init $ git add . $ git commit -m \"feat: init hugo site\" # 绑定远程博客仓库 $ git remote add origin git@github.com:will4j/myblog.git $ git push -u origin main # 增加主题子模块 # 这里因为 fork 主题仓库跟博客仓库在同一目录下,采用相对路径引入 $ git submodule add ../FixIt.git themes/FixIt # 设置主题 $ echo \"theme: FixIt\" \u003e\u003e hugo.yaml $ cat hugo.yaml baseURL: https://example.org/ languageCode: en-us title: My New Hugo Site theme: FixIt Hugo 默认的站点目录结构[9]如下: archetypes:原型目录,用于定义各种类型的内容模板。原型匹配顺序是优先本站点内,其次再到主题内查找。 assets:资产目录,用于放置 CSS,JavaScript 等全局资源库。 config:配置文件目录,主配置文件 hugo.yaml,支持多文件配置、多环境配置[10]。 content:内容目录,用于放置文章、分类、标签等内容页面。 data:数据目录,用于存取自定义配置数据。 i18n:国际化目录,用于页面文本的多语言翻译。 layouts:布局目录,用于放置 html 模板。 public:部署目录,用于存放 Hugo 构建的静态站点文件。 resources:资源目录,包含 Hugo 资产构建流水线产生的可缓存文件,如 CSS、图片等。 static:静态资源目录,该目录下的文件会被直接拷贝到站点根目录。 themes:主题目录,包含 Hugo 站点可以使用的主题。 可通过 Hugo mounts 配置[11]自定义站点目录结构。 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:2:2","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"文章创建及预览 Hugo 支持 Page bundles [12]模式,即文章内容打包在一个文件夹下,内部可以独立包含图片、子页面等静态资源,文章以 index.md 作为入口,可基于 FixIt 主题提供的 post-bundle 原型进行自定义修改: # 拷贝主题原型到站点目录 $ cp -r themes/FixIt/archetypes/post-bundle archetypes # 增加博客图片目录,最终结构如下,注意空目录在原型使用时不会生效 $ tree -a archetypes/post-bundle archetypes/post-bundle ├── images │ └── .gitkeep └── index.md # 创建 Page bundles 文章 $ hugo new content --kind post-bundle posts/hello-world # 文章目录结构 $ tree -a content/posts/hello-world content/posts/hello-world ├── images │ └── .gitkeep └── index.md # 添加 markdown 内容 $ echo \"\\n## Hello World\" \u003e\u003e content/posts/hello-world/index.md 上面的命令已经成功配置主题并创建了一篇文章,现在可以通过hugo server --buildDrafts命令启动本地预览服务,访问启动信息中显示的地址,如http://localhost:1313,进入首页,点击文章即可预览博客: 查看文章 index.md,其内容分为两部分:前置页[13]和文章主体。前置页用于配置文章元数据,如标题、分类、标签等显示设置以及评论是否开启等控制开关,文章主体是 markdown 内容。下面是一段前置页示例,yaml 格式前置页由---包裹: # content/posts/hello-world/index.md --- # 文章标题 title: Index # 文章创建时间 date: 2023-11-22T06:51:28+08:00 # 是否草稿 draft: true # 标签和分类 tags: - draft categories: - draft --- 前置页除了 Hugo 预定义的标签外,也支持用户自定义标签,比如 FixIt 主题就定义了很多自定义前置页标签[14]。 至此,博客站点的基本框架就搭起来了。 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:2:3","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"博客配置 通过 Hugo 和 FixIt 主题配置来调整博客显示布局,配置文件目录如下: $ tree -a config/_default config/_default ├── author.yaml # 作者信息配置 ├── hugo.yaml # Hugo 主配置 ├── languages.yaml # 多语言配置 ├── menus.yaml # 菜单配置,菜单也支持多语言,当前只配置中文 ├── module.yaml # 模块、目录挂载配置等 ├── outputs.yaml # 输出格式配置 ├── params.yaml # 额外参数配置,主题配置主要在这个文件 ├── permalinks.yaml # 站点路径映射 └── sitemap.yaml # 站点地图配置,主要用于 seo 优化 配置文件来源于 Hugo 定义变量[15] 和 FixIt 主题定义变量[16]。最终展示效果如下: ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"顶部菜单 创建首页、文章、分类和标签四个菜单,调整中文命名。FixIt 主题支持 FontAwesome 图标[17],可以根据个人喜好挑选。 # config/_default/menus.yaml --- # https://gohugo.io/content-management/menus # https://gohugo.io/content-management/multilingual/#menus # https://fixit.lruihao.cn/documentation/basics/#menu-configuration # https://fontawesome.com/icons main: - identifier: home name: 首页 url: / weight: 1 params: icon: fa-solid fa-home - identifier: posts name: 文章 url: /posts/ weight: 2 params: draft: false icon: fa-solid fa-archive - identifier: categories name: 分类 url: /categories/ weight: 3 params: icon: fa-solid fa-th-list - identifier: tags name: 标签 url: /tags/ weight: 4 params: icon: fa-solid fa-tags ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:1","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"搜索支持 FixIt 主题支持 Lunr.js、algolia 和 Fuse.js 三种搜索方式。 前置条件需要增加 json 输出格式,以生成搜索依赖的 index.json: # config/_default/outputs.yaml --- # https://gohugo.io/templates/output-formats/#customizing-output-formats # https://gohugo.io/templates/output-formats/#output-format-definitions home: - html - rss - json page: - html - markdown 配置使用 Fuse.js 搜索,忽略大小写: # config/_default/params.yaml --- # https://fixit.lruihao.cn/documentation/basics/#search-configuration search: enable: true type: fuse fuse: # https://fusejs.io/api/options.html isCaseSensitive: false ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:2","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"博客标题 修改默认顶部标题,去除 logo,使用 fa 图标,增加打字特效: # config/_default/params.yaml --- header: title: logo: \"\" name: Programmer William Wang pre: \u003ci class=\"fa fa-code\"\u003e\u0026nbsp\u003c/i\u003e typeit: true 在标题左边增加 GitHub 角: # config/_default/params.yaml --- githubCorner: enable: true permalink: \"https://github.com/will4j\" title: \"Visit Me on GitHub\" position: left ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:3","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"首页资料 在首页展示个人头像和联系方式: # config/_default/params.yaml --- home: profile: enable: true subtitle: The last leg of a journey just marks the halfway point. avatarMenu: github avatarURL: /img/avatar.png social: GitHub: will4j Email: williamw0825@gmail.com RSS: true ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:4","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"中文语言 在主配置文件中定义站点默认语言: # config/_default/hugo.yaml --- # Content without language indicator will default to this language. defaultContentLanguage: zh-cn languageCode: en defaultContentLanguageInSubdir: false 通过多语言配置文件单独配置中文: # config/_default/languages.yaml --- # https://gohugo.io/content-management/multilingual/#configure-languages zh-cn: # 这里大小写很重要,定义网站 html lang languageCode: zh-CN languageName: 简体中文 # If true, auto-detect Chinese/Japanese/Korean Languages in the content. # This will make .Summary and .WordCount behave correctly for CJK languages. hasCJKLanguage: true title: 程序员水王 params: description: 程序员水王的个人博客 home: profile: subtitle: 行百里者半九十。 header: title: name: 程序员水王 app: title: 程序员水王 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:5","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"作者信息 配置文章作者的名称、头像等信息: # config/_default/author.yaml --- name: 水王 email: williamw0825@gmail.com link: https://github.com/will4j avatar: img/avatar.png 头像图片放置到 assets 目录下: $ tree -a assets assets └── img └── avatar.png ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:6","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"分类别名 在 categories 目录下创建分类名称文件夹及对应 _index.md: $ tree -a content/categories content/categories ├── _index.md └── blog-build └── _index.md # content/categories/_index.md --- slug: \"categories\" title: \"分类\" --- # content/categories/blog-build/_index.md --- slug: \"blog-build\" title: \"建站笔记\" description: \"博客建站过程记录\" --- ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:7","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"页脚配置 页脚增加站点运行时长统计、开源证书配置等: # config/_default/params.yaml --- footer: enable: true since: 2023 hugo: true siteTime: enable: true value: \"2023-11-16T07:40:29+08:00\" visitor: enable: false license: '\u003ca rel=\"license external nofollow noopener noreferrer\" href=\"https://creativecommons.org/licenses/by-nc/4.0/\" target=\"_blank\"\u003eCC BY-NC 4.0\u003c/a\u003e' order: powered: 1 copyright: last statistics: first 其中,修改 FixIt 主题,增加了footer.visitor.enable参数,用于在开启不蒜子时隐藏全站访问量统计: # themes/FixIt/layouts/partials/footer.html -- {{- if eq .Site.Params.ibruce.enable true -}} ++ {{- if .Site.Params.ibruce.enable | and .Site.Params.footer.visitor.enable -}} ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:8","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"阅读数统计 开启不蒜子访问统计: # config/_default/params.yaml --- # Busuanzi count ibruce: enable: true enablePost: true ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:9","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"CDN 加速 使用 jsdelivr 对 css、js 库的静态资源进行 cdn 加速: # config/_default/params.yaml --- # https://fixit.lruihao.cn/documentation/basics/#cdn-configuration cdn: data: jsdelivr.yml ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:3:10","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"博客部署 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:4:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"部署密钥配置 首先需要配置部署密钥[18],在 GitHub Pages 仓库添加公钥,并允许写入权限: 在博客仓库 Actions 密钥中添加 GitHub Pages 仓库部署私钥: ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:4:1","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"GitHub Action 配置 在博客仓库增加 Workflows 配置文件[19]: # .github/workflows/deploy-to-github-pages.yaml --- # https://github.com/actions/starter-workflows/blob/main/pages/hugo.yml name: Deploy to Github Pages on: push: branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read metadata: read packages: read jobs: build: runs-on: ubuntu-22.04 concurrency: group: ${{ github.workflow }}-${{ github.ref }} env: HUGO_VERSION: 0.120.4 steps: - name: Install Dart Sass run: sudo snap install dart-sass - name: Setup Go uses: actions/setup-go@v4 with: go-version: \"^1.21.4\" - run: go version - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: ${{ env.HUGO_VERSION }} extended: true - name: Checkout Source Code uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - name: Build with Hugo env: # For maximum backward compatibility with Hugo modules HUGO_ENVIRONMENT: production HUGO_ENV: production run: hugo --minify --gc - name: Deploy 🚀 uses: peaceiris/actions-gh-pages@v3 # https://github.com/peaceiris/actions-gh-pages with: deploy_key: ${{ secrets.PAGES_DEPLOY_KEY }} external_repository: will4j/will4j.github.io publish_dir: ./public publish_branch: main user_name: 'github-actions[bot]' user_email: 'github-actions[bot]@users.noreply.github.com' full_commit_message: ${{ github.event.head_commit.message }} ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:4:2","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"总结 通过使用 Hugo 和 FixIt 主题创建博客站点,大量减少了网站搭建需要做的重复工作,同时 Hugo 也具备灵活扩展的能力,给用户提供了极大的自定义空间。 GitHub 工作流的引入,使作者可以专注于博客写作本身,本地预览后提交代码即可触发博客更新发布,整体工作流程如下: ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:5:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["blog-build"],"content":"参考资料 [1]. Hugo 官方文档:What is Hugo [2]. GitHub: Hugo 源代码仓库 [3]. Hugo 官方文档:Benefits of static site generators [4]. Hugo 官方文档:Installation [5]. Hugo 官方文档:Transpile Sass to CSS [6]. Hugo 官方文档:Hugo 主题站 [7]. GitHub: FixIt 源代码仓库 [8]. Hugo 官方文档:Hugo 命令行工具 [9]. Hugo 官方文档:Hugo 目录结构 [10]. Hugo 官方文档:Hugo 配置文件目录 [11]. Hugo 官方文档:Hugo 目录挂载 [12]. Hugo 官方文档:Page bundles [13]. Hugo 官方文档:前置页 [14]. FixIt 官方文档:前置页配置 [15]. Hugo 官方文档:Hugo 定义变量列表 [16]. FixIt 官方文档:主题配置 [17]. FontAwesome 图标 [18]. GitHub 文档:设置部署密钥 [19]. Hugo 官方文档:通过 GitHub Pages 部署 ","date":"2023-11-20","objectID":"/posts/create-blog-with-hugo-and-theme-fixit/:6:0","tags":["Hugo","FixIt"],"title":"使用 Hugo 和 FixIt 主题创建个人博客","uri":"/posts/create-blog-with-hugo-and-theme-fixit/"},{"categories":["python-lang"],"content":"从函数式编程和语法糖角度详解 Python 装饰器原理。","date":"2023-11-14","objectID":"/posts/python-decorator-explained/","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":" 工欲善其事,必先利其器。—《论语·卫灵公》 本文从装饰器使用到的函数式编程特性入手,讨论了无参装饰器、有参装饰器以及类装饰器三种语法糖规则下装饰器的实现,另外扩展讨论了基于类的实现方式。 总结部分提供了一些实用装饰器的参考资料,后记部分是作者对设计的一些思考,以及行文过程中发现的一些历史事件、奇闻轶事。 希望这篇文章可以帮助你更好地理解装饰器,全文思维导图如下: ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:0:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"问题场景 我想创建一个通用的日志装饰器,其作用是在函数调用之后记录参数和返回值,于是在Google一番之后,写下了下面的代码: import functools import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) logger.info(\"function %s called with args=%s kwargs=%s and result=%s\", func.__name__, args, kwargs, result) return result return wrapper @log def greeting(name, say=\"Hello\"): return f\"{say} {name}!\" 现在执行greeting函数会产生如下输出: \u003e\u003e\u003e greeting(\"World\", say=\"Hi\") INFO:__main__:function greeting called with args=('World',) kwargs={'say': 'Hi'} and result=Hi World! 'Hi World!' 看起来还不错?它足够通用,因为使用了*args和**kwargs,任何不同签名的其他函数都能通过log装饰器打印日志。但也存在一些问题,比如@functools.wraps在这里起什么作用?如果需要自定义不同的日志级别该怎么办?如果要自定义logger名称又该怎么办? 我需要更深入了解装饰器,才能回答上面这些问题。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:1:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"理解装饰器 也许你知道Java中的注解(@Annotation)是通过反射和动态代理来实现的,那么Python中的装饰器呢?可以看到,log装饰器本身是一个函数,如果把greeting函数上的log装饰器去掉,直接像下面这样调用log函数,可以得到跟使用装饰器一样的效果: \u003e\u003e\u003e log(greeting)(\"World\", say=\"Hi\") INFO:__main__:function greeting called with args=('World',) kwargs={'say': 'Hi'} and result=Hi World! 'Hi World!' 把函数作为参数?简单直接,Python装饰器也是这么做的,即在目标函数外部再包裹一层函数,并在目标函数调用前后动态增加行为,很典型的装饰器模式。那么,为什么能如此直接的做到函数作为参数传递及函数嵌套呢?先来了解一下头等函数。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:2:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"头等函数 头等函数[1],其实我更喜欢称之为一等公民函数,即在编程语言中,函数作为一等公民,享有完全行为能力,能进行编程语言实体所具备的所有操作,包括但不限于: 函数作为实参传递 函数作为返回值 函数赋值给变量 嵌套函数、匿名函数、闭包等 分别来看下装饰器使用到的几个特性:函数作为实参传递、嵌套函数以及函数作为返回值。 函数作为实参传递 典型的例子是Python内置函数map,其第一个参数为转换函数,map函数执行时会遍历列表,以列表元素作为入参调用转换函数,然后将转换函数返回值组成新的列表返回。比如下面的代码,可以完成对列表中元素求平方的转换。 def square(x): return x ** 2 \u003e\u003e\u003e map(square, [1, 2, 3, 4]) \u003cmap object at 0x102783520\u003e \u003e\u003e\u003e list(map(square, [1, 2, 3, 4])) [1, 4, 9, 16] 嵌套函数 即在函数内部嵌套声明函数,被嵌套的函数也可以称为内部函数。外部函数的局部变量(包括形参)对内部函数可见。 下面的代码用特定字符打印一个三行的三角形,内部函数print_line接收行参数n,并在函数体中直接使用了外部函数的char变量。 def print_triangle(char): def print_line(n): print(((2 * n - 1) * char).center(5)) print_line(1) print_line(2) print_line(3) \u003e\u003e\u003e print_triangle(\"*\") * *** ***** 函数作为返回值 函数返回值可以是另外一个函数。 在下面的代码中,times_n函数返回product函数,后者根据参数n不同,将返回不同的函数版本,因此times_n(3)将返回乘以3的product函数times_3,调用times_3(5)计算将得到结果15。 def times_n(n): def product(x): return x * n return product \u003e\u003e\u003e times_3 = times_n(3) \u003e\u003e\u003e times_3 \u003cfunction times_n.\u003clocals\u003e.product at 0x1027b96c0\u003e \u003e\u003e\u003e times_3(5) 15 重新认识装饰器 函数在Python中被视为一等公民,对头等函数有了初步了解之后,可以从函数的角度重新认识一下log装饰器,关注其中用到的头等函数特性:func函数作为入参传递,内部函数wrapper作为返回值: def log(func): # 1. func函数作为入参传递 @functools.wraps(func) def wrapper(*args, **kwargs): # 2. 嵌套函数(内部函数)wrapper result = func(*args, **kwargs) logger.info(\"function %s called with args=%s kwargs=%s and result=%s\", func.__name__, args, kwargs, result) return result return wrapper # 3. 函数作为返回值 可以把log(greeting)(\"World\", say=\"Hi\")进行分解: greeting = log(greeting) # 头等函数log的返回值是内部函数wrapper, # 将wrapper赋值给变量greeting,两者都是函数类型 greeting(\"World\", say=\"Hi\") # 使用greeting变量执行,实际调用的是wrapper函数 像上面这样分解后,greeting(\"World\", say=\"Hi\")调用已经不是原greeting函数,实际执行的是内部函数wrapper,因而可以在wrapper中动态新增行为,即在函数调用后输出日志。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:2:1","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"装饰器语法糖 从函数的角度重新认识装饰器后,我们知道显式调用log(greeting)(\"World\", say=\"Hi\")已经可以达到装饰器的效果,也清楚了调用的过程和原理,那么@log的使用方式有什么不同呢? 其实@操作符只是一个语法糖,在PEP 318[2]中有清晰的描述,其达到的效果如下,即使得: @dec2 @dec1 def func(arg1, arg2, ...): pass 等价于: def func(arg1, arg2, ...): pass func = dec2(dec1(func)) 同时支持有参装饰器(在函数装饰器进阶一节中将详细讨论有参装饰器),使得: @decomaker(argA, argB, ...) def func(arg1, arg2, ...): pass 等价于: def func(arg1, arg2, ...): pass func = decomaker(argA, argB, ...)(func) ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:2:2","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"functools.wraps 的作用 再来看下@functools.wraps的作用。 如果去掉log装饰器中内部函数wrapper上的@functools.wraps(func),查看greeting函数对象,会发现它其实是装饰器内部的wrapper函数,这符合之前对装饰器原理的理解。 ... def log(func): # 移除 @functools.wraps(func) def wrapper(*args, **kwargs): ... @log def greeting(name, say=\"Hello\"): ... \u003e\u003e\u003e greeting \u003cfunction log.\u003clocals\u003e.wrapper at 0x10459e2a0\u003e 在wrapper上加上@functools.wraps(func)后,再查看greeting函数对象,发现它仍然是greeting函数: ... def log(func): @functools.wraps(func) # 保留 wraps def wrapper(*args, **kwargs): ... @log def greeting(name, say=\"Hello\"): ... \u003e\u003e\u003e greeting \u003cfunction greeting at 0x10459f4c0\u003e 查看@functools.wraps的源码发现,它的作用是将func函数的部分元数据信息复制给wrapper函数,使其伪装成func,这些元数据[3]包括:函数名称(__name__)、函数注释(__doc__)以及函数模块路径(__module__)等,足够应付大部分场景。 此外,还可以通过__wrapped__属性访问被装饰函数的真身: \u003e\u003e\u003e greeting.__wrapped__ \u003cfunction greeting at 0x1079e2a20\u003e \u003e\u003e\u003e greeting.__wrapped__(\"World\") # 这里并没有打印日志,说明确实调用的是原始greeting函数 'Hello World!' ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:2:3","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"装饰器小结 装饰器通过@语法糖,使用wrapper函数包裹并通过@functools.wraps伪装成目标函数func,从而达到增强目标函数功能的目的。使用装饰器后,实际调用的是装饰器内的wrapper函数,而不再是func函数自身,简单示意如下图: 在开头提到的三个问题中,我们已经解答了第一个问题,即@functools.wraps的作用。接下来看下如何支持日志级别以及logger名称,这需要进阶的装饰器用法。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:2:4","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"函数装饰器进阶 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:3:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"有参装饰器 根据PEP 318[2]对@操作符语法糖的说明(可以回顾一下装饰器语法糖一节的内容),如果给log装饰器添加日志级别参数,其效果应该如下: @log(level=logging.INFO) def greeting(name, say=\"Hello\"): ... # 那么 greeting(\"World\") # 应该等价于 log(level=logging.INFO)(greeting)(\"World\") 有点太绕了,把中间过程拆解出来再看下: # 第一步接收参数,返回可访问参数的闭包函数 decorator = log(level=logging.INFO) # 用闭包函数作为新的装饰器对greeting函数进行包装 greeting = decorator(greeting) # 调用包装过后装饰器函数 greeting(\"World\") 可以看到,相比无参装饰器,有参装饰器引入了一个中间层,负责返回可访问入参的闭包函数,之后这个闭包函数就可以当做无参装饰器来使用。接下来使用这种方式重新实现log装饰器以支持自定义日志级别: import functools import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def log(level=logging.INFO): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) logger.log(level, \"function %s called with args=%s kwargs=%s and result=%s\", func.__name__, args, kwargs, result) return result return wrapper return decorator \u003e\u003e\u003e @log(level=logging.WARNING) ... def greeting(name, say=\"Hello\"): ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") WARNING:__main__:function greeting called with args=('World',) kwargs={} and result=Hello World! 'Hello World!' \u003e\u003e\u003e @log() ... def greeting(name, say=\"Hello\"): ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") INFO:__main__:function greeting called with args=('World',) kwargs={} and result=Hello World! 'Hello World!' 到目前为止效果都很理想✌️。不过不知你有没有注意到,在使用默认参数时,装饰器写法是带空括号的@log(),从装饰器语法糖的角度来看,这个空括号还不能去掉,不然的话包裹目标函数的会是中间层的decorator,而不是最终期望的wrapper,来验证一下: \u003e\u003e\u003e @log ... def greeting(name, say=\"Hello\"): ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") \u003cfunction log.\u003clocals\u003e.decorator.\u003clocals\u003e.wrapper at 0x107a95c60\u003e 果然如此,这时候调用greeting实际上执行的是decorator函数,所以返回值是wrapper。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:3:1","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"装饰器可选参数 那么,对于有参装饰器是否能支持不带括号使用呢?这样能使装饰器更加灵活,也能避免因括号遗漏导致编码错误。再次回顾装饰器语法糖,实际期望达到的效果应该如下: @log def greeting(name, say=\"Hello\"): ... # 使得 greeting(\"World\") # 等价于 log(greeting)(\"World\") @log(level=logging.INFO) def greeting(name, say=\"Hello\"): ... # 使得 greeting(\"World\") # 等价于 log(level=logging.INFO)(greeting)(\"World\") 即装饰器需要同时支持有参和无参两种情况,接下来对log装饰器进行改造: import functools import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def log(_func=None, *, level=logging.INFO): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) logger.log(level, \"function %s called with args=%s kwargs=%s and result=%s\", func.__name__, args, kwargs, result) return result return wrapper if _func is None: return decorator else: return decorator(_func) 实际效果验证: \u003e\u003e\u003e @log ... def greeting(name, say=\"Hello\"): ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") INFO:__main__:function greeting called with args=('World',) kwargs={} and result=Hello World! 'Hello World!' \u003e\u003e\u003e @log(level=logging.WARNING) ... def greeting(name, say=\"Hello\"): ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") WARNING:__main__:function greeting called with args=('World',) kwargs={} and result=Hello World! 'Hello World!' 改造之后的log装饰器增加了_func参数,总结来说,当_func为空时,log应该是有参装饰器,否则应该是无参装饰器。同样的逻辑可以使用partial[4] [5]重新实现[6],让代码看起来更直观,同时提取日志名称和日志模板参数,使log装饰器更加通用,最终结果如下: import functools import logging logging.basicConfig(level=logging.INFO) def log( func=None, *, level=logging.INFO, name=__name__, fmt=\"function %s called with args=%s kwargs=%s and result=%s\", ): if func is None: return functools.partial(log, level=level, fmt=fmt) # 将logger获取放到wrapper外部,只会在装饰器解析时执行一次,避免每次函数调用都执行 logger = logging.getLogger(name) @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) logger.log(level, fmt, func.__name__, args, kwargs, result) return result return wrapper 至此,log装饰器已经基本完善,开头提出来的日志级别和logger名称的问题也得到了解决,不要停下来,跟我一起继续向前探索。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:3:2","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"装饰器别名 有时候,有参装饰器的某几个参数会在多个场景中重复使用,使代码显得冗余且难以维护,这时可以使用partial为装饰器添加别名。还是以log为例,试下通过别名来自定义日志模板: # 装饰器别名,自定义日志模板 my_log = functools.partial(log, fmt=\"%s executed, args: %s kwargs: %s result: %s\") @my_log def greeting(name, say=\"Hello\"): return f\"{say} {name}!\" \u003e\u003e\u003e greeting(\"World\") INFO:__main__:greeting executed, args: ('World',) kwargs: {} result: Hello World! 'Hello World!' 函数别名可以快捷方便地做到特定签名的复用,尤其在装饰器以及第三方引入函数的使用上,可以有效消除重复的传参逻辑。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:3:3","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"类装饰器 到目前为止,装饰器涉及的参与对象都是函数,接下来加入类的场景,装饰器作用在类上有两种方式:一种是装饰类方法,这种方式其实与装饰函数相同;另一种是装饰类声明,其实现由PEP 3129[7]定义,语法糖效果如下: @foo @bar class A: pass # 等价于: class A: pass A = foo(bar(A)) 装饰器在类声明上主要的用途是修改类的元数据,并且其修改不会被继承,典型的使用场景是内置装饰器dataclass,它改变了类的初始化过程,同时提供了属性冻结(只读)等特性。dataclass太复杂,下面以一个相对简单的singleton装饰器[8]来实现装饰类: import functools def singleton(cls): \"\"\"Make a class a Singleton class (only one instance)\"\"\" @functools.wraps(cls) def wrapper_singleton(*args, **kwargs): if not wrapper_singleton.instance: wrapper_singleton.instance = cls(*args, **kwargs) return wrapper_singleton.instance # 给wrapper函数创建instance属性并初始化为None wrapper_singleton.instance = None return wrapper_singleton @singleton class TheOne: pass \u003e\u003e\u003e a = TheOne() \u003e\u003e\u003e b = TheOne() \u003e\u003e\u003e a is b True \u003e\u003e\u003e a.x = 5 \u003e\u003e\u003e b.x 5 使用@singleton装饰类后,类实例创建实际调用的是wrapper_singleton函数,函数内部控制返回共享实例,从而达到单例模式的效果。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:3:4","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"基于类实现装饰器 除了使用函数外,用类也可以实现装饰器,一切只需要符合语法糖的规则,所以前提是类的实例也能像函数一样被调用,而这正是callable [9]特性所支持的。 在Python中,所有能够被调用(不管是有参还是无参)的对象都属于callable对象,除了最直观的函数,类也是callable对象(调用类即类的构造函数),实现了__call__方法的类实例也一样。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:4:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"类的__call__方法 类的__call__方法[10]达到的效果是把所有对实例的调用都转换为对实例__call__方法的调用: class A: def __init__(self, name): self.name = name def __call__(self, say=\"Hi\"): return f\"{say} {self.name}\" \u003e\u003e\u003e a = A(\"World\") \u003e\u003e\u003e a() 'Hi World' \u003e\u003e\u003e a(\"Hello\") 'Hello World' ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:4:1","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"基于类的装饰器实现 既然类的实例可以被调用,那么使用类来实现装饰器就不是问题了。尝试使用类来重新实现日志打印装饰器,按惯例,先回顾装饰器语法糖,看下期望达到的效果: class Log: pass @Log def greeting(name, say=\"Hello\"): ... greeting(\"World\") # 应该等价于 Log(greeting)(\"World\") @Log(level=logging.INFO) def greeting(name, say=\"Hello\"): ... greeting(\"World\") # 应该等价于 Log(level=logging.INFO)(greeting)(\"World\") 在无参装饰器场景下,类构造器传入目标函数,__call__代理目标函数执行;在有参装饰器场景下,类构造器传入装饰器参数,__call__第一次调用传入目标函数,后续调用才是代理执行。具体实现如下: import functools import logging class Log: def __init__( self, func=None, *, level=logging.INFO, name=__name__, fmt=\"function %s called with args=%s kwargs=%s and result=%s\", ): self.level = level self.name = name self.fmt = fmt self.logger = logging.getLogger(self.name) self.__update_wrapper(func) def __update_wrapper(self, func): self.func = func if func is None: return functools.update_wrapper(self, func) def __call__(self, *args, **kwargs): if self.func is None: self.__update_wrapper(args[0]) return self result = self.func(*args, **kwargs) self.logger.log(self.level, self.fmt, self.func.__name__, args, kwargs, result) return result \u003e\u003e\u003e @Log ... def greeting(name, say=\"Hello\"): ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") INFO:__main__:function greeting called with args=('World',) kwargs={} and result=Hello World! 'Hello World!' \u003e\u003e\u003e @Log(level=logging.WARNING) ... def greeting(name, say=\"Hello\"): ... \"\"\"balabala\"\"\" ... return f\"{say} {name}!\" ... \u003e\u003e\u003e greeting(\"World\") WARNING:__main__:function greeting called with args=('World',) kwargs={} and result=Hello World! 'Hello World!' 这版实现看起来很繁琐,框架性的代码占比较大,可以抽取一个装饰器基类[11],封装装饰器构造的公共代码,这样创建新的装饰器就会简洁很多了。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:4:2","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"小心状态化 基于类的装饰器有个”副作用“,即每次语法糖解析过程都会创建一个类实例,而类实例是状态化的,这意味着同一目标函数的装饰器参数会在多次调用之间共享,所以除非你清楚自己在做什么,千万不要在__call__中对装饰器参数进行修改,我就在一个动态SQL执行装饰器中吃过这样的教训。举个例子,下面这段投硬币游戏的代码,一旦出现一次反面,结果就永远是反面了: import random class ConsoleDisplay: def __init__(self, func): self.func = func self.message = \"heads\" def __call__(self, *args, **kwargs): res = self.func(*args, **kwargs) if not res: self.message = \"tails\" self.print_message() def print_message(self): print(self.message) @ConsoleDisplay def flip_the_coin(): return random.choice([True, False]) \u003e\u003e\u003e flip_the_coin() heads \u003e\u003e\u003e flip_the_coin() tails \u003e\u003e\u003e flip_the_coin() tails ... # tails x 1000 (doge) ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:4:3","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"总结 Python装饰器初看很简单,但是要构建一个通用且稳定的装饰器却是一项比较有挑战性的工作。 首先要知道@语法糖的规则,清楚无参装饰器、有参装饰器以及类装饰器三种场景各自的执行过程,这可以说是装饰器世界的三项基本法则。其次是关于头等函数,理解函数作为实参传递、内部函数以及函数作为返回值的用法,这些是装饰器世界运转的三个前提条件。 三项基本法则和三个前提条件是我在Python装饰器世界中发现的不变的部分,我把它们分享给你。 使用装饰器可以实现很多非常极客的功能,比如单例模式、缓存、数据校验、注册事件监听者、异步调用等,有些功能也许你已经使用过,下次再遇到不妨停下来看看源码,相信你已经能够一眼就看懂它的实现了。限于篇幅,实战部分不再展开,可参考资料[8][12],里面有很多实用装饰器,酷壳[12]中甚至引用了一个装饰器合集[13],里面有40多个实用装饰器,牛哇。 最后,再奉上一份后台任务装饰器代码[14],作抛砖引玉之用,以上。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:5:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"后记 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:6:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"设计思考 函数式编程 作为一种编程范式,跟面向过程编程相比,函数式编程重视结果多于过程。这种倾向一方面体现为使用声明式而非指令式,即函数应该描述做了什么而不是怎么做;另一方面体现为不可变性或者说无状态化,即相同的参数调用函数总能得到一致的结果。 函数式编程实际上是将复杂的大问题分解成相对简单的独立子问题,简单子问题更容易求解和保证正确性。 关注点分离 封装的精髓所在,使用装饰器可以将一些通用逻辑抽离成独立的模块,比如日志、权限、缓存,一方面可以减少重复,另一方面可以让专业的“人”做专业的事情,日志装饰器只关注日志,它的目的就是把日志打出花来 :)。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:6:1","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"编程考古 2002年10月14日,Python 2.2.2 版本发布,引入了staticmethod和classmethod,使用方式纯靠手动,如foo = staticmethod(foo),期望未来会有一个语法来解决这个问题的种子在彼时就被埋下了。 2002年2月至2004年7月,与装饰器语法相关的讨论在社区持续进行,无数的提案被提交,如单词类的as、using等,符号类的;...、\u003c…\u003e、|…、[…]等,其中[…]是@…的强力竞争者。 2003年6月5日,定义装饰器规范的PEP 318[2]创建,彼时的标题还是pep 318, Decorators for Functions, Methods and Classes,PEP 318经多次修改,历时近14个月,最终只支持函数和方法装饰器。 2004年6月7日至9日,欧洲Python开发者大会召开,Python之父Guido携带社区提案到会上进行讨论,但仍未有结论。 2004年7月30号,”Pie-Thon”挑战(2003年Parrot虚拟机开发者Dag发起的挑战,比较Python通过Parrot虚拟机和CPython运行的性能)在OSCON上兑现,作为赢家的Guido可以往Dag脸上丢一个奶油派,社区普遍认为这个事件对Guido最终决定使用@具有重要作用,因而很多人称之为“命运之派“。 2004年8月2日,Anthony在Guido授意下提交了一版@装饰器实现,因为形状类似派,Barry给它取了个外号叫”派-装饰器“。 ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:6:2","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"},{"categories":["python-lang"],"content":"参考资料 [1]. 维基百科:头等函数 [2]. PEP 318 – Decorators for Functions and Methods [3]. Python documentation: User-defined functions [4]. Python documentation: functools.partial [5]. Python documentation: partial-objects [6]. Python Cookbook 3rd Edition: 9.6 带可选参数的装饰器 [7]. PEP 3129 – Class Decorators [8]. realpython: primer-on-python-decorators [9]. Python documentation: term-callable [10]. Python documentation: Emulating callable objects [11]. people-doc: Class-based decorators with Python [12]. 酷壳:PYTHON修饰器的函数式编程 [13]. PythonDecoratorLibrary [14]. gist: add background task with task register decorator ","date":"2023-11-14","objectID":"/posts/python-decorator-explained/:7:0","tags":["Python"],"title":"Python 装饰器详解","uri":"/posts/python-decorator-explained/"}]