[译]15条变量&方法命名的最佳实践

原文地址:15 Best Practices of Variable & Method Naming

  1. 不同的代码段采用不同的命名长度。通常来说,循环计数器(loop counters)采用1位的单字符来命名,循环判断变量(condition/loop variables)采用1个单词来命名,方法采用1-2个单词命名,类采用2-3个单词命名,全局变量采用3-4个单词命名。
  2. 对变量采用具体的命名(specific names)方式,”value”, “equals”, “data”在任何情况下都不是一种有效的命名方式。
  3. 采用有意义的命名(meaningful names)。变量的名字必须准确反映它的含义和内容。
  4. 不要用 o_, obj_, m_ 等前缀命名。变量不需要前缀标签来表示自己是一个变量。
  5. 遵循公司的变量命名规则,在项目中坚持使用同一种变量命名方式。例如txtUserName, lblUserName, cmbSchoolType等,否则会对可读性造成影响,而且会令查找/替换工具(find/replace tools)不可用。
  6. 遵循当前语言的变量命名规则,不要不统一(inconsistently)地使用大/小写字母。例如:userName, UserName, USER_NAME, m_userName, username, …。
    以Java为例:
    * 类名使用驼峰命名法(Camel Case):VelocityResponseWriter
    * 包名使用小写:com.company.project.ui
    * 变量使用首字母小写的驼峰命名法(Mixed Case):studentName
    * 常量使用大写:MAX_PARAMETER_COUNT = 100
    * 枚举类(enum class)采用驼峰命名法,枚举值(enum values)采用大写。
    * 除了常量和枚举值以外,不要使用下划线’_’
  7. 在同一个类不同的场景(contexts)中不要复用变量名。例如在方法、初始化方法和类中。这样做可以提高可读性和可维护性。
  8. 不要对不同使用目的的变量使用同一个变量名,而是赋予它们不同的名字。这同样对保持可读性和可维护性很重要。
  9. 变量名不要使用非ASCII字符(non-ASCII chars)。这样做可能会在跨平台使用时产生问题。
  10. 不要使用过长的变量名(例如50个字符)。过长的变量名会导致代码丑陋(ugly)和难以阅读(hard-to-read),还可能因为字符限制在某些编译器上存在兼容性问题。
  11. 仅使用一种自然语言(natural language)来命名变量。例如,同时使用德语和英语来命名变量会导致(理解)不一致和降低可读性。
  12. 使用有意义的方法名。方法名必须准确表达该方法的行为,在多数情况下以动词(verb)开头。(例如:createPasswordHash)
  13. 遵循公司的方法命名规则,在项目中坚持使用同一种方法命名方式。例如 getTxtUserName(), getLblUserName(), isStudentApproved(),否则会对可读性造成影响,而且会令查找/替换工具不可用。
  14. 遵循当前语言的变量命名规则,不要不统一地使用大/小写字母。例如:getUserName, GetUserName, getusername, …。
    以Java为例:
    * 方法使用首字母小写的驼峰命名法:getStudentSchoolType
    * 方法参数使用首字母小写的驼峰命名法:setSchoolName(String schoolName)
  15. 使用有意义的方法参数命名,这样做可以在没有文档的情况下尽量做到“自解释(documentate itself)”

如何选择日志级别

一般来说,日志级别有以下几个:

  • FATAL(CRITICAL)
  • ERROR
  • WARN
  • INFO
  • DEBUG

它们的权重从大到小。当我们设置好log级别后,比它权重低的其他log都会被忽略。

不同的语言,不同的库有不同的log实现,使用方法也比较简单。但运用好log的关键不在库本身,而是在恰当的地方使用合适的log级别。

在不同的场景下,应该选择相应的log级别。google了一番,总结如下所示:

FATAL(CRITICAL)

代表发生了最严重的错误,会导致整个服务停止(或者需要整个服务停止)。简单地说就是服务死掉了。

ERROR

代表发生了必须马上处理的错误。此类错误出现以后可以允许程序继续运行,但必须马上修正。

WARN

代表存在潜在的错误,或者触发了容易引起错误的操作。程序可以继续运行,但必须多加注意。

INFO

此输出级别常用语业务事件信息。例如某项业务处理完毕,或者业务处理过程中的一些信息。

此输出级别也常用于输出一些对系统有比较大的影响的需要被看到的message,例如数据库更新,系统发送了额外的请求等。

DEBUG

此输出级别用于开发阶段的调试,可以是某几个逻辑关键点的变量值的输出,或者是函数返回值的验证等等。


从今天开始用log代替print吧!

