- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
14.9 标准库中的生成器函数
标准库提供了很多生成器,有用于逐行迭代纯文本文件的对象,还有出色的 os.walk 函数。这个函数在遍历目录树的过程中产出文件名,因此递归搜索文件系统像 for 循环那样简单。
os.walk 生成器函数的作用令人赞叹,不过本节专注于通用的函数:参数为任意的可迭代对象,返回值是生成器,用于生成选中的、计算出的和重新排列的元素。在下述几个表格中,我会概述其中的 24 个,有些是内置的,有些在 itertools 和 functools 模块中。为了方便,我按照函数的高阶功能分组,而不管函数是在哪里定义的。
你可能知道本节所述的全部函数,但是某些函数没有得到充分利用,因此快速概览一遍能让你知道有什么函数可用。
第一组是用于过滤的生成器函数:从输入的可迭代对象中产出元素的子集,而且不修改元素本身。本章前面的 14.8.1 节用过 itertools.takewhile 函数。与 takewhile 函数一样,表 14-1 中的大多数函数都接受一个断言参数(predicate)。这个参数是个布尔函数,有一个参数,会应用到输入中的每个元素上,用于判断元素是否包含在输出中。
表14-1:用于过滤的生成器函数
模块 | 函数 | 说明 |
itertools | compress(it, selector_it) | 并行处理两个可迭代的对象;如果 selector_it 中的元素是真值,产出 it 中对应的元素 |
itertools | dropwhile(predicate, it) | 处理 it,跳过 predicate 的计算结果为真值的元素,然后产出剩下的各个元素(不再进一步检查) |
(内置) | filter(predicate, it) | 把 it 中的各个元素传给 predicate,如果 predicate(item) 返回真值,那么产出对应的元素;如果 predicate 是 None,那么只产出真值元素 |
itertools | filterfalse(predicate, it) | 与 filter 函数的作用类似,不过 predicate 的逻辑是相反的:predicate 返回假值时产出对应的元素 |
itertools | islice(it, stop) 或 islice(it, start, stop, step=1) | 产出 it 的切片,作用类似于 s[:stop] 或 s[start:stop:step],不过 it 可以是任何可迭代的对象,而且这个函数实现的是惰性操作 |
itertools | takewhile(predicate, it) | predicate 返回真值时产出对应的元素,然后立即停止,不再继续检查 |
示例 14-14 在控制台中演示表 14-1 中各个函数的用法。
示例 14-14 演示用于过滤的生成器函数
>>> def vowel(c): ... return c.lower() in 'aeiou' ... >>> list(filter(vowel, 'Aardvark')) ['A', 'a', 'a'] >>> import itertools >>> list(itertools.filterfalse(vowel, 'Aardvark')) ['r', 'd', 'v', 'r', 'k'] >>> list(itertools.dropwhile(vowel, 'Aardvark')) ['r', 'd', 'v', 'a', 'r', 'k'] >>> list(itertools.takewhile(vowel, 'Aardvark')) ['A', 'a'] >>> list(itertools.compress('Aardvark', (1,0,1,1,0,1))) ['A', 'r', 'd', 'a'] >>> list(itertools.islice('Aardvark', 4)) ['A', 'a', 'r', 'd'] >>> list(itertools.islice('Aardvark', 4, 7)) ['v', 'a', 'r'] >>> list(itertools.islice('Aardvark', 1, 7, 2)) ['a', 'd', 'a']
下一组是用于映射的生成器函数:在输入的单个可迭代对象(map 和 starmap 函数处理多个可迭代的对象)中的各个元素上做计算,然后返回结果。12 表 14-2 中的生成器函数会从输入的可迭代对象中的各个元素中产出一个元素。如果输入来自多个可迭代的对象,第一个可迭代的对象到头后就停止输出。
12这里所说的“映射”与字典没有关系,而与内置的 map 函数有关。
表14-2:用于映射的生成器函数
模块 | 函数 | 说明 |
itertools | accumulate(it, [func]) | 产出累积的总和;如果提供了 func,那么把前两个元素传给它,然后把计算结果和下一个元素传给它,以此类推,最后产出结果 |
(内置) | enumerate(iterable, start=0) | 产出由两个元素组成的元组,结构是 (index, item),其中 index 从 start 开始计数,item 则从 iterable 中获取 |
(内置) | map(func, it1, [it2, ..., itN]) | 把 it 中的各个元素传给func,产出结果;如果传入 N 个可迭代的对象,那么 func 必须能接受 N 个参数,而且要并行处理各个可迭代的对象 |
itertools | starmap(func, it) | 把 it 中的各个元素传给 func,产出结果;输入的可迭代对象应该产出可迭代的元素 iit,然后以 func(*iit) 这种形式调用 func |
示例 14-15 演示 itertools.accumulate 函数的几个用法。
示例 14-15 演示 itertools.accumulate 生成器函数
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1] >>> import itertools >>> list(itertools.accumulate(sample)) # ➊ [5, 9, 11, 19, 26, 32, 35, 35, 44, 45] >>> list(itertools.accumulate(sample, min)) # ➋ [5, 4, 2, 2, 2, 2, 2, 0, 0, 0] >>> list(itertools.accumulate(sample, max)) # ➌ [5, 5, 5, 8, 8, 8, 8, 8, 9, 9] >>> import operator >>> list(itertools.accumulate(sample, operator.mul)) # ➍ [5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0] >>> list(itertools.accumulate(range(1, 11), operator.mul)) [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] # ➎
❶ 计算总和。
❷ 计算最小值。
❸ 计算最大值。
❹ 计算乘积。
❺ 从 1! 到 10!,计算各个数的阶乘。
表 14-2 中剩余函数的演示如示例 14-16 所示。
示例 14-16 演示用于映射的生成器函数
>>> list(enumerate('albatroz', 1)) # ➊ [(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')] >>> import operator >>> list(map(operator.mul, range(11), range(11))) # ➋ [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100] >>> list(map(operator.mul, range(11), [2, 4, 8])) # ➌ [0, 4, 16] >>> list(map(lambda a, b: (a, b), range(11), [2, 4, 8])) # ➍ [(0, 2), (1, 4), (2, 8)] >>> import itertools >>> list(itertools.starmap(operator.mul, enumerate('albatroz', 1))) # ➎ ['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz'] >>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1] >>> list(itertools.starmap(lambda a, b: b/a, ... enumerate(itertools.accumulate(sample), 1))) # ➏ [5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333, 5.0, 4.375, 4.888888888888889, 4.5]
❶ 从 1 开始,为单词中的字母编号。
❷ 从 0 到 10,计算各个整数的平方。
❸ 计算两个可迭代对象中对应位置上的两个元素之积,元素最少的那个可迭代对象到头后就停止。
❹ 作用等同于内置的 zip 函数。
❺ 从 1 开始,根据字母所在的位置,把字母重复相应的次数。
❻ 计算平均值。
接下来这一组是用于合并的生成器函数,这些函数都从输入的多个可迭代对象中产出元素。chain 和 chain.from_iterable 按顺序(一个接一个)处理输入的可迭代对象,而 product、zip 和 zip_longest 并行处理输入的各个可迭代对象。如表 14-3 所示。
表14-3:合并多个可迭代对象的生成器函数
模块 | 函数 | 说明 |
itertools | chain(it1, ..., itN) | 先产出 it1 中的所有元素,然后产出 it2 中的所有元素,以此类推,无缝连接在一起 |
itertools | chain.from_iterable(it) | 产出 it 生成的各个可迭代对象中的元素,一个接一个,无缝连接在一起;it 应该产出可迭代的元素,例如可迭代的对象列表 |
itertools | product(it1, ..., itN, repeat=1) | 计算笛卡儿积:从输入的各个可迭代对象中获取元素,合并成由 N 个元素组成的元组,与嵌套的 for 循环效果一样;repeat 指明重复处理多少次输入的可迭代对象 |
(内置) | zip(it1, ..., itN) | 并行从输入的各个可迭代对象中获取元素,产出由 N 个元素组成的元组,只要有一个可迭代的对象到头了,就默默地停止 |
itertools | zip_longest(it1, ..., itN, fillvalue=None) | 并行从输入的各个可迭代对象中获取元素,产出由 N 个元素组成的元组,等到最长的可迭代对象到头后才停止,空缺的值使用 fillvalue 填充 |
示例 14-17 展示 itertools.chain 和 zip 生成器函数及其同胞的用法。再次提醒,zip 函数的名称出自 zip fastener 或 zipper(拉链,与 ZIP 压缩没有关系)。“出色的 zip 函数”附注栏介绍过 zip 和 itertools.zip_longest 函数。
示例 14-17 演示用于合并的生成器函数
>>> list(itertools.chain('ABC', range(2))) # ➊ ['A', 'B', 'C', 0, 1] >>> list(itertools.chain(enumerate('ABC'))) # ➋ [(0, 'A'), (1, 'B'), (2, 'C')] >>> list(itertools.chain.from_iterable(enumerate('ABC'))) # ➌ [0, 'A', 1, 'B', 2, 'C'] >>> list(zip('ABC', range(5))) # ➍ [('A', 0), ('B', 1), ('C', 2)] >>> list(zip('ABC', range(5), [10, 20, 30, 40])) # ➎ [('A', 0, 10), ('B', 1, 20), ('C', 2, 30)] >>> list(itertools.zip_longest('ABC', range(5))) # ➏ [('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)] >>> list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # ➐ [('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
❶ 调用 chain 函数时通常传入两个或更多个可迭代对象。
❷ 如果只传入一个可迭代的对象,那么 chain 函数没什么用。
❸ 但是 chain.from_iterable 函数从可迭代的对象中获取每个元素,然后按顺序把元素连接起来,前提是各个元素本身也是可迭代的对象。
❹ zip 常用于把两个可迭代的对象合并成一系列由两个元素组成的元组。
❺ zip 可以并行处理任意数量个可迭代的对象,不过只要有一个可迭代的对象到头了,生成器就停止。
❻ itertools.zip_longest 函数的作用与 zip 类似,不过输入的所有可迭代对象都会处理到头,如果需要会填充 None。
❼ fillvalue 关键字参数用于指定填充的值。
itertools.product 生成器是计算笛卡儿积的惰性方式;在 2.2.3 节,我们在多个 for 子句中使用列表推导计算过笛卡儿积。此外,也可以使用包含多个 for 子句的生成器表达式,以惰性方式计算笛卡儿积。示例 14-18 演示 itertools.product 函数的用法。
示例 14-18 演示 itertools.product 生成器函数
>>> list(itertools.product('ABC', range(2))) # ➊ [('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)] >>> suits = 'spades hearts diamonds clubs'.split() >>> list(itertools.product('AK', suits)) # ➋ [('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'), ('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')] >>> list(itertools.product('ABC')) # ➌ [('A',), ('B',), ('C',)] >>> list(itertools.product('ABC', repeat=2)) # ➍ [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] >>> list(itertools.product(range(2), repeat=3)) [(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)] >>> rows = itertools.product('AB', range(2), repeat=2) >>> for row in rows: print(row) ... ('A', 0, 'A', 0) ('A', 0, 'A', 1) ('A', 0, 'B', 0) ('A', 0, 'B', 1) ('A', 1, 'A', 0) ('A', 1, 'A', 1) ('A', 1, 'B', 0) ('A', 1, 'B', 1) ('B', 0, 'A', 0) ('B', 0, 'A', 1) ('B', 0, 'B', 0) ('B', 0, 'B', 1) ('B', 1, 'A', 0) ('B', 1, 'A', 1) ('B', 1, 'B', 0) ('B', 1, 'B', 1)
❶ 三个字符的字符串与两个整数的值域得到的笛卡儿积是六个元组(因为 3 * 2 等于 6)。
❷ 两张牌('AK')与四种花色得到的笛卡儿积是八个元组。
❸ 如果传入一个可迭代的对象,product 函数产出的是一系列只有一个元素的元组,不是特别有用。
❹ repeat=N 关键字参数告诉 product 函数重复 N 次处理输入的各个可迭代对象。
有些生成器函数会从一个元素中产出多个值,扩展输入的可迭代对象,如表 14-4 所示。
表14-4:把输入的各个元素扩展成多个输出元素的生成器函数
模块 | 函数 | 说明 |
itertools | combinations(it, out_len) | 把 it 产出的 out_len 个元素组合在一起,然后产出 |
itertools | combinations_with_replacement(it, out_len) | 把 it 产出的 out_len 个元素组合在一起,然后产出,包含相同元素的组合 |
itertools | count(start=0, step=1) | 从 start 开始不断产出数字,按 step 指定的步幅增加 |
itertools | cycle(it) | 从 it 中产出各个元素,存储各个元素的副本,然后按顺序重复不断地产出各个元素 |
itertools | permutations(it, out_len=None) | 把 out_len 个 it 产出的元素排列在一起,然后产出这些排列;out_len 的默认值等于 len(list(it)) |
itertools | repeat(item, [times]) | 重复不断地产出指定的元素,除非提供 times,指定次数 |
itertools 模块中的 count 和 repeat 函数返回的生成器“无中生有”:这两个函数都不接受可迭代的对象作为输入。14.8.1 节见过 itertools.count 函数。cycle 生成器会备份输入的可迭代对象,然后重复产出对象中的元素。示例 14-19 演示 count、repeat 和 cycle 的用法。
示例 14-19 演示 count、repeat 和 cycle 的用法
>>> ct = itertools.count() # ➊ >>> next(ct) # ➋ 0 >>> next(ct), next(ct), next(ct) # ➌ (1, 2, 3) >>> list(itertools.islice(itertools.count(1, .3), 3)) # ➍ [1, 1.3, 1.6] >>> cy = itertools.cycle('ABC') # ➎ >>> next(cy) 'A' >>> list(itertools.islice(cy, 7)) # ➏ ['B', 'C', 'A', 'B', 'C', 'A', 'B'] >>> rp = itertools.repeat(7) # ➐ >>> next(rp), next(rp) (7, 7) >>> list(itertools.repeat(8, 4)) # ➑ [8, 8, 8, 8] >>> list(map(operator.mul, range(11), itertools.repeat(5))) # ➒ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
❶ 使用 count 函数构建 ct 生成器。
❷ 获取 ct 中的第一个元素。
❸ 不能使用 ct 构建列表,因为 ct 是无穷的,所以我获取接下来的 3 个元素。
❹ 如果使用 islice 或 takewhile 函数做了限制,可以从 count 生成器中构建列表。
❺ 使用 'ABC' 构建一个 cycle 生成器,然后获取第一个元素——'A'。
❻ 只有受到 islice 函数的限制,才能构建列表;这里获取接下来的 7 个元素。
❼ 构建一个 repeat 生成器,始终产出数字 7。
❽ 传入 times 参数可以限制 repeat 生成器生成的元素数量:这里会生成 4 次数字 8。
❾ repeat 函数的常见用途:为 map 函数提供固定参数,这里提供的是乘数 5。
在 itertools 模块的文档中,combinations、combinations_with_replacement 和 permutations 生成器函数,连同 product 函数,称为组合学生成器(combinatoric generator)。itertools.product 函数和其余的组合学函数有紧密的联系,如示例 14-20 所示。
示例 14-20 组合学生成器函数会从输入的各个元素中产出多个值
>>> list(itertools.combinations('ABC', 2)) # ➊ [('A', 'B'), ('A', 'C'), ('B', 'C')] >>> list(itertools.combinations_with_replacement('ABC', 2)) # ➋ [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')] >>> list(itertools.permutations('ABC', 2)) # ➌ [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] >>> list(itertools.product('ABC', repeat=2)) # ➍ [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
❶ 'ABC' 中每两个元素(len()==2)的各种组合;在生成的元组中,元素的顺序无关紧要(可以视作集合)。
❷ 'ABC' 中每两个元素(len()==2)的各种组合,包括相同元素的组合。
❸ 'ABC' 中每两个元素(len()==2)的各种排列;在生成的元组中,元素的顺序有重要意义。
❹ 'ABC' 和 'ABC'(repeat=2 的效果)的笛卡儿积。
本节要讲的最后一组生成器函数用于产出输入的可迭代对象中的全部元素,不过会以某种方式重新排列。其中有两个函数会返回多个生成器,分别是 itertools.groupby 和 itertools.tee。这一组里的另一个生成器函数,内置的 reversed 函数,是本节所述的函数中唯一一个不接受可迭代的对象,而只接受序列为参数的函数。这在情理之中,因为 reversed 函数从后向前产出元素,而只有序列的长度已知时才能工作。不过,这个函数会按需产出各个元素,因此无需创建反转的副本。我把 itertools.product 函数划分为用于合并的生成器,列在表 14-3 中,因为那一组函数都处理多个可迭代的对象,而表 14-5 中的生成器最多只能接受一个可迭代的对象。
表14-5:用于重新排列元素的生成器函数
模块 | 函数 | 说明 |
itertools | groupby(it,key=None) | 产出由两个元素组成的元素,形式为 (key, group),其中 key 是分组标准,group 是生成器,用于产出分组里的元素 |
(内置) | reversed(seq) | 从后向前,倒序产出 seq 中的元素;seq 必须是序列,或者是实现了 __reversed__ 特殊方法的对象 |
itertools | tee(it, n=2) | 产出一个由 n 个生成器组成的元组,每个生成器用于单独产出输入的可迭代对象中的元素 |
示例 14-21 演示 itertools.groupby 函数和内置的 reversed 函数的用法。注意,itertools.groupby 假定输入的可迭代对象要使用分组标准排序;即使不排序,至少也要使用指定的标准分组各个元素。
示例 14-21 itertools.groupby 函数的用法
>>> list(itertools.groupby('LLLLAAGGG')) # ➊ [('L', <itertools._grouper object at 0x102227cc0>), ('A', <itertools._grouper object at 0x102227b38>), ('G', <itertools._grouper object at 0x102227b70>)] >>> for char, group in itertools.groupby('LLLLAAAGG'): # ➋ ... print(char, '->', list(group)) ... L -> ['L', 'L', 'L', 'L'] A -> ['A', 'A',] G -> ['G', 'G', 'G'] >>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', ... 'bat', 'dolphin', 'shark', 'lion'] >>> animals.sort(key=len) # ➌ >>> animals ['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin'] >>> for length, group in itertools.groupby(animals, len): # ➍ ... print(length, '->', list(group)) ... 3 -> ['rat', 'bat'] 4 -> ['duck', 'bear', 'lion'] 5 -> ['eagle', 'shark'] 7 -> ['giraffe', 'dolphin'] >>> for length, group in itertools.groupby(reversed(animals), len): # ➎ ... print(length, '->', list(group)) ... 7 -> ['dolphin', 'giraffe'] 5 -> ['shark', 'eagle'] 4 -> ['lion', 'bear', 'duck'] 3 -> ['bat', 'rat'] >>>
❶ groupby 函数产出 (key, group_generator) 这种形式的元组。
❷ 处理 groupby 函数返回的生成器要嵌套迭代:这里在外层使用 for 循环,内层使用列表推导。
❸ 为了使用 groupby 函数,要排序输入;这里按照单词的长度排序。
❹ 再次遍历 key 和 group 值对,把 key 显示出来,并把 group 扩展成列表。
❺ 这里使用 reverse 生成器从右向左迭代 animals。
这一组里的最后一个生成器函数是 iterator.tee,这个函数只有一个作用:从输入的一个可迭代对象中产出多个生成器,每个生成器都可以产出输入的各个元素。产出的生成器可以单独使用,如示例 14-22 所示。
示例 14-22 itertools.tee 函数产出多个生成器,每个生成器都可以产出输入的各个元素
>>> list(itertools.tee('ABC')) [<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>] >>> g1, g2 = itertools.tee('ABC') >>> next(g1) 'A' >>> next(g2) 'A' >>> next(g2) 'B' >>> list(g1) ['B', 'C'] >>> list(g2) ['C'] >>> list(zip(*itertools.tee('ABC'))) [('A', 'A'), ('B', 'B'), ('C', 'C')]
注意,这一节的示例多次把不同的生成器函数组合在一起使用。这是这些函数的优秀特性:这些函数的参数都是生成器,而返回的结果也是生成器,因此能以很多不同的方式结合在一起使用。
既然讲到了这个话题,那就介绍一下 Python 3.3 中新出现的 yield from 语句。这个语句的作用就是把不同的生成器结合在一起使用。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论