Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Python——Python中没有Char类型


没有Char类型

不同于C++和Java等语言,Python中没有字符char类型,只有字符串类型


关于字符类型的操作

  • 把长度为1的字符串当成字符来操作,比如函数ord(s)中只要s的长度为1(len(s) == 1)即可,否则ord函数抛出异常
    • 长度为1的字符串本质上还是一个字符串类型<type str>
  • 判断字符串某一位置的字符时直接比较即可,如:
    1
    2
    3
    4
    5
    6
    7
    s = "12345"
    if s[2] == '2':
    print s[3]
    # 等价于
    if s[2] == "2":
    print s[3]
    # "2"和'2'都是<type str>类型的

Python——strip函数用法注意事项


整体说明

  • strip() 方法容易误解,使用时可能会发生未知问题,所以要小心
  • strip() 方法会从字符串的开头和结尾移除指定的字符,但它是逐个字符进行匹配的,而不是匹配整个子字符串
    • 即把输入参数当做一个字符集合(注意是一个字符集合而不是一个字符串)
    • 后续的文字只要在这个集合内都会被清除掉,知道遇到第一个不在这个字符集合的字符为止

具体示例分析

  • 下面是一个简单示例:

    1
    2
    string = "SYSTEM:You are an AI assistant. You will be given a task.  "
    print(string.strip('SYSTEM:'))
    • 上面的句子输出是 "ou are an AI assistant. You will be given a task. "(注意:"SYSTEM:Y" 都被删除了,不仅仅是 "SYSTEM:")
  • 具体来说,当执行 strip('SYSTEM:') 时:

    • strip() 会把 'SYSTEM:' 看作是一个字符集合 :{'S', 'Y', 'S', 'T', 'E', 'M', ':'}
    • 从字符串开头开始,逐个检查字符是否在这个集合中:
      • 'S' 在集合中,移除
      • 'Y' 在集合中,移除
      • 'S' 在集合中,移除
      • 'T' 在集合中,移除
      • 'E' 在集合中,移除
      • 'M' 在集合中,移除
      • ':' 在集合中,移除
      • 'Y' 在集合中,移除(这里是 “You” 的 ‘Y’)
      • 'o' 不在集合中,停止移除,后续的字符都不再移除
  • 所以最终结果是 "ou are an AI assistant. You will be given a task. "

  • 如果你想移除特定的前缀字符串,应该使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 方法1:使用 removeprefix() (Python 3.9+)
    result = string.removeprefix('SYSTEM:')

    # 方法2:使用字符串切片
    if string.startswith('SYSTEM:'):
    result = string[7:] # 'SYSTEM:' 长度为7

    # 方法3:使用 replace() (但要小心,会替换所有匹配项)
    result = string.replace('SYSTEM:', '', 1) # 只替换第一个
  • 这样就能得到正确的结果:"You are an AI assistant. You will be given a task. "

Python——tqdm库使用


整体说明

  • Tqdm 是一个快速、可扩展的 Python 进度条库,可以轻松地为你的循环添加一个智能进度条,让你直观地了解任务的执行进度
  • Tqdm 的名字来源于阿拉伯语 “taqaddum”,意为“进展”
  • Tqdm 可以通过一行 pip 指令安装:
    1
    pip install tqdm

Tqdm 的最常用用法(自动控制)

  • Tqdm 最核心的用法就是将可迭代对象包装在 tqdm() 函数中

    1
    2
    3
    4
    5
    6
    7
    from tqdm import tqdm
    import time

    # 循环 100 次,每次暂停 0.01 秒
    for i in tqdm(range(100)):
    time.sleep(0.01)
    # 100%|██████████| 100/100 [00:01<00:00, 83.01it/s]
    • 运行这段代码,你会看到一个实时的进度条,显示循环的完成百分比、已完成的迭代次数、总迭代次数、每秒迭代次数以及预计剩余时间