用python编写1024游戏(4)

上一篇blog,我们来用python实现ai版1024游戏。


如前文所述,关键在于 获得最优滑动方向 这一步。整理一下思路:

此方法会调用一个用递归实现的深度优先搜索:

直接将其翻译成python代码:

其中 evaluation_func 是关键。如何定义这个函数呢?前文提到:

其中 H(x) 是评估函数。我们要思考究竟是什么因素影响了当前的局面,什么才算对自己有利。

这里这里的讨论可以总结出以下两点:

  • 最大值尽量靠在边角
  • 尽可能地沿着某个方向呈等比数列排列

于是我们可以这样设计:

各种因子的实现为:






递归深度为2时,运行一次大概要4-5min。当我设置递归深度为1时,进行了n次实验(n>50),胜率大概在50%左右。跟这里提及的算法效果比起来简直一个天上一个地下。

如何设计更好的评估函数呢?如何制定更完善的搜索策略呢?这有点超出我目前的能力范围了。。。

完整代码我放到Gist上,如下所示:

用python编写1024游戏(3)

这篇blog后,我们来尝试实现ai版的1024游戏。

背景

先来认识两个概念:树状搜索(Tree-Searching)和评价函数(Evaluate Function)

树状搜索

以象棋为例子、下棋的过程可以看作是棋局变化的过程。假设某时刻某个棋局,接下来可以移动棋子的方式有k种,每种移动方式会导致另外一种棋局。故整个下棋的过程可以用一棵n叉树来描述。目前的局面称为“根局面”(Root Position)或“根结点”(Root Node),后续分支称为“后续局面”(Successor Position)或“后续结点”(Successor Nodes)。

理论上可以从最初的局面开始生成n叉树,直至所有叶子节点满足游戏结束的条件为止,但随着生成层次增加,计算的次数迅速上升,往往会超出当前应用场景的接受范围。所以在一些复杂的游戏当中,会对搜索层次进行限制。同时,可以对某些特定算法进行“剪枝”优化,根据某些条件终止当前路径的搜索以减少分支数。

评估函数

假设当前局面对应的n叉树已经生成,如何选择对自己最有利的下一步呢?在此我们引入一个评估函数,该函数输入一个局面,返回对该局面的评估值,表示当前路径下可以获得的优势。我们对下一步所有可能出现的情况进行评估,然后根据评估值最大的局面来走棋。

注意到某局面的评估值依赖于下一步的所有可能出现的局面的评估值(是它们的和),这些可能出现的局面又依赖于在下一步可能出现的局面的评估值。所以我们需要从叶子节点开始计算评估值,然后回溯到顶层的同时更新父节点的评估值。

思路

首先,很明显,玩1024的过程可以用一棵n叉树来描述。游戏状态的变化按照规则:

设棋盘大小为4*4, 每一回合的平均空格数为4*4/2,大部分回合都可以朝4个方向滑动,那么随着搜索层数的增长,节点的个数如下增长:

仅四层节点个数便达到百万级别(我没算错的话),所以我们必须采用启发式搜索。


接下来我们大致写出整个ai游戏过程:

具体其中的 ai下棋 过程:

用循环和判断语句描述一下:


关键在 获得最优滑动方向 这一步。具体化一下:

其中 评估当前局面 思路如下所示:

这是一个深度优先搜索,可以用递归或者栈来实现,后续文章会有实例代码。


注意到 当前有意义的滑动方向 这一步。如何获得这个方向序列呢?我们来思考一下。“局面改变”是指:

于是我们可以获得两个判断“可移动”的条件


接下来观察 计算评估函数值部分。设评估值为H, 影响因子(决定评估值得因素)的值为A, B, C…,它们的权重为a, b, c, …,则有:

可以写成:

其中x代表当前局面。

我们暂时简单地设计一些影响因子函数,例如“当前局面的剩余空格数”,“当前累计的总分”,“形成可组合的对数”等等。然后根据实验来调整权重。


好了,说完思路,接下来将采用python实现。

用python编写1024游戏 (2)

续上篇文章,我们来一步步实现1024游戏。

总体设计

考虑到实现的难度和效率,改进了一下整体思路:

首先直接把它翻译成python代码:

设计数据结构

然后设计一下用到的数据结构:

由于我用的python版本没有enum类型,所以用class代替。

这里的棋盘类我使用一维数组来作为内部存储实现。要注意get和set方法中坐标到下标的换算方式。

其中:

是用来判断棋盘边界的。

实现

接下来是几个关键函数的实现:

随机放置2或4

