2017年鉴

瞄了一眼2016年写的总结,发现定的目标果然没有完成,哈哈,当成2018年的目标好了。

年初的时候买了《最后的守护者:食人的大鹫》,上天人文的作品,传说是游戏里面最智能的一条狗,可惜买来之后发现自己并不是很喜欢。纯粹的解谜游戏,没有什么提示,游戏的过程比较无聊,到现在也没有通关。

后来玩了《使命召唤13》,没啥感觉……大约花了8个小时通关了,去线上老是被虐,就封箱了。不顾最近出了《使命召唤二战》,看着挺想玩的。不是很喜欢未来向的游戏,喜欢基于历史的,枪械也比较老的那种风格。等降价入一下《二战》吧。

有一段时间在公司和同事一起玩《BROFORCE》,4人同屏横版闯关游戏,有点像魂斗罗,非常欢乐。后来打到最后的BOSS,打了几次都过不了,渐渐的就不玩了。然后大家基本上不一块玩游戏了。

中间去尝试了一些PS PLUS赠送的游戏,比如《HUE》《刺客信条 黑旗·自由的呐喊》《合金装备V幻痛》,但是都没有深入玩下去。

后来认真通关了《恶名昭彰2 次子》,这个游戏设计的画面很好,游戏性一般,UI也很一般,界面上没有用的画面占了很多地方(菜单)。趁着打折买了心仪已久的《辐射4》,也不错,只不过游戏系统太复杂了,很多操作需要重复做,比如整理道具之类的,不太友好。世界观设定蛮好的,缺点就是太不人性化了。年底会员免费了《拉结特与克拉克》,太喜欢这个游戏了!画面,游戏性,音乐,剧情都可以给满分了!而且非常低龄化,游戏性非常高,可以玩很久!

工作之后玩游戏的时间少了,不过这并不是抱怨,以前在学校很无聊,也不知道做什么,学东西也不知道用在什么地方,学校又不教啥有用的东西,所以很多时间想打游戏。工作之后比较充实,研究一些技术问题、写代码很多时候都感觉比打游戏有意思!今年也没在游戏上花什么钱,倒是Switch出了很多好游戏(年度游戏《塞尔达·荒野之息》),想买,明年看看吧。

7月份延迟1个月顺利拿到了毕业证,在公司入职,一切都很顺利。工作之后真的爽很多啊,比起在学校,做的东西都更有意义了,有了更多的学习目标,学到了更多有有意义的东西,生活也感觉更幸福了。花了很多时间补充Python的知识,用Linux更熟练了。

10月份搬家了,在市区,新家有冰箱和厨房可以用, 感觉生活质量上升了一个档次。之前租的房子打算凑活一下,没想也整整住了一年。做饭这种事情吧,确实费时间,但这就是生活吧,可以学做不一样的东西。

这一年的生活大多数时候还是挺开心的。明年想多花点时间锻炼身体。

很喜欢现在工作的一点是:工作上的事情大家都直来直去,review代码,讨论意见可以就事论事,没有太多顾虑。新方案的提出所有人都可以参与讨论,决定事情比较快,实施也快。现在我们基本上都迁移到Python3了,用的数据库,Elasticsearch基本也都更新很及时,跟着官方的新版本。每天上班心情都很好。

不足的是,有一些项目因为不断添加新的东西,添加的时候一直在复制粘贴,导致线上API那边几百行的函数是很正常的事情。以及前端的代码,同样的逻辑要写在很多地方很多次。现在添加展示一种新的数据类型,经常要grep一下之前的数据是怎么展示的,然后对照这些文件复制粘贴来添加新的。很不好。明年希望能重构一下。首先得熟练用pytest保证不挂掉东西。

去年定的目标,看完《SICP》和《CSAPP》,到年底了,两本书都没有动。今年系统性的认真看完的书很好,学习的东西比较浅,基本就是感兴趣某个方面或者某个问题,去网上翻翻资料,整理一下。豆瓣上“想读”的书还是那么多,今年看的书好像就《永恒的边缘》。

明年少花点时间逛论坛了,纯粹就是浪费时间。很多东西看了还让自己不爽。多花点时间读代码、文档、书,尝试一下流行的技术。多写点博客。