Tqdm 的常用参数

  • desc: 给进度条添加一个描述性前缀

    1
    2
    3
    for i in tqdm(range(100), desc="Processing data"):
    time.sleep(0.01)
    # Processing data: 100%|██████████| 100/100 [00:01<00:00, 82.70it/s]
  • total: 当可迭代对象没有 __len__ 方法时,你可以手动指定总迭代次数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import random

    # 假设有一个生成器
    def my_generator():
    for _ in range(100):
    yield random.randint(1, 10)

    # 因为生成器没有长度,所以需要指定 total
    for item in tqdm(my_generator(), total=100):
    time.sleep(0.01)
    # 100%|██████████| 100/100 [00:01<00:00, 81.47it/s]
  • unit: 设置迭代单位,例如 'B'(字节)、'it'(迭代)

    1
    2
    3
    4
    # 模拟文件下载进度条
    for i in tqdm(range(1024), unit='B', unit_scale=True, desc="Downloading file"):
    time.sleep(0.001)
    # Downloading file: 100%|██████████| 1.02k/1.02k [00:12<00:00, 83.3B/s]
    • unit_scale=True 会自动将单位转换为 K、M、G 等,让显示更友好
  • ncols: 设置进度条的宽度,可以是一个整数或 None(自动适应终端宽度)

    1
    2
    3
    for i in tqdm(range(100), ncols=80): # 固定宽度为 80 个字符
    time.sleep(0.01)
    # 100%|█████████████████████████████████████████| 100/100 [00:01<00:00, 83.97it/s]
  • inital: 设置初始进度,用于从中间恢复任务的场景,一般用于手动控制的场景,下面会介绍


Tqdm 手动控制用法

  • 在某些情况下,可能无法直接将可迭代对象传递给 tqdm,例如当你需要在一个循环中分步更新进度时
  • 这时可以使用 tqdm 的上下文管理器或手动控制

使用上下文管理器 (推荐)

  • 使用 with 语句可以确保进度条在循环结束后正确关闭

    1
    2
    3
    4
    5
    6
    7
    with tqdm(total=100, desc="Manual loop") as pbar:
    for i in range(100):
    # 你的任务代码
    time.sleep(0.01)
    # 手动更新进度条
    pbar.update(1)
    # Manual loop: 100%|██████████| 100/100 [00:01<00:00, 82.50it/s]
    • pbar.update(n) 会将进度条前进 n 步,注意超过以后继续更新会导致超出部分显示异常(不会报错)
      1
      2
      3
      4
      5
      6
      7
      with tqdm(total=100, desc="Manual loop") as pbar:
      for i in range(100):
      # 你的任务代码
      time.sleep(0.01)
      # 手动更新进度条
      pbar.update(2)
      # Manual loop: 200it [00:01, 165.95it/s]
  • 若使用 initial 参数,则代码示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from tqdm import tqdm
    import time

    with tqdm(initial=50, total=100, desc="Manual loop") as pbar:
    for i in range(50):
    # 你的任务代码
    time.sleep(0.01)
    # 手动更新进度条
    pbar.update(1)
    # 开始就是下面这样
    # Manual loop: 50%|█████ | 50/100 [00:00<?, ?it/s]
    # 最终变成这样:
    # Manual loop: 100%|██████████| 100/100 [00:00<00:00, 81.43it/s]

手动创建和关闭

  • 如果不能使用上下文管理器,可以手动创建和关闭进度条
    1
    2
    3
    4
    5
    6
    7
    8
    pbar = tqdm(total=100)
    for i in range(100):
    # 你的任务代码
    time.sleep(0.01)
    # 更新进度条
    pbar.update(1)
    # 任务完成后手动关闭进度条
    pbar.close()

Tqdm 高级用法

tqdm.notebook for Jupyter/IPython(暂未测试)

  • 如果在 Jupyter Notebook 或 IPython 环境中,可以使用 tqdm.notebook 模块,它会生成一个更美观的 HTML 进度条
    1
    2
    3
    4
    5
    from tqdm.notebook import tqdm
    import time

    for i in tqdm(range(100)):
    time.sleep(0.01)