这个函数会随着空值元素减少效率变低,应该有优化的算法。其中的0.1控制着2和4出现的比例。

检测当前棋盘的状态是否可变

改变棋盘状态

由于滑动的方向有四个,各个方向的处理办法都不一样。留意到上下、左右可以成组,组内操作的方向是反的。但组间的操作却是转置的。

考虑到棋盘本身可以看做一个矩阵,可以用一个线性变换将棋盘统一到某一方向,然后应用同一种操作,然后再变换回来。这样子虽然避免了重复的分支操作,但是程序的复杂度上升,效率也受到影响。

换一个思路,我们尝试提取不同方向下的不同点:定位和遍历。例如向左滑动和向右滑动,它们的定位方法是一样的,但是遍历方向相反。向下和向上滑动的遍历方向也是相反的,定位方法一样。向左滑动和向上滑动的遍历方向一样,但是定位方法确实相反的(一个i行j列,一个j行i列。于是我们对上述两种操作再针对不同滑动方向赋予不同的实现,从而使操作得到统一。

完整代码

代码放在了Github Gist上,如下所示:

用python编写1024游戏 (1)

我们从整体到局部地设计这个游戏。

首先,整个游戏过程可以简化如下:

再想具体一点,可以变成这样:

思考一下 进行 这一步,可以变成:

改写一下,加上判断条件,如下:

具体一下随机出现一个数字这一步,有:

再思考一下滑动这一步,有:

再具体思考一下更改棋盘状态这一步,可得:

其中序列中元素合并步骤具体为:

注意这一步在具体实现时可以同时进行。具体如下:

其中计分的规则是:

回到一开始判断未结束步骤,具体地:

初始化步骤具体为:

综上,可以写出整体的游戏设计思路:

好了,说完思路,接下来将采用python实现。

红眼睛与蓝眼睛

问题引入

知乎上看到这样一个问题

一个岛上有100个人,其中有5个红眼睛,95个蓝眼睛。这个岛有三个奇怪的宗教规则。 1. 他们不能照镜子,不能看自己眼睛的颜色。 2. 他们不能告诉别人对方的眼睛是什么颜色。 3. 一旦有人知道了自己是红眼睛,他就必须在当天夜里自杀。

某天,有个旅行者到了这个岛上。由于不知道这里的规矩,所以他在和全岛人一起狂欢的时候,不留神就说了一句话:【你们这里有红眼睛的人。】

最后的问题是:假设这个岛上的人足够聪明,每个人都可以做出缜密的逻辑推理。请问这个岛上将会发生什么?

我本来是很怕做这种逻辑题的,但是的票最高的回答中有这样的一句话:

「大声说出来」跟「彼此心照不宣」有着决定性的区别。

其中充满着的正能量让我尝试去理解这个题目。于是经过一番思考,总结出了一点自己的见解。

思路

下面设k为红眼睛的人数,Ri(i=1,2,…,k)为红眼睛者,以K=1、k=2、k=3三种情况为例,展开进行阐述:

k = 1

宣告前

第一天,R1看到了99个蓝眼睛的人,但他不知道自己是红眼睛(他不确定这个岛上有红眼睛),也无法知道自己是红眼睛,所以R1不会自杀。 蓝眼睛们各自看到了一个红眼睛和98个蓝眼睛,但是不知道自己是不是红眼睛,由于没有人自愿自杀,所以这些蓝眼睛们都作出了自己不是红眼睛的推断,于是他们会等待一天。如果明天R1自杀了,那么确定自己就是蓝眼睛。如果没有人自杀,由于无法排除是由于R1不知道存在红眼睛造成的,所以蓝眼睛们不知道自己是否是红眼睛,于是他们不会自杀。

第二天,各人重复第一天的推理,所以相安无事,一直没有人自杀。也就是说,“没有人自杀”这个事件不能令他们确定红眼睛的存在,于是系统达到了平衡。

宣告后

第一天,R1看到了99个蓝眼睛的人,知道了自己是红眼睛,于是当天晚上自杀了。 第二天,剩下来的99个蓝眼睛看到有红眼睛自杀了,因此知道这个自杀的红眼睛是因为看到除自己以外的都是蓝眼睛才会自杀,也就是知道自己不是红眼睛,于是他们快乐地生活下去。

k = 2

宣告前

第一天,R1看到了1个红眼睛的人(R2)和98个蓝眼睛的人,但他不知道自己是不是红眼睛,所以R1就想:“如果自己是蓝眼睛,那么就只有一个红眼睛,那么R2看到的都是蓝眼睛。如果他知道至少存在一个红眼睛,那么明天他就会自杀(因为今天他看到的全部是蓝眼睛),那么我就是蓝眼睛。如果他不知道至少存在一个红眼睛,那么他就会作出全部人都是蓝眼睛的推断,那么今天晚上他不会自杀。由于今天不确定我是不是红眼睛,所以今天我不会自杀。”。

同样地,R2也这么想。

其余的蓝眼睛各自看到了两个红眼睛(R1和R2)和97个蓝眼睛,假设其中一个为B1。B1不知道自己是不是红眼睛。假设B1认为自己是蓝眼睛,那么,不失一般性,B1认为其中一个红眼睛R1只看到了另一个红眼睛R2。由于R1不会自愿自杀,所以R1会作自己是蓝眼睛的假设。也就是R1今晚不会自杀(R1会像K=1时的蓝眼睛那样推理)。B1无法根据现有条件判断自己是不是红眼睛,所以B1不会自杀。

第二天,R1发现R2没有自杀,但是他不知道R2知不知道至少存在一个红眼睛,所以R1无法确定R2第二天没有自杀是不是因为这个原因,也就是R1无法确定自己是不是红眼睛,所以第二天R1也不会自杀。同理R2也这样想。

蓝眼睛们无法根据第二天R1、R2都没有死亡推断出“他们没死是因为他们都看到了2个红眼睛,因此我是红眼睛”这个结论,因此不知道自己是不是红眼睛,所以蓝眼睛们没有行动。

于是他们都无法得知自己是不是红眼睛,于是快乐地生活下去。

宣告后

第一天,R1看到了1个红眼睛的人(R2)和98个蓝眼睛的人,但他不知道自己是不是红眼睛,所以R1就想:“如果自己是蓝眼睛,那么就只有一个红眼睛,那么R2看到的都是蓝眼睛,那么明天他就会自杀。也就是明天如果R2自杀的话,我就是蓝眼睛,如果R2没自杀,那么我就是红眼睛”。所以R1这天晚上没有自杀。

同样地,R2也像R1这样想,所以R2这天晚上没有自杀。

其余蓝眼睛各自看到了两个红眼睛(R1和R2)和97个蓝眼睛,假设其中一个为B1。B1不知道自己是不是红眼睛,但他可以这样推理:“假设我是蓝眼睛,那么其中一个红眼睛R1只看到了另一个红眼睛R2。由于R1不会自愿自杀,所以R1会作自己是蓝眼睛的假设。也就是R1今晚不会自杀,他会等到明天看R2会不会自杀。同理当天R2也会这样想,大家都按兵不动,所以第一天晚上没有人自杀。那么如果第二天晚上R1和R2自杀了,那么说明只有两个红眼睛,那么我就是蓝眼睛了,如果没有自杀,那么说明R2和R1也看到了2个红眼睛,R1和R2也在等待事态发展(也像自己那样推理)而没有行动那么我就是红眼睛了”。B1当天没有自杀。

第二天,R1,R2发现对方没有自杀,于是知道自己是红眼睛,于是就在晚上自杀了。蓝眼睛们在等待R1和R2今天会不会自杀。

第三天,其余的98个蓝眼睛知道了两个红眼睛自杀了,所以知道自己是蓝眼睛,于是他们快乐地生活下去。

k = 3

宣告前

红眼睛们各自看到2个红眼睛和97个蓝眼睛,他们像宣告前K = 2时的蓝眼睛那样推理,所以不会自杀。蓝眼睛们各自看到3个红眼睛和96个蓝眼睛,于是他们根据这3个红眼睛的行动来确定自己的状态。但由于无法“知道R1知道R2知道R3知道存在红眼睛”,所以蓝眼睛们一直无法确定“今天三个红眼睛都没自杀是因为各自都看到了三个红眼睛,都在等待,所以我是红眼睛”这个结论,所以不会自杀。

宣告后

第一天,红眼睛们各自看到两个红眼睛和97个蓝眼睛,他们像宣告后K = 2时的蓝眼睛那样推理。直至第三天,红眼睛们发现依然没人自杀。不失一般性,以R1为例,R1可推断出R2,R3都看到了2个红眼睛,都在等待事态发展,就是说存在三个红眼睛,从而推断出自己是红眼睛,于是R1在当晚就自杀了。同理R2,R3也自杀了。蓝眼睛们在第四天发现看到的三个红眼睛都自杀了,由此推断自己是蓝眼睛,于是快乐地生活下去。

总结

于是,我们可能归纳出一个可能的结论:旅行者宣告红眼睛存在前,大家相安无事;旅行者宣告红眼睛存在后,第k天时,k个红眼睛将在晚上自杀。 该结论可以用归纳法证明,详细可以看这里,我就不详述了。在这里我说一下自己的见解:

  1. 没人愿意自杀,所以各人的推理都是以自己是蓝眼睛为起点。
  2. 旅行者宣告红眼睛的存在后,某个看到k个红眼睛的人O会这样想:“眼前的其中一个红眼睛R1会看到比我少一个红眼睛(O认为自己是蓝眼睛)”,而且O认为R1认为R2也是像自己那样想的,同样地O认为R1认为R2认为R3也是这样想的…如此层层深入,到最后O认为R1认为R2…Rk-1认为Rk看到的全部是蓝眼睛。

    O要等到第k天才能从R1的行为来判断自己的状态:第二天一早,O知道Rk不会死,O认为R1认为R2认为…Rk-2认为Rk-1认为Rk看到了不止一个红眼睛,O认为R1认为R2认为…Rk-2会根据Rk-1和Rk在第三天存活情况判断自己眼睛的颜色;第三天一早O知道Rk-1和Rk都不会死,O认为R1认为R2认为…Rk-3认为Rk-2认为Rk-1和Rk看到了不止两个红眼睛,O认为R1认为R2认为…Rk-3会根据Rk-2、Rk-1和Rk在第三天存活情况判断自己眼睛的颜色…如此类推。 到了第k天,如果眼前这些红眼睛都没有死,那么O知道了R1和R2和…Rk-1和Rk看到了不止k-1个红眼睛,也就是说自己也是红眼睛,O将会在今晚自杀。如果眼前这些红眼睛全部死了,那么O知道了R1和R2和…Rk-1和Rk看到了k-1个红眼睛,自己是蓝眼睛,所以不会自杀。

  3. 如果旅行者不宣告,那么第二天一早,O知道Rk不会死,但O认为R1认为R2认为…Rk-2认为Rk-1不知道Rk没有死是因为不知道存在红眼睛还是看到了不止一个红眼睛;第三天一早,O知道Rk-1和Rk都不会死,但O认为R1认为R2认为…Rk-2不知道Rk-1和Rk没有死是因为前一天的不确定还是看到了不止两个红眼睛…如此类推,第k天时,O不知道昨天晚上没人自杀是因为前一天的不确定还是有k个红眼睛,所以O不会自杀。

  4. 可以看出,每个人判断自己是红眼睛的逻辑的最深层的条件就是由于存在红眼睛,而且现在我看到其余的人都是蓝眼睛,所以我是红眼睛。这也是整个归纳法的初始条件

有一种不同的观点是:题目中说明岛上有5个红眼睛和95个蓝眼睛,那就是说,每个人都能看到红眼睛,那么旅行者说的“你们这里有红眼睛的人”没有增加任何信息,是一句废话。但我认为:

  1. “每个人都能看到红眼睛”和“你们这里有红眼睛的人”是不等价的,后者还带有“任意一人知道了另一个人知道了…另一个人知道了存在红眼睛”这个信息。
  2. 假设有两个红眼睛,那么岛上每个人都可以看到红眼睛(红眼睛看到一个,蓝眼睛看到两个)。第一天早上,某人看到红眼睛,但是不知道自己是不是红眼睛,所以没有自杀。第二天早上,某个人看到红眼睛没有死,他认为有两种可能导致的:一是自己是蓝眼睛,红眼睛看到全部的都是蓝眼睛(我认为他不知道存在红眼睛),所以红眼睛昨晚没有自杀;二是自己是红眼睛,昨天眼前的这个红眼睛他知道我知道存在红眼睛,没有死是因为他看到我是红眼睛(我认为他知道存在红眼睛),他跟我有同样的疑惑,所以没有自杀。由于这个人不知道哪种推断是正确的,所以他今天不会自杀。造成这种情况的根本原因在于某个人不知道红眼睛知不知道存在红眼睛,而旅行者的话推翻了其中一种可能,所以某个人才能从前一天晚上的自杀情况推断自己是不是红眼睛。
  3. 3、4、5…k个红眼睛的情况也类似,每个人都可以看到红眼睛,但是每个人都作了层层的推断,最终都无法确定造成目前的状态的原因,所以无人自杀。当旅行者宣布存在一个红眼睛时,推理链条的分支被剪掉,可能性消失了,于是发生了自杀事件。
  4. 可以说,旅行者的话使得归纳法中n = 1时的条件成立了,于是整个死亡链条就开启了。