Python 工匠:编写地道循环的两个建议 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
piglei
V2EX    Python

Python 工匠:编写地道循环的两个建议

  •  
  •   piglei
    piglei 2019-04-27 18:30:36 +08:00 5258 次点击
    这是一个创建于 2435 天前的主题,其中的信息可能已经有所发展或是发生改变。

    不知不觉,已经把自己开的坑《 Python 工匠系列》更新到第 7 篇了。回想在 V2EX 上面发第 1 篇文章,已经是快三年以前的事情了。再次感谢当时在第 1 篇文章下面给过意见和鼓励的朋友们。

    系列会继续更新,欢迎通过下面的索引地址关注我或者订阅我的博客。

    谢谢。


    所有文章链接:


    前言

    这是 “ Python 工匠”系列的第 7 篇文章。[查看系列所有文章]

    循环是一种常用的程序控制结构。我们常说,机器相比人类的最大优点之一,就是机器可以不眠不休的重复做某件事情,但人却不行。而**“循环”**,则是实现让机器不断重复工作的关键概念。

    在循环语法方面,Python 表现的即传统又不传统。它虽然抛弃了常见的 for (init; condition; incrment) 三段式结构,但还是选择了 forwhile 这两个经典的关键字来表达循环。绝大多数情况下,我们的循环需求都可以用 for <item> in <iterable> 来满足,while <condition> 相比之下用的则更少些。

    虽然循环的语法很简单,但是要写好它确并不容易。在这篇文章里,我们将探讨什么是“地道”的循环代码,以及如何编写它们。

    什么是“地道”的循环?

    “地道”这个词,通常被用来形容某人做某件事情时,非常符合当地传统,做的非常好。打个比方,你去参加一个朋友聚会,同桌的有一位广东人,对方一开口,句句都是标准京腔、完美儿化音。那你可以对她说:“您的北京话说的真地道”。

    既然“地道”这个词形容的经常是口音、做菜的口味这类实实在在的东西,那“地道”的循环代码又是什么意思呢?让我拿一个经典的例子来解释一下。

    如果你去问一位刚学习 Python 一个月的人:“如何在遍历一个列表的同时获取当前下标?”。他可能会交出这样的代码:

    index = 0 for name in names: print(index, name) index += 1 

    上面的循环虽然没错,但它确一点都不“地道”。一个拥有三年 Python 开发经验的人会说,代码应该这么写:

    for i, name in enumerate(names): print(i, name) 

    enumerate() 是 Python 的一个内置函数,它接收一个“可迭代”对象作为参数,然后返回一个不断生成 (当前下标, 当前元素) 的新可迭代对象。这个场景使用它最适合不过。

    所以,在上面的例子里,我们会认为第二段循环代码比第一段更“地道”。因为它用更直观的代码,更聪明的完成了工作。

    enumerate() 所代表的编程思路

    不过,判断某段循环代码是否地道,并不仅仅是以知道或不知道某个内置方法作为标准。我们可以从上面的例子挖掘出更深层的东西。

    如你所见,Python 的 for 循环只有 for <item> in <iterable> 这一种结构,而结构里的前半部分 - 赋值给 item - 没有太多花样可玩。所以后半部分的 可迭代对象 是我们唯一能够大做文章的东西。而以 enumerate() 函数为代表的*“修饰函数”*,刚好提供了一种思路:通过修饰可迭代对象来优化循环本身。

    这就引出了我的第一个建议。

    建议 1:使用函数修饰被迭代对象来优化循环

    使用修饰函数处理可迭代对象,可以在各种方面影响循环代码。而要找到合适的例子来演示这个方法,并不用去太远,内置模块 itertools 就是一个绝佳的例子。

    简单来说,itertools 是一个包含很多面向可迭代对象的工具函数集。我在之前的系列文章《容器的门道》里提到过它。

    如果要学习 itertools,那么 Python 官方文档 是你的首选,里面有非常详细的模块相关资料。但在这篇文章里,侧重点将和官方文档稍有不同。我会通过一些常见的代码场景,来详细解释它是如何改善循环代码的。

    1. 使用 product 扁平化多层嵌套循环

    虽然我们都知道*“扁平的代码比嵌套的好”*。但有时针对某类需求,似乎一定得写多层嵌套循环才行。比如下面这段:

    def find_twelve(num_list1, num_list2, num_list3): """从 3 个数字列表中,寻找是否存在和为 12 的 3 个数 """ for num1 in num_list1: for num2 in num_list2: for num3 in num_list3: if num1 + num2 + num3 == 12: return num1, num2, num3 

    对于这种需要嵌套遍历多个对象的多层循环代码,我们可以使用 product() 函数来优化它。product() 可以接收多个可迭代对象,然后根据它们的笛卡尔积不断生成结果。

    from itertools import product def find_twelve_v2(num_lis1, num_list2, num_list3): for num1, num2, num3 in product(num_list1, num_list2, num_list3): if num1 + num2 + num3 == 12: return num1, num2, num3 

    相比之前的代码,使用 product() 的函数只用了一层 for 循环就完成了任务,代码变得更精炼了。

    2. 使用 islice 实现循环内隔行处理

    有一份包含 Reddit 帖子标题的外部数据文件,里面的内容格式是这样的:

    python-guide: Python best practices guidebook, written for humans. --- Python 2 Death Clock --- Run any Python Script with an Alexa Voice Command --- <... ...> 

    可能是为了美观,在这份文件里的每两个标题之间,都有一个 "---" 分隔符。现在,我们需要获取文件里所有的标题列表,所以在遍历文件内容的过程中,必须跳过这些无意义的分隔符。

    参考之前对 enumerate() 函数的了解,我们可以通过在循环内加一段基于当前循环序号的 if 判断来做到这一点:

    def parse_titles(filename): """从隔行数据文件中读取 reddit 主题名称 """ with open(filename, 'r') as fp: for i, line in enumerate(fp): # 跳过无意义的 '---' 分隔符 if i % 2 == 0: yield line.strip() 

    但对于这类在循环内进行隔行处理的需求来说,如果使用 itertools 里的 islice() 函数修饰被循环对象,可以让循环体代码变得更简单直接。

    islice(seq, start, end, step) 函数和数组切片操作*( list[start:stop:step] )有着几乎一模一样的参数。如果需要在循环内部进行隔行处理的话,只要设置第三个递进步长参数 step 值为 2 即可(默认为 1 )*。

    from itertools import islice def parse_titles_v2(filename): with open(filename, 'r') as fp: # 设置 step=2,跳过无意义的 '---' 分隔符 for line in islice(fp, 0, None, 2): yield line.strip() 

    3. 使用 takewhile 替代 break 语句

    有时,我们需要在每次循环开始时,判断循环是否需要提前结束。比如下面这样:

    for user in users: # 当第一个不合格的用户出现后,不再进行后面的处理 if not is_qualified(user): break # 进行处理 ... ... 

    对于这类需要提前中断的循环,我们可以使用 takewhile() 函数来简化它。takewhile(predicate, iterable) 会在迭代 iterable 的过程中不断使用当前对象作为参数调用 predicate 函数并测试返回结果,如果函数返回值为真,则生成当前对象,循环继续。否则立即中断当前循环。

    使用 takewhile 的代码样例:

    from itertools import takewhile for user in takewhile(is_qualified, users): # 进行处理 ... ... 

    itertools 里面还有一些其他有意思的工具函数,他们都可以用来和循环搭配使用,比如使用 chain 函数扁平化双层嵌套循环、使用 zip_longest 函数一次同时循环多个对象等等。

    篇幅有限,我在这里不再一一介绍。如果有兴趣,可以自行去官方文档详细了解。

    4. 使用生成器编写自己的修饰函数

    除了 itertools 提供的那些函数外,我们还可以非常方便的使用生成器来定义自己的循环修饰函数。

    让我们拿一个简单的函数举例:

    def sum_even_only(numbers): """对 numbers 里面所有的偶数求和""" result = 0 for num in numbers: if num % 2 == 0: result += num return result 

    在上面的函数里,循环体内为了过滤掉所有奇数,引入了一条额外的 if 判断语句。如果要简化循环体内容,我们可以定义一个生成器函数来专门进行偶数过滤:

    def even_only(numbers): for num in numbers: if num % 2 == 0: yield num def sum_even_only_v2(numbers): """对 numbers 里面所有的偶数求和""" result = 0 for num in even_only(numbers): result += num return result 

    numbers 变量使用 even_only 函数装饰后,sum_even_only_v2 函数内部便不用继续关注“偶数过滤”逻辑了,只需要简单完成求和即可。

    Hint:当然,上面的这个函数其实并不实用。在现实世界里,这种简单需求最适合直接用生成器 /列表表达式搞定:sum(num for num in numbers if num % 2 == 0)

    建议 2:按职责拆解循环体内复杂代码块

    我一直觉得循环是一个比较神奇的东西,每当你写下一个新的循环代码块,就好像开辟了一片黑魔法阵,阵内的所有内容都会开始无休止的重复执行。

    但我同时发现,这片黑魔法阵除了能带来好处,它还会引诱你不断往阵内塞入越来越多的代码,包括过滤掉无效元素、预处理数据、打印日志等等。甚至一些原本不属于同一抽象的内容,也会被塞入到同一片黑魔法阵内。

    你可能会觉得这一切理所当然,我们就是迫切需要阵内的魔法效果。如果不把这一大堆逻辑塞满到循环体内,还能把它们放哪去呢?

    让我们来看看下面这个业务场景。在网站中,有一个每 30 天执行一次的周期脚本,它的任务是是查询过去 30 天内,在每周末特定时间段登录过的用户,然后为其发送奖励积分。

    代码如下:

    import time import datetime def award_active_users_in_last_30days(): """获取所有在过去 30 天周末晚上 8 点到 10 点登录过的用户,为其发送奖励积分 """ days = 30 for days_delta in range(days): dt = datetime.date.today() - datetime.timedelta(days=days_delta) # 5: Saturday, 6: Sunday if dt.weekday() not in (5, 6): continue time_start = datetime.datetime(dt.year, dt.month, dt.day, 20, 0) time_end = datetime.datetime(dt.year, dt.month, dt.day, 23, 0) # 转换为 unix 时间戳,之后的 ORM 查询需要 ts_start = time.mktime(time_start.timetuple()) ts_end = time.mktime(time_end.timetuple()) # 查询用户并挨个发送 1000 奖励积分 for record in LoginRecord.filter_by_range(ts_start, ts_end): # 这里可以添加复杂逻辑 send_awarding_points(record.user_id, 1000) 

    上面这个函数主要由两层循环构成。外层循环的职责,主要是获取过去 30 天内符合要求的时间,并将其转换为 UNIX 时间戳。之后由内层循环使用这两个时间戳进行积分发送。

    如之前所说,外层循环所开辟的黑魔法阵内被塞的满满当当。但通过观察后,我们可以发现 整个循环体其实是由两个完全无关的任务构成的:“挑选日期与准备时间戳” 以及 “发送奖励积分”

    复杂循环体如何应对新需求

    这样的代码有什么坏处呢?让我来告诉你。

    某日,产品找过来说,有一些用户周末半夜不睡觉,还在刷我们的网站,我们得给他们发通知让他们以后早点睡觉。于是新需求出现了:“给过去 30 天内在周末凌晨 3 点到 5 点登录过的用户发送一条通知”

    新问题也随之而来。敏锐如你,肯定一眼可以发现,这个新需求在用户筛选部分的要求,和之前的需求非常非常相似。但是,如果你再打开之前那团循环体看看,你会发现代码根本没法复用,因为在循环内部,不同的逻辑完全被 耦合 在一起了。

    在计算机的世界里,我们经常用**“耦合”**这个词来表示事物之间的关联关系。上面的例子中,*“挑选时间”“发送积分”*这两件事情身处同一个循环体内,建立了非常强的耦合关系。

    为了更好的进行代码复用,我们需要把函数里的*“挑选时间”*部分从循环体中解耦出来。而我们的老朋友,**“生成器函数”**是进行这项工作的不二之选。

    使用生成器函数解耦循环体

    要把 “挑选时间” 部分从循环内解耦出来,我们需要定义新的生成器函数 gen_weekend_ts_ranges(),专门用来生成需要的 UNIX 时间戳:

    def gen_weekend_ts_ranges(days_ago, hour_start, hour_end): """生成过去一段时间内周六日特定时间段范围,并以 UNIX 时间戳返回 """ for days_delta in range(days_ago): dt = datetime.date.today() - datetime.timedelta(days=days_delta) # 5: Saturday, 6: Sunday if dt.weekday() not in (5, 6): continue time_start = datetime.datetime(dt.year, dt.month, dt.day, hour_start, 0) time_end = datetime.datetime(dt.year, dt.month, dt.day, hour_end, 0) # 转换为 unix 时间戳,之后的 ORM 查询需要 ts_start = time.mktime(time_start.timetuple()) ts_end = time.mktime(time_end.timetuple()) yield ts_start, ts_end 

    有了这个生成器函数后,旧需求“发送奖励积分”和新需求“发送通知”,就都可以在循环体内复用它来完成任务了:

    def award_active_users_in_last_30days_v2(): """发送奖励积分""" for ts_start, ts_end in gen_weekend_ts_ranges(30, hour_start=20, hour_end=23): for record in LoginRecord.filter_by_range(ts_start, ts_end): send_awarding_points(record.user_id, 1000) def notify_nonsleep_users_in_last_30days(): """发送通知""" for ts_start, ts_end in gen_weekend_ts_range(30, hour_start=3, hour_end=6): for record in LoginRecord.filter_by_range(ts_start, ts_end): notify_user(record.user_id, 'You should sleep more') 

    总结

    在这篇文章里,我们首先简单解释了“地道”循环代码的定义。然后提出了第一个建议:使用修饰函数来改善循环。之后我虚拟了一个业务场景,描述了按职责拆解循环内代码的重要性。

    一些要点总结:

    • 使用函数修饰被循环对象本身,可以改善循环体内的代码
    • itertools 里面有很多工具函数都可以用来改善循环
    • 使用生成器函数可以轻松定义自己的修饰函数
    • 循环内部,是一个极易发生“代码膨胀”的场地
    • 请使用生成器函数将循环内不同职责的代码块解耦出来,获得更好的灵活性

    看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧。

    附录

    29 条回复    2019-04-30 16:10:35 +08:00
    ksedz
        1
    ksedz  
       2019-04-27 20:15:43 +08:00
    支持一下
    yushi17
        2
    yushi17  
       2019-04-27 20:45:16 +08:00 via Android
    一直想找一个类似 product 的东西 终于看到了!!
    whoisghost
        3
    whoisghost  
       2019-04-27 20:47:55 +08:00   1
    以后谁再说 Python 代码清晰易读好学,我会一巴掌抽过去,全都是黑魔法。
    Vegetable
        4
    Vegetable  
       2019-04-27 20:54:27 +08:00
    资瓷
    congeec
        5
    congeec  
       2019-04-27 21:17:39 +08:00 via iPhone
    我又要来吹一波 Haskell 了。python itertools 模块里的东西不少是从 Haskell 抄的。入门 Haskell 后再用 python itertools 爽的一笔。

    不过话说回来 python 的 generator comprehension 抄的相当有水平
    Hopetree
        6
    Hopetree  
       2019-04-27 21:35:15 +08:00   1
    我知道 itertools 很强,有很多函数可以减少日常的一些操作,特别是各种迭代,但是我总是记不住这些函数,真的烦,看的时候总是觉得好有用,但是实际上自己总是忘记用了,所以久而久之,就忘记怎么用了。。。。。。。。。。。
    Northxw
        7
    Northxw  
       2019-04-27 21:46:07 +08:00
    马克
    piglei
        8
    piglei  
    OP
       2019-04-27 22:00:14 +08:00
    @whoisghost 淡定,都没出现什么双下划线方法和描述符之类的,还远远算不上什么黑魔法。

    @congeec 是的,itertools 官方文档第一句话就是:

    > This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python.

    函数式编程在一些特定场景下用起来确实爽,一点都不拖泥带水。

    @Hopetree 建议你详细的理解每一个函数的名字,itertools 下的每个函数名字含义非常清楚的,比如 dropwhile、takewhile。这样可以加强记忆。
    chinesehuazhou
        9
    chinesehuazhou  
       2019-04-27 22:01:47 +08:00 via Android
    前排撒花
    WhoMercy
        10
    WhoMercy  
       2019-04-27 22:09:43 +08:00 via Android
    很赞,py 新手要向编写出更加 pythonic 的代码方向努力
    piglei
        11
    piglei  
    OP
       2019-04-27 22:17:42 +08:00
    @chinesehuazhou 感谢

    @WhoMercy 谢谢支持,加油。
    GlobalNPC
        12
    GlobalNPC  
       2019-04-27 22:23:04 +08:00 via iPhone
    @Hopetree 我也是,还以为是因为我笨才这样
    onlyice
        13
    onlyice  
       2019-04-27 22:57:08 +08:00 via Android
    支持,讲得好懂,例子也很赞
    chengxiao
        14
    chengxiao  
       2019-04-28 03:23:50 +08:00 via iPhone
    干货支持
    hp66722667
        15
    hp66722667  
       2019-04-28 09:03:52 +08:00   1
    python 看似简单,其实反倒比静态语言要更复杂,自己写着 high 了,其他人看起来可就头疼了,如果单纯把语言作为一种生产工具,功能越单一才更适合当今的发展。换句话说,中国这么大各地都有地方口音,如果没有普通话真心不敢想象
    Eds1995
        16
    Eds1995  
       2019-04-28 09:16:43 +08:00
    @congeec 不怕被人砍?推荐人学 Haskell
    vipppppp
        17
    vipppppp  
       2019-04-28 09:20:42 +08:00
    作为一个 pythoner,真的想说,python 很多库的源码真的很难阅读,因为黑魔法真的太多。。。
    zeromake
        18
    zeromake  
       2019-04-28 09:24:30 +08:00
    go 才是真的什么魔法都木有
    claymore94
        19
    claymore94  
       2019-04-28 09:56:36 +08:00
    get ,谢谢分享
    piglei
        20
    piglei  
    OP
       2019-04-28 10:29:15 +08:00
    @hp66722667 是的,go 比较符合你说的简单哲学。

    但编程语言这东西,究其本质还是体现语言创始人个人经历和语言审美的“作品”。所以它必定是百花齐放、各有千秋的。但如你所说,Python 语言现存的问题之一,确实就是太复杂了。各种可能不该暴露出来的语言内部细节、不断增加的新语法,搞得全世界没有任何一个人可以说自己了解“全部”的 Python。

    但我觉得,如果要用 Python 开发一些比较严肃的项目,一些核心的语言特性和思想还是必须要了解的,比如生成器、描述符等等。不然的话,就像我在文章里举的那个例子一样,程序员只是以套用一种编程语言的经验在写 Python。而非如《代码大全》里所说的真正 **深入一门语言编程** 。

    @vipppppp 虽然不知道你说的是什么库,但是我觉得 Python 里面真正称得上“黑魔法”的特性其实还是偏少的。当然,如果以其他语言,比如 Go 来作为标准的话,那 Python 确实遍地都是黑魔法了。
    JStatham
        21
    JStatham  
       2019-04-28 11:53:41 +08:00
    牛逼, 后悔没有早点看到这么优秀的文章
    nuance2ex
        22
    nuance2ex  
       2019-04-28 17:08:18 +08:00 via iPhone
    精彩,精彩。开阔思路,通俗易懂。
    bwangel
        23
    bwangel  
       2019-04-28 17:31:12 +08:00
    感觉 Python 和 Go 就是两个极端。

    Python 是什么操作都能自定义,运算符重载,__geattr__, __getitem__ property.setter, property.getter, __iter__ 等,更别说还可以用 type 动态创建类,用 Metaclass 修改类的创建过程

    而 Go 是啥语法都不让自定义,连个最基本的运算符重载都没有,Add 方法还要写上好几个 IntAdd, FloatAdd。
    piglei
        24
    piglei  
    OP
       2019-04-28 17:46:05 +08:00
    @bwangel 是的,所以你可以说 Python “优雅”,也可以说 Python “乱七八糟”。同样也可以说 Go “务实稳重”,或者可以说它 “死板不懂得变通”。
    bwangel
        25
    bwangel  
       2019-04-28 17:54:53 +08:00
    @ #22 举一个标准库的例子

    https://golang.org/src/sync/atomic/doc.go

    因为没有函数重载,出现了这样的写法。

    @piglei 现在越来越喜欢 Go 了。

    曾经遇到过一种写法,类的定义是空的,然后 setattr 往里面塞。然后光看代码很难知道一个对象中有哪些属性,必须要跑起来,通过 vars 才能看出到底有哪些属性。

    感觉宁愿多写一些,也不愿意搞这些黑魔法了。
    piglei
        26
    piglei  
    OP
       2019-04-28 18:03:46 +08:00
    @bwangel 你提的这个 setattr 例子可以算是极大的反模式。是的,我的体会是越灵活和复杂的语言,对团队在该语言上的整体经验要求会比其他死板一点的语言更严格,也对团队配套行为,比如 Code Review、知识共享等提出了更高的要求。

    "Write Less, Do More" 不是没有代价的,一切早已在暗中标好了价格。
    xpresslink
        27
    xpresslink  
       2019-04-28 22:48:44 +08:00
    [enumerate() 是 Python 的一个内置函数] 这句话是错误的。在 python 里面千万不能见到用括号调用的东西就认为是函数。还有一种是对象实例化。
    piglei
        28
    piglei  
    OP
       2019-04-29 10:30:07 +08:00
    @xpresslink 特意查了一下 enumerate 的实现,发现简单称之为“函数”确实不严谨,它更像是一个“工厂类”。

    但我觉得这属于语言内部的细节,不应该作为额外阅读成本引入到文章中。所以,我不准备把它作为一个“错误”修复,仍然保持“内置函数”这个比较容易理解的表达。感谢提醒。
    bwangel
        29
    bwangel  
       2019-04-30 16:10:35 +08:00
    啊,刚刚又遇到一个坑,忍不住过来吐槽一下。

    https://gist.github.com/bwangelme/4002260ea023736eb7e51bb0a53a23e0

    上面的代码是大概的伪代码。`update_id_time ` 函数正常是能够成功更新 ID 的 time 属性的,但是如果把 `update_id_time` 变成一个异步任务,程序就失败了。

    因为在执行 `self.props['time'] = datetime.datetime.now().strftime('%Y/%m/%d')` 我们相当于创建了一个临时变量,props,然后对这个临时变量做了一些修改。

    但是在整个代码放到队列中后,队列消费者 unpickle 代码的时候,`self.props`不会使用我们这个临时变量,而是直接从`props_getter`中获取。这样我们的修改就丢失了。

    感觉打造一支高水平的 Python 开发团队难度太大了,比 Java 要难很多。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     967 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 33ms UTC 19:03 PVG 03:03 LAX 11:03 JFK 14:03
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86