Python functools 深度解析:lru_cache、wraps 与 partial 的底层逻辑

4418 字
22 分钟
Python functools 深度解析:lru_cache、wraps 与 partial 的底层逻辑

你可能在日常代码中用过 @lru_cache 做缓存加速,或者在写装饰器时加过 @wraps。但 functools 远不止这两个工具——它是 Python 标准库中最被低估的模块之一

这篇文章不会停留在”怎么用”的层面。我会带你深入到每个函数的底层实现逻辑,理解它们为什么这样设计,以及在复杂场景下如何组合使用。读完后,你会对 functools 有一个全新的认识。

一、functools 全景图:不止是装饰器#

functools 模块的核心定位是:高阶函数(Higher-Order Functions)的工具箱。所谓高阶函数,就是那些操作函数(接受函数作为参数,或返回函数)的工具。

截至 Python 3.12,functools 提供了以下核心工具:

函数/装饰器用途复杂度
wraps装饰器中保留被包装函数的元数据入门
partial冻结函数的部分参数入门
partialmethod为类方法提供部分参数绑定中级
lru_cache / cacheLRU 缓存装饰器中级
cached_property属性级别的缓存中级
singledispatch函数分派(单分派泛型)高级
singledispatchmethod方法级别的分派高级
reduce累积归约操作中级
cmp_to_key旧式比较函数 → key 函数转换中级
update_wrapperwraps 的底层实现底层

接下来我们逐个深入。

二、lru_cache:缓存装饰器的底层机制#

2.1 从暴力递归到一行加速#

斐波那契数列的暴力递归是经典的反面教材:

def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
import time
start = time.perf_counter()
fib(35)
print(f"未缓存耗时: {time.perf_counter() - start:.3f}s")
# 输出: 未缓存耗时: 3.421s

fib(35) 需要约 3.4 秒,因为 fib(30) 被计算了数百万次。加上 @lru_cache

from functools import lru_cache
@lru_cache(maxsize=None)
def fib_cached(n: int) -> int:
if n < 2:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
start = time.perf_counter()
fib_cached(35)
print(f"缓存耗时: {time.perf_counter() - start:.3f}s")
# 输出: 缓存耗时: 0.000s

从 3.4 秒到几乎瞬时。这就是缓存的威力。但 lru_cache 内部到底做了什么?

2.2 LRU 算法的双向链表实现#

LRU = Least Recently Used(最近最少使用)。核心思想是:当缓存满了,淘汰掉最久没有被访问过的项

Python 的 lru_cache 内部使用了一个双向链表 + 哈希表的组合结构(类似 collections.OrderedDict):

哈希表:O(1) 查找缓存项是否存在
双向链表:O(1) 移动最近使用的项到"最近"端
O(1) 淘汰"最久未使用"端的项
# 简化的 LRU 缓存实现(理解原理用)
from functools import wraps
from collections import OrderedDict
def simple_lru_cache(maxsize: int = 128):
def decorator(func):
cache = OrderedDict()
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
if key in cache:
# 命中:移到末尾(标记为最近使用)
cache.move_to_end(key)
return cache[key]
# 未命中:计算并存储
result = func(*args, **kwargs)
cache[key] = result
# 如果超出容量,弹出最久未使用的(头部)
if len(cache) > maxsize:
cache.popitem(last=False)
return result
# 暴露缓存统计信息
wrapper.cache_info = lambda: (
f"hits=0, misses=0, maxsize={maxsize}, currsize={len(cache)}"
)
wrapper.cache_clear = cache.clear
return wrapper
return decorator

关键设计细节lru_cache 的参数必须是可哈希的(hashable)。这是因为缓存的 key 需要存入哈希表。如果你传入了 listdict,就会报 TypeError: unhashable type

2.3 maxsize 的三种策略#