tqdm.pandas for Pandas(暂未测试)

  • Tqdm 可以轻松地与 Pandas 的 apply、groupby 等方法结合,为数据处理过程添加进度条
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import pandas as pd
    from tqdm.pandas import tqdm # 注意这里导入的是 tqdm.pandas 包

    tqdm.pandas(desc="Processing DataFrame")

    df = pd.DataFrame({'a': range(100000)})

    # 使用 progress_apply 替代 apply
    df['b'] = df['a'].progress_apply(lambda x: x * 2)

嵌套进度条

  • 当需要为嵌套循环添加进度条时,可以将内部循环的 tqdm 实例作为外部 tqdm 的子项

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    for i in tqdm(range(5), desc="Outer loop"):
    for j in tqdm(range(10), desc=f"Inner loop {i}", leave=False):
    time.sleep(0.01)
    # Outer loop: 0%| | 0/5 [00:00<?, ?it/s]
    # Inner loop 0: 0%| | 0/10 [00:00<?, ?it/s]
    # Inner loop 0: 90%|█████████ | 9/10 [00:00<00:00, 82.94it/s]
    # Outer loop: 20%|██ | 1/5 [00:00<00:00, 8.23it/s]
    # Inner loop 1: 0%| | 0/10 [00:00<?, ?it/s]
    # Inner loop 1: 90%|█████████ | 9/10 [00:00<00:00, 84.44it/s]
    # Outer loop: 40%|████ | 2/5 [00:00<00:00, 8.31it/s]
    # Inner loop 2: 0%| | 0/10 [00:00<?, ?it/s]
    # Inner loop 2: 90%|█████████ | 9/10 [00:00<00:00, 82.45it/s]
    # Outer loop: 60%|██████ | 3/5 [00:00<00:00, 8.26it/s]
    # Inner loop 3: 0%| | 0/10 [00:00<?, ?it/s]
    # Inner loop 3: 90%|█████████ | 9/10 [00:00<00:00, 80.50it/s]
    # Outer loop: 80%|████████ | 4/5 [00:00<00:00, 8.16it/s]
    # Inner loop 4: 0%| | 0/10 [00:00<?, ?it/s]
    # Inner loop 4: 80%|████████ | 8/10 [00:00<00:00, 79.85it/s]
    # Outer loop: 100%|██████████| 5/5 [00:00<00:00, 8.15it/s]
    • 注意 leave=False 参数,它会确保内部进度条在完成后立即消失,避免屏幕杂乱

Python——全局解释器锁


什么是 GIL?

  • 全局解释器锁(Global Interpreter Lock,GIL)是 Python 解释器(如 CPython)中的一个机制,它确保在同一时刻只有一个线程执行 Python 字节码
  • GIL 的存在意味着:即使在多核处理器上 ,多个线程也无法真正并行执行 Python 代码

GIL 存在的原因

  • GIL 的存在主要是为了保护 Python 解释器的内部状态,避免多线程同时修改导致的数据竞争问题
  • 由于 Python 的内存管理不是线程安全的,GIL 提供了一种简单的解决方案来保证线程安全

GIL对多线程的影响

  • GIL对多线程程序的影响取决于线程执行的任务类型:
    • CPU密集型任务 :这类任务主要是进行大量的计算,需要频繁使用CPU
      • GIL使得,即使使用多线程,多个 CPU 核心也无法同时执行 Python 代码
      • 多线程在 CPU 密集型任务上的表现可能比单线程更差,因为线程切换会带来额外的开销
    • I/O密集型任务 :这类任务主要是进行输入输出操作,如网络请求、文件读写等
      • 在执行 I/O 操作时,线程会释放 GIL,允许其他线程执行
      • 多线程在I/O密集型任务上可以显著提高性能

如何规避 GIL 的限制

  • 使用多进程(常用) :multiprocessing模块允许创建多个进程,每个进程都有自己的 Python 解释器和 GIL,因此可以真正并行执行 CPU 密集型任务
  • 使用 C 扩展(不常用) :将关键部分的代码用 C 语言实现,并在 C 扩展中释放 GIL。这样可以让C代码在多核处理器上并行执行

Python——判断未知源的编码类型

有时候遇到一个文件,而我们并不知道它是什么编码方式编码的,本文给出了一些判断未知文件编码方式的方法