今年7月20日,林肯公园主唱查斯特·贝宁顿在家中自杀身亡。

最后,汇总一下2018年的目标:

  1. 读完《SICP》和《CSAPP》(这好像连续3年被列为目标了)
  2. 在LeetCode做完500道题目
  3. 学习Rust(能直接编译成程序码很酷,打算实现一个HTTPS over DNS)
  4. 买switch

过去四年:

  1. 2013年
  2. 2014年
  3. 2015年
  4. 2016年
 

闭包初探

看过了很多有关闭包的文章,也看了很多书,还是对闭包一知半解。听说闭包是面向对象系统实现的基础,但是一直不明白这其中有什么关系。今天看《Fluent Python》终于有点理解了!

我更新了一下之前的《理解Python的UnboundLocalError》这篇博文,这篇讨论了Python的作用域以及原因。建议读本文之前先阅读一下这篇。

维基百科是这样解释的:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。

这里的“自由变量”是一个技术术语,指的是没有在本地作用域绑定的变量。用通俗的话说,就是一个函数内使用的变量并没有在函数内定义,而是在函数外定义的。这个函数和它之外的这个变量就构成了闭包。“闭包”的概念很难理解,我觉得是因为你乍一听上面这段话,就算明白了什么意思,也想不出有什么用。请看下面这个例子(来自《Flunt Python》):

有一个商品,它的价格每天都会变动。我们想要求出它的平均价格(其实这个例子用Python的生成器更加方便)。

averager函数和series构成了闭包。虽然make_averager函数执行一次之后退出了,但是函数内部的变量series并没有被销毁,多次调用avg发现series确实是之前的状态。

假如说不用闭包这个特性,使用面向对象来实现,代码要写成这样:

从中可以体会到,其实面向对象的中对象的概念,包含函数和属性。而函数里面使用的对象的属性,而不是函数内的局部变量,这就是有闭包的支持。属性和函数形成了闭包,属性就不会被销毁。闭包可以引用函数外的变量,但是这个变量又可以不是全局变量,从而对外隐藏了内部的一个状态,从而即达到了维持一个状态的目的,又做到了封装。我想,这就是为什么“闭包”是面向对象的基础吧。

2018年1月3日更新:我又有了一个想法,上面这个例子是“不用闭包这个特性”,为了体会一下闭包的“封装”功能,假设我们“没有闭包”这个特性,那么第一个例子的代码中,内部函数将不能访问外部函数的局部变量,因为没有了“闭包”这个通道。按照Python寻找变量的规则(大多数编程语言的规则)会到上一层函数->全局变量这个顺序查找。即,我们的代码将写成这样:

因为averager函数内用到了一个内部的状态变量,这个变量要独立于函数调用的生命周期,所以如果没有闭包,我们只要在每次使用averager函数之前创建一个变量series供它使用。这样即破坏了函数封装的作用,又失去了函数黑箱调用的方便性。

有关Python中闭包的实现,因为Python有“一等函数”的特性(函数是对象),“自由变量”保存在函数的__code__.co_freevars中,然后在avg.__closure__中保存了cell对象,对应每一个co_freevarscell对象中的cell_content中保存了真实的值。

Python的这种实现有一个需要特别注意的陷阱。这种绑定闭包的方式是“迟绑定的”,这意味着,闭包绑定的值只有在函数真正运行的时候才去查询。在讨论for循环作用域的时候,Python邮件列表的邮件提到这段代码:

这段代码的执行结果是10个9,而不是0-9.因为只有lambda函数执行的时候才去查询i的值,这个时候i已经变成了9.一个“不太优美”的解决方法是使用默认参数立即绑定 lst.append(lambda i=i: i)。提醒一下,造成这个陷阱的原因不是Python的Lambda函数,lambda并没有什么特殊的,真正的原因是Python闭包的迟绑定实现方式。

很多博客将“闭包”和“匿名函数”混为一谈,通过这篇文章,显然这不是等价的。只不过只有是嵌套在其他函数中的函数才可能需要处理不是全局作用域的外部变量,而这种函数通常是匿名函数罢了。

 

象棋与围棋