maxsize行为适用场景
None无上限缓存纯函数,参数空间有限(如 fib(n)
正整数(如 128)固定容量 LRU参数空间大,需要控制内存
0(不推荐)禁用缓存,仅统计调试用途
from functools import lru_cache
@lru_cache(maxsize=3)
def expensive_compute(x: int) -> int:
print(f" 计算 {x}...")
return x * x
print(expensive_compute(1)) # 计算 1... → 1
print(expensive_compute(2)) # 计算 2... → 4
print(expensive_compute(3)) # 计算 3... → 9
print(expensive_compute(4)) # 计算 4... → 16(缓存满了,淘汰 1)
print(expensive_compute(1)) # 计算 1... → 1(已被淘汰,重新计算)
print(expensive_compute.cache_info())
# CacheInfo(hits=0, misses=5, maxsize=3, currsize=3)

2.4 typed=True:类型敏感缓存#

默认情况下,lru_cache 认为 33.0 是同一个 key:

@lru_cache()
def check(x):
print(f" 计算 {x!r}")
return x * 2
check(3) # 计算 3...
check(3.0) # 命中缓存,不计算

但有时类型不同意味着语义不同。加上 typed=True

@lru_cache(typed=True)
def check_typed(x):
print(f" 计算 {x!r}")
return x * 2
check_typed(3) # 计算 3...
check_typed(3.0) # 计算 3.0... ← 分别缓存

2.5 Python 3.9+ 的 cache():无限缓存的快捷方式#

from functools import cache
@cache # 等价于 @lru_cache(maxsize=None)
def pure_function(x):
return x ** 2

更短、更清晰。适合纯函数场景。

2.6 缓存失效策略:手动清理#

@lru_cache(maxsize=256)
def db_query(table: str, id: int) -> dict:
# 模拟数据库查询
return {"table": table, "id": id, "data": "..."}
# 缓存会一直命中
db_query("users", 1) # 查数据库
db_query("users", 1) # 命中缓存
# 数据更新后,需要手动清除
db_query.cache_clear()
db_query("users", 1) # 重新查数据库

在生产环境中,cache_clear() 通常在以下时机调用:

  • 数据库写入操作完成后
  • 配置文件重新加载后
  • 定时任务(如每 5 分钟清除一次)

三、wraps 与 update_wrapper:装饰器的元数据守护#

3.1 问题重现#

def log_calls(func):
def wrapper(*args, **kwargs):
print(f"[LOG] 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def process_data(data: list) -> list:
"""处理数据列表,返回排序后的结果。"""
return sorted(data)
print(process_data.__name__) # wrapper ← 不是 process_data!
print(process_data.__doc__) # None ← 文档丢失!
print(process_data.__annotations__) # {} ← 类型注解丢失!

这会引发一系列问题

  • help(process_data) 显示的是 wrapper 的帮助,不是原始函数
  • inspect.signature(process_data) 获取的是 *args, **kwargs,不是原始签名
  • 自动化工具(如 Sphinx、FastAPI、Click)无法正确提取文档和类型

3.2 update_wrapper 的底层逻辑#

@wraps(func) 本质上是 update_wrapper(wrapper, func) 的快捷方式。它做了这几件事:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__',
'__annotations__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)
def my_update_wrapper(wrapper, wrapped):
# 1. 拷贝属性
for attr in WRAPPER_ASSIGNMENTS:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
# 2. 更新字典(保留 wrapper 自己的 __dict__,同时合并 wrapped 的)
for attr in WRAPPER_UPDATES:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# 3. 添加 __wrapped__ 属性,保留对原始函数的引用
wrapper.__wrapped__ = wrapped
return wrapper

关键细节__wrapped__ 属性的存在,使得 inspect.unwrap() 可以追溯到最原始的函数。这是调试和自省的关键。

3.3 实战:多层装饰器的元数据追踪#

from functools import wraps
import inspect
def decorator_a(func):
@wraps(func)
def wrapper_a(*args, **kwargs):
return func(*args, **kwargs)
return wrapper_a
def decorator_b(func):
@wraps(func)
def wrapper_b(*args, **kwargs):
return func(*args, **kwargs)
return wrapper_b
@decorator_a
@decorator_b
def target(x: int) -> int:
"""目标函数"""
return x * 2
# 直接看签名
print(inspect.signature(target)) # (x: int) -> int ✅
print(target.__wrapped__.__name__) # wrapper_b(decorator_b 的 wrapper)
# unwrap 追溯最原始函数
original = inspect.unwrap(target)
print(original.__name__) # target ✅

如果没有 @wrapsinspect.signature 得到的是 (*args, **kwargs),所有自动化工具都会失效。

四、partial:函数参数的”快照”#

4.1 基本用法#

partial 的核心思想很简单:冻结一个函数的部分参数,返回一个新函数

from functools import partial
def power(base: float, exponent: int) -> float:
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
sqrt = partial(power, exponent=0.5)
print(square(5)) # 25
print(cube(3)) # 27
print(sqrt(16)) # 4.0

partial 返回的是一个 functools.partial 对象,它保存了原始函数和已绑定的参数。

4.2 内部实现原理#

class MyPartial:
def __init__(self, func, /, *args, **keywords):
self.func = func
self.args = args
self.keywords = keywords
def __call__(self, /, *args, **keywords):
# 合并已绑定的参数和新传入的参数
keywords = {**self.keywords, **keywords}
return self.func(*self.args, *args, **keywords)
def __repr__(self):
return f"partial({self.func.__name__}, args={self.args}, keywords={self.keywords})"

当你调用 square(5) 时,实际执行的是 power(5, exponent=2)

4.3 实战场景一:回调函数#

import tkinter as tk
from functools import partial
def on_button_click(button_id: int):
print(f"按钮 {button_id} 被点击")
root = tk.Tk()
for i in range(1, 6):
# ❌ 错误:lambda 会捕获循环变量 i 的最终值
# btn = tk.Button(root, text=f"按钮{i}", command=lambda: on_button_click(i))
# ✅ 正确:partial 在创建时就固定了参数
btn = tk.Button(root, text=f"按钮{i}", command=partial(on_button_click, i))
btn.pack()
root.mainloop()

这是 partial 最经典的用途之一:避免闭包延迟绑定陷阱

4.4 实战场景二:排序的 key 函数#

from functools import partial
data = [
{"name": "Alice", "score": 90},
{"name": "Bob", "score": 85},
{"name": "Charlie", "score": 95},
]
# 按 score 字段排序
sorted_by_score = sorted(data, key=partial(dict.get, "score"))
print(sorted_by_score)
# [{'name': 'Bob', 'score': 85}, {'name': 'Alice', 'score': 90}, ...]

4.5 partialmethod:为类方法绑定参数#

from functools import partialmethod
class Cell:
def __init__(self):
self.alive = False
def set_state(self, state: str):
self.alive = (state == "ALIVE")
# 预设两个便捷方法
set_alive = partialmethod(set_state, "ALIVE")
set_dead = partialmethod(set_state, "DEAD")
cell = Cell()
cell.set_alive()
print(cell.alive) # True
cell.set_dead()
print(cell.alive) # False

这在定义大量状态切换方法时非常有用,比如游戏引擎中的实体状态机。

五、singledispatch:Python 的函数重载#

5.1 问题:Python 不支持函数重载#

在其他语言中,你可以这样写:

// Java — 函数重载
void handle(String s) { ... }
void handle(int n) { ... }
void handle(List l) { ... }

但 Python 不支持原生重载——同名函数会互相覆盖。singledispatch 提供了另一种思路:基于第一个参数的类型,分发到不同的实现

5.2 基本用法#

from functools import singledispatch
@singledispatch
def process(data):
"""默认实现"""
raise NotImplementedError(f"不支持的类型: {type(data)}")
@process.register
def _(data: str):
print(f"处理字符串: {data.upper()}")
return len(data)
@process.register
def _(data: int):
print(f"处理整数: {data * 2}")
return data * 2
@process.register
def _(data: list):
print(f"处理列表: 共 {len(data)} 个元素")
return sum(data)
process("hello") # 处理字符串: HELLO → 5
process(42) # 处理整数: 84 → 84
process([1, 2, 3]) # 处理列表: 共 3 个元素 → 6
process(3.14) # NotImplementedError: 不支持的类型: <class 'float'>

5.3 底层机制:类型注册表#

singledispatch 内部维护了一个类型到函数的映射表

# 简化的 singledispatch 实现
class SimpleDispatcher:
def __init__(self, default_func):
self.default = default_func
self.registry = {}
def register(self, type_hint, func=None):
if func is None:
# @dispatch.register 作为装饰器使用
def decorator(f):
self.registry[type_hint] = f
return f
return decorator
# @dispatch.register(SomeType, func) 直接注册
self.registry[type_hint] = func
def __call__(self, arg, *args, **kwargs):
# 查找 arg 类型对应的处理函数
arg_type = type(arg)
handler = self.registry.get(arg_type, self.default)
return handler(arg, *args, **kwargs)

Python 的实际实现更复杂——它支持类型继承查找(如果找不到精确匹配,会沿着 MRO 查找最近的注册类型),还支持 Union 类型注册(Python 3.11+)。

5.4 实战:序列化器#

from functools import singledispatch
from datetime import datetime
from enum import Enum
class Status(Enum):
OK = "ok"
FAIL = "fail"
@singledispatch
def serialize(obj):
return str(obj)
@serialize.register
def _(obj: datetime):
return obj.isoformat()
@serialize.register
def _(obj: Enum):
return obj.value
@serialize.register(list)
def _(obj: list):
return [serialize(item) for item in obj]
@serialize.register(dict)
def _(obj: dict):
return {k: serialize(v) for k, v in obj.items()}
# 测试
data = {
"name": "测试",
"time": datetime(2026, 5, 30, 12, 0),
"status": Status.OK,
"items": [1, datetime(2026, 1, 1), Status.FAIL],
}
print(serialize(data))
# {'name': '测试', 'time': '2026-05-30T12:00:00', 'status': 'ok', 'items': [1, '2026-01-01T00:00:00', 'fail']}

这是一个简单但实用的递归序列化器,通过类型分派避免了大量的 isinstance() 判断。

六、reduce:函数式编程的归约操作#

6.1 从求和到任意累积#

reduce 源自函数式编程传统,它把一个序列”归约”为一个值:

from functools import reduce
# 求和
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda x, y: x + y, numbers)
print(total) # 15
# 求积
product = reduce(lambda x, y: x * y, numbers, 1)
print(product) # 120

6.2 执行过程图解#

reduce(f, [a, b, c, d], init)
步骤 1: f(init, a) → result1
步骤 2: f(result1, b) → result2
步骤 3: f(result2, c) → result3
步骤 4: f(result3, d) → result4(最终结果)
如果没有 init,则从 f(a, b) 开始。

6.3 实战:嵌套字典的路径访问#

def get_nested(data: dict, path: str, default=None):
"""通过 'a.b.c' 路径访问嵌套字典"""
keys = path.split(".")
try:
return reduce(lambda d, key: d[key], keys, data)
except (KeyError, TypeError):
return default
config = {
"server": {
"database": {
"host": "localhost",
"port": 5432,
}
}
}
print(get_nested(config, "server.database.host")) # localhost
print(get_nested(config, "server.database.port")) # 5432
print(get_nested(config, "server.missing.key")) # None

6.4 为什么 Guido 不喜欢 reduce?#

Python 之父 Guido van Rossum 曾说:“除了 operator.addoperator.mul,我几乎想不出 reduce 的非抽象用例。“他建议用 for 循环替代:

# reduce 写法(需要脑内模拟)
result = reduce(lambda acc, x: acc + x * 2, numbers, 0)
# for 循环写法(一目了然)
result = 0
for x in numbers:
result += x * 2

我的观点reduce 适合表达明确的归约语义(如链式操作、管道处理),但在做简单累加时,for 循环或 sum() 更具可读性。选择的标准是:阅读这段代码的人能否在 3 秒内理解意图?

七、cmp_to_key:旧式比较函数的现代适配#

7.1 历史背景#

Python 2 的 sort() 支持 cmp 参数——一个接受两个参数、返回负数/零/正数的比较函数:

# Python 2 风格(Python 3 已移除 cmp 参数)
def compare(a, b):
if a[1] > b[1]:
return -1 # a 排在前面
elif a[1] < b[1]:
return 1 # b 排在前面
else:
return 0

Python 3 移除了 cmp,改用 key。但如果你有一段遗留代码或库使用了 cmpcmp_to_key 可以桥接:

from functools import cmp_to_key
def compare(a, b):
"""按第二个元素降序排列"""
if a[1] > b[1]:
return -1
elif a[1] < b[1]:
return 1
return 0
data = [("apple", 3), ("banana", 1), ("cherry", 2)]
sorted_data = sorted(data, key=cmp_to_key(compare))
print(sorted_data)
# [('apple', 3), ('cherry', 2), ('banana', 1)]

7.2 底层原理:Key 类#

cmp_to_key 返回一个,这个类实现了比较魔术方法:

class CmpKey:
def __init__(self, obj, cmp):
self.obj = obj
self.cmp = cmp
def __lt__(self, other):
return self.cmp(self.obj, other.obj) < 0
def __gt__(self, other):
return self.cmp(self.obj, other.obj) > 0
def __eq__(self, other):
return self.cmp(self.obj, other.obj) == 0
def __le__(self, other):
return self.cmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return self.cmp(self.obj, other.obj) >= 0

排序时,Python 将每个元素包装成 CmpKey 对象,然后通过 __lt__ 等方法进行比较。这是一个适配器模式的经典例子。

八、cached_property:惰性属性的缓存#

8.1 与 lru_cache 的区别#

cached_property 缓存的是实例属性,不是函数调用结果。它只在属性首次访问时计算,之后直接返回缓存值:

from functools import cached_property
import time
class DataSet:
def __init__(self, data: list[int]):
self.data = data
@cached_property
def mean(self) -> float:
print(" 计算平均值...")
time.sleep(1) # 模拟耗时计算
return sum(self.data) / len(self.data)
@cached_property
def std_dev(self) -> float:
print(" 计算标准差...")
time.sleep(1)
m = self.mean # 不会重新计算!
return (sum((x - m) ** 2 for x in self.data) / len(self.data)) ** 0.5
ds = DataSet([1, 2, 3, 4, 5])
# 第一次访问
print(ds.mean) # 计算平均值... → 3.0
print(ds.std_dev) # 计算标准差... → 1.414...
# 第二次访问(命中缓存,不重新计算)
print(ds.mean) # → 3.0(没有打印"计算平均值...")

8.2 与 @property 的对比#

特性@property@cached_property
每次访问重新执行仅执行一次
适用场景动态计算、轻量级昂贵计算、结果不变
内存开销存储计算结果
线程安全取决于实现非线程安全

8.3 缓存失效:删除属性#

ds = DataSet([1, 2, 3])
print(ds.mean) # 计算... → 2.0
# 删除缓存,下次访问会重新计算
del ds.__dict__["mean"]
print(ds.mean) # 再次计算... → 2.0

九、综合实战:构建一个小型 ORM 查询构建器#

下面我们把 functools 的多个工具组合起来,构建一个实用的查询构建器:

from functools import lru_cache, singledispatch, partial, wraps
from typing import Any
# 1. singledispatch:根据类型生成 SQL 值字面量
@singledispatch
def sql_literal(value: Any) -> str:
return str(value)
@sql_literal.register
def _(value: str) -> str:
return f"'{value.replace(chr(39), chr(39)+chr(39))}'" # 转义单引号
@sql_literal.register(type(None))
def _(value: None) -> str:
return "NULL"
@sql_literal.register
def _(value: bool) -> str:
return "1" if value else "0"
# 2. partial:预定义常用的查询条件
eq = partial(lambda col, val: f"{col} = {sql_literal(val)}", val=None) # placeholder
like = partial(lambda col, val: f"{col} LIKE {sql_literal(val)}", val=None)
def where_eq(col: str, val: Any) -> str:
return f"{col} = {sql_literal(val)}"
def where_in(col: str, vals: list) -> str:
literals = ", ".join(sql_literal(v) for v in vals)
return f"{col} IN ({literals})"
# 3. lru_cache:缓存表元数据
@lru_cache(maxsize=64)
def get_table_columns(table_name: str) -> list[str]:
"""模拟从数据库获取表结构(实际应用中会查 information_schema)"""
schema = {
"users": ["id", "name", "email", "age", "is_active"],
"orders": ["id", "user_id", "amount", "status", "created_at"],
}
return schema.get(table_name, [])
# 4. cached_property:惰性构建查询
class QueryBuilder:
def __init__(self, table: str):
self.table = table
self._conditions: list[str] = []
self._select_fields: list[str] = ["*"]
def select(self, *fields: str) -> "QueryBuilder":
self._select_fields = list(fields) if fields else ["*"]
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
@property
def sql(self) -> str:
cols = ", ".join(self._select_fields)
query = f"SELECT {cols} FROM {self.table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
return query + ";"
# 使用示例
qb = QueryBuilder("users")
qb.select("name", "email")
qb.where(where_eq("age", 25))
qb.where(where_in("name", ["Alice", "Bob"]))
print(qb.sql)
# SELECT name, email FROM users WHERE age = 25 AND name IN ('Alice', 'Bob');
# 缓存的列信息
print(get_table_columns("users"))
# ['id', 'name', 'email', 'age', 'is_active']

这个例子展示了 functools 中多个工具的协同工作

  • singledispatch 处理不同类型值的 SQL 字面量生成
  • partial 预定义常用查询模板
  • lru_cache 缓存表元数据,避免重复查询
  • @property 惰性构建最终 SQL

十、常见陷阱与最佳实践#

10.1 lru_cache 的内存泄漏#

# ⚠️ 危险:无上限缓存 + 无限参数空间
@lru_cache(maxsize=None)
def process_user_request(user_id: int, timestamp: float, request_data: str) -> dict:
# 每次请求的 timestamp 都不同 → 缓存永远不命中且无限增长!
pass

规则:只有纯函数 + 有限参数空间才用 maxsize=None。否则设一个合理的上限。

10.2 lru_cache 与可变默认参数#

# ⚠️ 危险:缓存的 key 不包含可变对象的内容
@lru_cache()
def process(data: tuple) -> int:
return len(data)
# 如果你传 list,必须转 tuple 才能作为 key
# 更好的做法是在函数内部做类型转换

10.3 装饰器顺序很重要#

@lru_cache()
@log_calls # log_calls 在外层
def expensive(x):
return x * x
# 执行顺序:log_calls → lru_cache → expensive
# 每次调用都会打印日志,即使命中缓存

如果反过来:

@log_calls
@lru_cache() # lru_cache 在外层
def expensive(x):
return x * x
# 执行顺序:lru_cache → log_calls → expensive
# 只有未命中缓存时才打印日志 ✅ 更高效

规则:缓存装饰器应该放在最外层(离 def 最远的位置)。

10.4 测试时的缓存污染#

import pytest
@pytest.fixture(autouse=True)
def clear_caches():
"""每个测试前清除所有 lru_cache"""
yield
expensive_function.cache_clear()
db_query.cache_clear()

在测试中使用 lru_cache 时,记得在 setUp 或 fixture 中调用 cache_clear(),否则测试之间会互相影响。

十一、总结#

functools 是 Python 标准库中最实用的模块之一。掌握它,你就能:

工具核心价值
lru_cache / cache一行代码实现函数级缓存,性能提升可达数量级
wraps / update_wrapper装饰器的正确写法,保护函数元数据
partial / partialmethod参数绑定,消除 lambda 闭包陷阱
singledispatch类型驱动的分派,替代 if-elif-else 类型判断
cached_property惰性属性缓存,昂贵计算只执行一次
reduce序列归约,适合链式管道操作
cmp_to_key旧式比较函数的适配器

延伸阅读#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Python functools 深度解析:lru_cache、wraps 与 partial 的底层逻辑
https://boke.hackerdream.xyz/posts/python-functools-deep/
作者
晴天
发布于
2026-05-30
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
155
分类
24
标签
387
总字数
345,424
运行时长
0
最后活动
0 天前

目录