使用chardet包


在程序中判断

  • 安装Chardet包

    1
    pip install chardet
  • 使用Chardet包做判断

    1
    2
    3
    4
    5
    6
    import urllib
    rawdata = urllib.urlopen('http://yahoo.co.jp/').read()
    import chardet
    print chardet.detect(rawdata)
    # Output:
    {'confidence': 0.99, 'language': '', 'encoding': 'utf-8'}

更多高级使用方法可参考chardet文档


直接使用命令判断

  • 安装chardetect工具

    1
    pip install chardet
  • 使用chardetect命令

    1
    2
    3
    4
    # 检测test.txt文件的编码方式
    chardetect test.txt
    # Output:
    test-chardetect.txt: ascii with confidence 1.0

Python——反编译(Disassemble)与字节码(Bytecode)

为了知道Python代码底层都做了哪些操作,我们常常需要反编译Python代码以获得Python的字节码
我们可以获得: classes, methods, functions, or code 的字节码


获取字节码的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 比较`[]`和`list()`两者的不同
from dis import dis

# test case 1
dis("[]")
# Output:
1 0 BUILD_LIST 0
2 RETURN_VALUE

# test case 2
dis("list()")
# Output:
1 0 LOAD_NAME 0 (list)
2 CALL_FUNCTION 0
4 RETURN_VALUE

**由上述输出可知,`list()` 比 `[]` 会多执行一行字节码`LODA_NAME`**

Python——命名规范

Python命名规范(Name Convention)
Reference: https://blog.csdn.net/real_myth/article/details/68927665


命名:

module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_VAR_NAME, instance_var_name, function_parameter_name, local_var_name.


应该避免的名称

  • 单字符名称, 除了计数器和迭代器.
  • 包/模块名中的连字符(-)
  • 双下划线开头并结尾的名称(Python保留, 例如__init__)

命名约定

  • 所谓”内部(Internal)”表示仅模块内可用, 或者, 在类内是保护或私有的.
  • 用单下划线(_)开头表示模块变量或函数是protected的(使用import * from时不会包含).
  • 用双下划线(__)开头的实例变量或方法表示类内私有.
  • 将相关的类和顶级函数放在同一个模块里. 不像Java, 没必要限制一个类一个模块.
  • 对类名使用大写字母开头的单词(如CapWords, 即Pascal风格), 但是模块名应该用小写加下划线的方式(如lower_with_under.py). 尽管已经有很多现存的模块使用类似于CapWords.py这样的命名, 但现在已经不鼓励这样做, 因为如果模块名碰巧和类名一致, 这会让人困扰.

引号

  • 自然语言使用双引号(想表达人为意思的,比如log,错误,提示等)
  • 机器标识使用单引号(比如dict的key等)
  • 正则表达式使用原生的双引号 r”…”
  • docstring使用三双引号

Python之父Guido推荐的规范

Type Public Internal
Modules lower_with_under _lower_with_under
Packages lower_with_under
Classes CapWords _CapWords
Exceptions CapWords
Functions lower_with_under() _lower_with_under()
Global/Class Constants CAPS_WITH_UNDER _CAPS_WITH_UNDER
gobal/Class Variables lower_with_under _lower_with_under
Instance Variables lower_with_under _lower_with_under (protected) or __lower_with_under (private)
Method Names lower_with_under() _lower_with_under() (protected) or __lower_with_under() (private)
Function/Method Parameters lower_with_under
Local Variables lower_with_under

Python——np.nan, None的判断和比较

Python值的判断与比较: np.nan, None


None

1
2
3
4
5
6
7
8
type(None)
# Output: <type 'NoneType'>

None is None
# Output: True

None == None
# Output: True

np.nan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type(np.nan)
# Output: <type 'float'>

np.nan == np.nan
# Output: False

np.nan is np.nan
# Output: True

np.nan != np.nan
# Output: True

np.nan > np.nan
# Output: False

np.nan < np.nan
# Output: False

np.nan 与 None

1
2
3
4
5
6
7
8
None == np.nan
# Output: False