《Fluent Python》这本书非常值得读,此书不仅讨论Python,经常结合其他编程语言讨论一些语言设计上的问题。读此书就和一位经验老道的开发者聊天一样,非常尽兴。

今天在书上(16章协程)看到这样一段话,很震撼:

对编程语言来说,关键字的作用是建立控制流程和表达式计算的基本规则。

语言的关键字像是棋盘游戏中的棋子。对国际象棋来说,关键字是王、后、车、马、象兵;对围棋来说,关键字是●。

国际象棋的棋手实现计划时,有六种类型的棋子可用;而围棋的棋手看起来只有一种类型的棋子可用。可是,在围棋的玩法中,相邻的棋子能构成更大更稳定的棋子,形状各异,不受束缚。围棋棋子的某些排列是不可摧毁的。围棋的表现力比国际象棋强。围棋的开局走法有 361 种,大约有 1e+170 个合规的位置;而国际象棋的开局走法有 20 种,有 1e+50 个位置。

如果为国际象棋添加一个新棋子,将带来颠覆性的改变;为编程语言添加一个新的关键字也是如此。因此,语言的设计者谨慎考虑引入新关键字是合理的。

Scheme 继承了 Lisp 的宏,允许任何人创建特殊的形式,为语言添加新的控制结构和计算规则。用户定义的这种标识符叫作“句法关键字”。Scheme R5RS 标准声称,“这门语言没有保留的标识符”(标准的第 45页),但是MIT/GNUScheme这种特殊的实现预定义了 34 个句法关键字,例如 if、lambda 和 definesyntax(用于创建新关键字的关键字)。

Python 像国际象棋,而 Scheme 像围棋。

现在,回到 Python 句法。我觉得 Guido 对关键字的态度过于保守了。关键字的数量应该少,添加新关键字可能会破坏大量代码,但是在循环中使用 else 揭示了一个递归问题:在更适合使用新关键字的地方重用现有的关键字。在 for、while 和 try 的上下文中,应该使用 then 关键字,而不该妄用 else。

在这个问题上,最严重的一点是重用 def。现在,这个关键字用于定义函数、生成器和协程,而这些对象之间的差异很大,不应该使用相同的句法声明。

引入 yield from 句法尤其让人失望。再次声明,我觉得真的应该为 Python 使用者提供新的关键字。更糟的是,这开启了新的趋势:把现有的关键字串起来,创建新的句法,而不添加描述性的合理关键字。恐怕有一天我们要苦苦思索 raise from lambda 是什么意思。

这段文字很有意思,作者抱怨的是,该引入新的关键字的时候却不引入,过于保守。

比如 for-else 语句就很让人困惑—— “for循环跑完了,这段else会不会执行呢?” 我每次看 for-else 代码,都得在心里来回算几遍。因为 else 表示转折,但是这里并没有转折的意思——“如果循环正常结束,就直接执行这段代码。” 我的方法是将这个 else 当成是 “then” 来理解,显然如果改成 “then”,就一点歧义也没有了。“循环结束,then 执行这个”。对于 “try…expect…else”来说,换成 then 更是好理解的多,“try…except…then”。

将人们已经熟悉的内容重复赋予新的意义,甚至组合一些关键字来使用,会更加让人困惑。

不过好在现在Python已经引入了async/await来定义协程(PEP 492),好的多了。

贴一下书中推荐的两个链接,很有意思:

  1. The Value Of Syntax? 这个讨论组叫做lambda-the-ultimate,是编程语言极客的聚集地
  2. What color is your function?
 

上海越来越冷了……

最近有很多想写的东西,这篇博客是日记,非技术。

1. 写博客

我觉得写博客和逛博客是不分家的,如果自己不写基本上没什么动力去看别人写的东西,自己写博客也会经常去看看别人的博客。关于能获得什么……现在想想看,其实并没有多少收获。不写又不会死。不玩游戏也不会死,不编程也不会死。不去做什么事情都不会死。就是一种爱好吧,想说的话能说出来,有一个属于自己的地方,不受审查和限制的地方。wordpress的后台编辑器越来越好了,打开这里觉得很幸福。

有时候在网上翻翻别人的博客,仿佛看到一个人一天一天的生活,自己也不会觉得无聊。从一个人的博客可以发现很多好玩的链接,以及友情链接,又能发现好玩的东西——这就是上网冲浪吧!