None != np.nan
# Output: True

None is np.nan
# Output: False

与数字的比较

1
2
3
4
5
6
7
8
np.nan > 10
# Output: False

np.nan < 10
# Output: False

np.nan == 10
# Output: False

总结

  • 【Python2和Python3表现相同】
    np.nan 只有在np.nan != np.nan或者np.nan is np.nan时为True , 其他情况下和数字比较(包括和自身)都为False
  • 【Python2和Python3表现相异】
    Python3与Python2在直接使用np.nan时表现正常,但是当涉及到DataFrame的NaN时表现不同

特殊情况

DataFrame中的NaN与数字比较时会出现有时候为True有时候为False的情况

  • 这种情况出现在Python3中,当NaN 与数字比较时
    • 此时对于列属性类型为数值型,那么返回False
    • 否则返回True
  • Python2中NaN和数字的就是np.nan和数字比较的结果,都为False

    Python2与Python3比较

  • 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Python3:
import pandas as pd
df = pd.DataFrame([['a',2,3], ['a',3,4], ['a',8,9]], index=['a', 'b', 'c'])
df = df.reindex(['a', 'b', 'c', 'd', 'e', 'f'])
print(df)
# Output:
0 1 2
a a 2.0 3.0
b a 3.0 4.0
c a 8.0 9.0
d NaN NaN NaN
e NaN NaN NaN
f NaN NaN NaN

print(df > 5)
# Output:
0 1 2
a True False False
b True False False
c True True True
d True False False
e True False False
f True False False

print(df.values)
# Output:
[['a' 2.0 3.0]
['a' 3.0 4.0]
['a' 8.0 9.0]
[nan nan nan]
[nan nan nan]
[nan nan nan]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Python2:
import pandas as pd
df = pd.DataFrame([['a',2,3], ['a',3,4], ['a',8,9]], index=['a', 'b', 'c'])
df = df.reindex(['a', 'b', 'c', 'd', 'e', 'f'])
print(df)
# Output:
0 1 2
a a 2.0 3.0
b a 3.0 4.0
c a 8.0 9.0
d NaN NaN NaN
e NaN NaN NaN
f NaN NaN NaN

print(df > 5)
# Output:
0 1 2
a True False False
b True False False
c True True True
d False False False
e False False False
f False False False

print(df.values)
# Output:
[['a' 2.0 3.0]
['a' 3.0 4.0]
['a' 8.0 9.0]
[nan nan nan]
[nan nan nan]
[nan nan nan]]

Python——位运算与逻辑运算和C++有什么不同

参考链接: https://blog.csdn.net/weixin_39129504/article/details/85958295


位运算(与C++相同)

按位与 &

按位或 |

按位异或 ^

按位取反 ~

移位运算 >>,<<,<<=和>>=


逻辑运算(与C++不同)


逻辑与

  • Python: and
  • C++: &&

逻辑或

  • Python: or
  • C++: ||

逻辑非

  • Python: not
  • C++: !

Python——使用pip管理包

使用 pip 管理 Python 包的常见用法如下:


管理包(安装、升级、卸载和查看等)

  • 安装最新版本:

    1
    pip install package_name
  • 安装指定版本:

    1
    pip install package_name==1.2.3
  • 从本地文件安装:

    1
    pip install /path/to/package.whl
  • 从 GitHub 安装:

    1
    pip install git+https://github.com/user/repo.git
  • 升级到最新版本:

    1
    pip install --upgrade package_name
  • 卸载包:

    1
    pip uninstall package_name
  • 查看某个包的详细信息:

    1
    pip show package_name
  • 列出所有已安装的包:

    1
    pip list

搜索包

  • 搜索包:

    1
    pip search package_name
    • 这个命令已经弃用,现在需要访问官网才行 https://pypi.org/
  • 搜索包版本:

    1
    pip index versions package_name

检查包更新

  • 检查哪些包需要更新:
    1
    pip list --outdated

配置镜像源

  • 临时使用镜像源:

    1
    pip install package_name -i https://pypi.tuna.tsinghua.edu.cn/simple
  • 永久配置镜像源:

    1
    pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

其他常用命令

  • 查看帮助:

    1
    pip --help
  • 查看版本:

    1
    pip --version

用pip批量导出或安装环境配置

  • 可以使用 pip freeze 命令导出当前 Python 环境的配置

  • 下面的pip freeze 命令将在当前目录下创建一个名为 requirements.txt 的文件,并写入当前环境中已安装的所有包及其版本信息 :

    1
    pip freeze > requirements.txt
  • 下面的命令可以在任何地方根据 requirements.txt 中列出的包及其版本信息重新安装所有必需的包,从而重建相同的环境(其中-r参数用于指定依赖性的文件名):

    1
    pip install -r requirements.txt

从源码安装包

  • 以通过源码安装transformers为例子
  • clone仓库 :git clone https://github.com/huggingface/transformers.git
  • 进入目录 :cd transformers
  • 安装依赖(准备项) :确保你的Python环境中已经安装了setuptools等必要的依赖。如果没有安装,可以先使用pip install setuptools进行安装
  • 安装(推荐方式) :pip install .
    • 注意:这种方式会将transformers安装到你的Python环境中,但如果后续你对源码进行了修改,需要重新执行安装命令才能使修改生效
    • 其他安装方式(可编辑安装) :pip install -e .命令,该命令会将克隆的仓库链接到你的Python库路径,这样Python不仅会在正常的库路径中搜索库,也会在你克隆到的文件夹中进行查找,方便修改后生效
  • 安装完成后,对于还未发行的版本,版本名称可能是带有后缀.dev等的(依赖可能识别不了)

附录:安装带 -e 的含义

  • 是否使用 -e 在安装方式和后续开发行为上有本质区别
  • 具体来说 pip install . 和 pip install -e . 都是用来安装当前目录下的 Python 包的命令,但安装本质不同

pip install . (标准安装)

  • 将当前目录(. 代表当前目录)下的 Python 包进行复制安装(静态)
  • 安装过程 :
    • 1)pip 会查找当前目录中的 setup.py 或 pyproject.toml 文件
    • 2)根据配置,将包的源代码复制到 Python 环境的 site-packages 目录下
    • 3)同时安装包的依赖项
  • 安装后,你在项目源码中做的任何修改,不会自动反映到已安装的包中,即不可编辑
    • 如果你修改了源代码,必须重新运行 pip install . 来更新已安装的包,才能让改动生效
  • 当你准备发布一个稳定版本时,通常使用这种方式