今天尝试申请Google的Adsense,一个广告服务。这是我第四次申请了,结果和前三次一样,被拒:

网站不符合Google合作规范:您的网站没有遵守Google AdSense合作规范或网站站长质量指南,因此我们目前无法批准您加入AdSense的申请。我们的目标是向广告客户提供具有以下几个特点的网站:可提供丰富、实用的内容,能够获得自然流量并且允许我们向用户投放具有理想针对性的广告。我们认为您的网站目前没有达到这一标准。

很不理解,写四年了,转载的内容不超过3篇。不过算了,不过就不过吧,一来也挣不到多少钱,二来有这东西在,写博客的时候心里总会想着写点SEO友好的、能吸引流量的东西。还是自己想写什么就写什么。

2. 厨艺

沉迷下厨无法自拔,吃自己做的意大利面就像回到了德国一样。好久没做红烧肉了,最近打算试一下,可能会尝到爱情的味道,嘻嘻。

3. 容器

最近尝试了很多虚拟化技术,非常着迷。我觉得有系统洁癖的人都会喜欢这玩意儿的。想想看,即使是在个人电脑上,所有的东西都是一个个你命名的名字有条不紊的运行着,看着多舒服。虽然我觉得个人电脑尽管折腾就行,保持服务器干净就行了……哦对了,主要容器技术最好的地方就是所有的东西都是可重置,要么有Vgrantfile,要么有Dockerfile,这些file将操作的过程全部透明化了。不用每次配置个wordpress都要在网上找教程看半天了!

Vagrant和Docker,不一样的东西。我觉得都挺好的,两手都要抓,两手都要硬。比如学习docker的swarm的时候,搞个两台机器建集群,Vagrant就再合适不过啦(虽然最后自己没搞成,偷懒用了官方的docker-machine)。

还有一个好处,有了这种东西,以后看到什么稀奇玩意儿都可以装一装尝试一下。

4. 水果

圣诞节快到了!祝读者圣诞快乐!

公司一人发了一箱苹果。乖乖,我现在有20个苹果(超级大个),9个橙子,1个牛油果,一盒红毛丹(味道和荔枝一样),可能是我这辈子拥有水果最多的时候。

5. 游戏

趁打折买了《辐射4》,沉迷捡垃圾不能自拔。就是感觉这游戏引导比较差,什么武器好使,什么建筑要先造,都要凭着之前游戏的经验。很容易迷失自我。

12月PlayStation Plus免费了《瑞奇与叮当》,下载下来一看,我去,那叫一个好玩!1080p的画质,创意丰富的游戏道具,游戏性非常好。缺点就是故事剧情太俗套,基本上没剧情。总体来讲是一个非常难得的游戏了。最近《恶灵附身》非常火,但这是个恐怖游戏,我暂时应该不会接触了…… 目前最渴望玩《使命召唤·二战》以及明年的《战神4》《最后生还者2》《蜘蛛侠》《超越善恶》,非常期待!

最近经常看“星际老男孩”的录像,发现星际2我竟然还能看得懂,哪些快捷键也都还记得住。好想再打一盘啊。但是我的键盘是60%,鼠标是人体工学的,电脑也不是windows。哎,怎么感觉跟老人一样。

6. 漫画

最近在看的书是《我们》,一个俄罗斯人写的,对乌托邦社会有理解,这位作者在俄罗斯比较出名。虽然现在的俄罗斯已经不是六七十年代恐怖的俄罗斯了。

还看了一些漫画作品,非常不错:《和女儿的日常1,2》萌萌的很可爱。《蓝色小药丸》讲了艾滋病人的故事,其实艾滋病已经没有现在的人们想象的那样可怕,艾滋病人有权力像正常人一样生活。《利维坦之书》,这个我还没看完,和《塔希里亚故事集》有点像。

7. 开源

挖了太多的坑了,每天回到家吃饱了就想当咸鱼……

希望年前(元旦是没指望了,就大年吧)把《Python并行编程》这本书翻译完(flagged)。

 

Hash碰撞和解决策略

很多语言都有Hash Table这种数据结构,有的叫哈希表,有的叫散列表,有的叫字典,有的叫map。大体上讲,这就是一种存储键值对的数据结构,通过hash算法,将给定的key算出一个hash值,将value存到hash值对应的地方。下次查找key的时候,以同样的hash算法算出key的hash值,到相应的地方找到该value。从原理可以看出,这种数据结构的好处是,无论多大的一个数据量,算法复杂度总是Ο(1)的。所以大多数的编程语言都实现了这种数据结构。

1.什么是hash碰撞?

那么从上面介绍的原理中可以看出,这种数据结构的关键就是hash算法。hash算法决定了你的key将在什么地方保存value,如果你要让各种对象都可以作为key的话,你就要为它们实现hash算法。hash算法又是一个很神奇的算法。

一个设计良好的 hash 算法,具有以下特性:

  1. 压缩性:任意长度的数据都可以通过hash来压缩(或扩展)到相同长度
  2. 抗计算原性:给定一个hash结果h,寻找原来的M来满足H(M)=h是计算困难的。(hash是一种单向的函数,这个功能很强大。例如,我们在数据库保存用户的密码并不是保存密码明文,而是保存密码明文的hash值,验证密码是否正确的时候将明文hash之后与数据库保存的hash值对比即可。这样即使数据库泄露,也不会泄露用户的密码)
  3. 抗碰撞性。找到两个明文M,M’ 使hash值 H(M)=H(M’)是计算困难的

正因为有第三点,所以我们的hash表才能正常工作。但实际上,由于hash将任意长度生成固定长度的值,所以必然可能存在两个不同的值M和M’,它们的hash值相等,数据结构必须能正确处理这种情况。本文就来讨论这种情况的一些解决方案——如何处理hash碰撞(Hash Collision)。在开始讨论解决方法之前,我们先要明确下面几点:

  1. 字典(hash表等称呼在本文就统称为“字典”了)必须能够处理无限大的数据量
  2. 字典要尽量保持Ο(1)的查询速度
  3. 字典要能够处理hash一致,但是值不一致的情况,屏蔽底层细节提供保存、查询等功能
  4. hash函数的散列度越高越好(减少hash碰撞的情况)

2.字典的工作原理

在《为什么list不能作为字典的key?》一文中详细介绍了字典的工作原理。

字典其实是用一个array来保存key-value pair的,value保存的位置就是hash key的值。保存的时候计算hash(key)拿到下标,将value保存到该位置,查找的时候计算hash(key)得到下标,到该位置拿到value。这样就可以做到Ο(1)的速度。

需要注意的是,因为“hash碰撞”的存在,hash(key)得到的下标的位置存放的元素并不一样是我们要找的元素。所以一个准确的查询过程应该是:

  1. hash(key)拿到下标
  2. 拿到下标的元素(实际上Array并不只存放的value,而是一个key-value键值对),比较如果该地址的key==我要找的key,那么这个value才是我要找的value

举个例子,将  {89, 18, 49, 58, 9} 存入hash表中。

存放的步骤如下:

  1. 计算89的hash位置(为什么是hash(key, size),因为我们得到的下标值必须是一个array容量内的下标,所以跟size mask一下),得到9,位置9空,放进这个位置
  2. 计算18的位置并存放,同上
  3. 计算49的位置,发现这个位置已经有元素了:
    1. 如果这个元素==89,说明我们想要存放的元素已经放好了,结束
    2. 如果!=89,说明此位置发生hash碰撞,我们将49存入下位置9的下一个位置——位置0
  4. 继续存放下一个元素,如果没发生hash碰撞就执行步骤1,如果发生hash碰撞就执行步骤3

查询的时候,比如要查询49:

  1. 计算hash(49)的值,得到下标9,发现下标9的元素key不等于49
  2. 去位置1,发现key等于49,找到,结束

3.Probing

上面觉得这个例子,其实就是解决hash碰撞的一种方案,叫做“Probing”(其实是最简单的一种Probing,叫做Liner Probing)。如果发生了hash碰撞,就使用一种策略在Array中找到一个空的位置。查找的时候只要使用同样的策略去找,肯定能找回这个元素。Probing有几个很关键的问题需要注意:

3.1惰性删除