pip install -e . (可编辑安装 / 开发安装)

  • -e 是 --editable 的缩写,进行的是可编辑安装或开发安装(动态链接)
  • 安装过程 :
    • 1)pip 同样查找 setup.py 或 pyproject.toml
    • 2)它不会复制源代码,而是在 site-packages 目录下创建一个指向你当前项目目录的符号链接(symlink) 或特殊的 .pth 文件
    • 3)这样,Python 解释器在导入这个包时,会直接从你的项目源码目录加载模块
  • 你在项目源码中做的任何修改,立即生效 ,不需要重新安装,即是可编辑的
    • 非常适合在开发过程中使用,可以快速测试代码变更
  • 包的依赖项仍然会被安装到环境中
  • 包的“安装位置”就是开发目录,删除开发目录会导致包“消失”

附录:安装包的用法

常规用法

  • 使用普通命令足以

    1
    pip install package==1.1.0
  • 若已经安装有包(不管是升级还是降级均可),想要强制重新安装,使用:

    1
    pip install package==1.1.0 --force-reinstall
  • 已经有包的情况下,也可以使用:

    1
    2
    pip3 uninstall package
    pip3 install package==1.1.0

推荐用法

  • 升级包时使用 --upgrade 参数,确保升级操作能够执行

    1
    pip install --upgrade package==1.1.0
    • 若没有 --upgrade 参数可能会默认不安装,因为认为当前包已经符合版本(高于目标版本)
  • 降级包时加一个强制参数

    1
    pip install package==1.1.0 --force-reinstall
1…474849…64
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

631 posts
53 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4