回想上面这个例子,现在我们要从字典中删除89这个元素,如果直接将89设置为空的话,就会产生很严重的问题:如果此时存放49,计算hash得9,位置9为空,放入,就会出现两个49在字典中;如果此时查找49,发现位置9为空,得到结果49不存在;等等。

所以要删除元素的时候,我们并不会真的将这个位置的元素删除,位置设置一个特殊的flag,表示这个地方的元素已经被删除了。并不会影响49这个元素的查询(在本例子中)。

这种方案存在的问题是:如果字典要经常删除元素,就会留下大量“尸体”,而存放新的元素需要不断扩大字典。但是如今存储空间非常廉价,所以这个方案依然是比较有效的。

3.2聚集问题

在文章一开始我就提到,字典的散列度越高越好。试想如果我们计算10个元素的hash值都一样,那么每次存放和读取都会发生Probing,读取时间就会变成最高Ο(n)复杂度。

一个简单的策略是,当字典中存放的元素超过70%,就扩大字典体积。下图描述了一个散列度尚可的字典,有一小部分的聚集,当保存的元素超过70%就扩张。

这个问题其实很有意思,有一种web攻击叫做“Hash碰撞攻击”,简单而致命。一般的web服务器都用字典这种简单有效的数据结构来保存请求参数,如果我们发送一个请求,里面所有key都是相同的hash(所以如果想要使用这种攻击方式,必须知道目标服务器/编程语言的hash策略),服务器去保存你的参数时耗费的时间从Ο(1)变成Ο(n).这里有个例子,展示了PHP保存正常的65535个int值只需要0.021s,如果保存一个恶意构造的65535个int,需要42s!

4.其他Probing方式

所以,hash函数必须减少碰撞的机会。除此之外,probing也要减少计算的机会,也就是说,probing的策略也要尽量提高散列度。

4.1 Quadratic Probing

顾名思义,就是通过一个二次方程计算下一个位置。举个例子,上面的字典我们使用如下策略,如果当前位置冲突,就使用当前位置 + 2^n来保存,即K+1,K+4,K+9,结果如下。

明显比Liner Probing的散列度高了很多。

Python使用的方程是:

如果j=3,size=32的时候,序列如下:

3 -> 11 -> 19 -> 29 -> 5 -> 6 -> 16 -> 31 -> 28 -> 13 -> 2…

这里加了个perturb,很有意思,通过这里的伪代码我发现这个值也是通过hash计算出来的。这就意味着,对Python的字典进行hash碰撞攻击是非常困难的。

4.2 Double Hashing

发生冲突的时候,使用另一个hash函数计算probing的位置。

4.3 总结

这里有一个权衡,就是存取效率(每次计算耗费的时间)和散列度的权衡(计算的次数)。

如果存取效率高了,也就是计算probing的速度快了,那么散列度就会下降。Liner Probing是一个极端,存取效率很高,只要+1就可以,但是probing的次数很多。

散列度如果高了,比如Double Hashing,使用hash函数来probing肯定散列度很高,但是每次计算都要使用hash函数,计算效率就下降。

二次方程probing是一个权衡的策略,用的比较多(Python)。

5. Closed Hashing

上面提到probing的这种方法,本质上都是“当位置冲突了想办法找到另一个位置”,存放的结构是一维数组。都叫做“Open Adressing”。

还有一种“Open hashing”,也叫做“Separate Chaining”。将相同hash值的元素用Linked List链接在一起,如果Hash碰撞,在链表中找key相等的那个元素。这种形式省去了计算地址的时间,相当于Liner Probing更极端的一种形式,存取非常块,但是聚合问题最大。实现也更加复杂,需要编程语言动态分配内存。

参考资料:

  1. How can Python dict have multiple keys with same hash? 这里有些回答非常精彩,基本解释了散列值工作的详细过程
  2. Hash碰撞与拒绝服务攻击  这篇文章讲了hash算法以及碰撞攻击
  3. Python dictionary implementation Laurent写了很多Python实现讲解,我从他博客学到了很多东西
  4. Collision Resolution CMU讲义,大多数讨论hash碰撞的地方都引用了此文,非常易读
  5. wiki open addressing
  6. hash碰撞攻击
  7. Meaning of Open hashing and Closed hashing