刘思宁的学习笔记

后续更新会在简书进行:http://www.jianshu.com/u/99f3318bcc01

如何证明 Pearson 相关系数的值域为 -1 到 1

| Comments

Pearson 相关系数是一个用来度量2个变量间线性关系强度的统计量。这听起来有点绕,但用图形解释会很直观:

(在二维坐标下)2个变量形成一个点,这些点是否大致沿着一条直线发展。如果正好在一条直线上,那么相关系数是1,或者-1。

但问题来了,为什么是 1 或 -1 ?更进一步,为什么各种地方都说 Pearson 相关系数的值域是[-1, 1] ?我搜索了一些地方,但是没有直接的证明,所以看看能不能自己填补一下这个。

根据国内某教材给出的计算公式1,我完全看不出来为什么:

如果我是当年还在上学的我,觉得考试能得分就行了,顶多就把公式背下来,恶劣一点还会想办法不用背下来。但今天,学习是为了解决问题,不多理解一点这个公司在现实中的因果(而不是在分数上的因果),不太敢拿来解决问题2

其实,Pearson 相关系数更本质的计算方法是,变量1的各个数据点到平均数的距离(以标准方差为一个距离单位,下同)乘以变量2对应的数据点到平均数的距离,的平均值。或者说,就是变量1的各个数据点的 P 值,乘以变量2对应的数据点 P 值,的平均值。

写成表达式,就是:

最后得到的表达式意味着啥?

假设我们有两个向量,一个向量是:

另一个向量是:

再想想向量的点乘是什么:

这样,表达式的分子就是 x.y

而分母是||x|| . ||y||

x.y / ||x|| . ||y|| 也就是 cosø,cosø 的值域是 [-1, 1]。

关键词

Pearson’s r, Pearson coefficient correlation, range, prove, 线性相关系数,皮尔逊相关系数,值域,证明,


  1. 《统计学》 贾俊平 中国人民大学出版社 

  2. 虽然有时候还是先用再说 

5分钟,用一个例子了解对象访问

| Comments

值是一样的,但对象是不一样的

每一个对象都有一个存储的物理位置。

有时候,用a == b这样的符号来判断两个对象是不是一样,你只希望得到他们的值是否相同。但是,他们的值相同,不代表他们是同一个对象。

同一个值,如果被存储在两个不同的地方,那么就是两个不同的对象。

以下用 python 聚个例子:

>>> a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> b = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> a == b
True
>>> a is b
False

这里,a 和 b 都被赋值成一个 list。a 和 b 的值可以用 a == b来判断,而 a 和 b 是不是同一个对象可以用a is b来判断。

我们取出 a 和 b 两个对象在 python 中的标识号(identity)

>>> id(a)
4549481536
>>> id(b)
4549481968

可以看到他们的 id 是不一样的。他是两个不同的对象。你改变 a 对象,不会改变 b 对象。

但是如果有一个 c 对象,他和 a 对象是一个对象,那么你改变 a 对象,也就是改变了 c 对象。

>>> a
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> c = a
>>> a is c
True
>>> a[0] = [9,9,9]
>>> a
[[9, 9, 9], [4, 5, 6], [7, 8, 9]]
>>> c
[[9, 9, 9], [4, 5, 6], [7, 8, 9]]

你会发现,当我们改变了 a 这个 list 对象的第一个元素,那么 c 的值也就改变了。因为他们本来就是一个对象。

除此之外,你会发现我们在上面赋值的 a 其实的3个 list 对象组成的一个 list。我们可以做下面这样一个实验:

>>> a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> a
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> id(a) # a 的 id
4549481608
>>> id(a[0]) # a 的第一个元素的 id
4549481104
>>> d = [100,100,100]
>>> id(d) # 新对象 d 的 id
4549481896

我们给 a 赋值一个 list 对象,这个 list 对象又包含3个 list 对象。如果我们把 d 这个 list 对象变成给 a 的第一元素,你说会发生什么呢? a 的 id 会变吗,a 的第一个元素的 id 会变吗,d 的 id 会变吗?

>>> a[0] = d
>>> a[0] is d
True
>>> id(a)
4549481608
>>> id(a[0])
4549481896

你会看到,a 的 id 并没有变,a 的第一个元素变成了 d。不是说 a 的第一个元素的值是 d,而是说 a 的第一个元素本身就是d。

这有什么用呢?这不是对象的捉迷藏游戏,让你猜猜对象藏在哪。而是有下面这个用处:

>>> a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> for x in a:
...     print x
...     for y in x:
...             x = [100,100,100]
...             print y
...

你猜猜这几行代码的结果是什么?这里面在对 x 进行循环的时候更改了 x,你说会因此而改变 y 么。把 y 打印出来会是100,100, 100 么?

答案是:

[1, 2, 3]
1
2
3
[4, 5, 6]
4
5
6
[7, 8, 9]
7
8
9

for y in x的第一循环中,x 代表的对象的值是 [1, 2, 3] ,他会有一个自己的 id,我们叫 id-1,当你在循环中给 x 赋值一个新的对象,比如上面的[100,100,100],y 在进行循环时,访问的还是 id-1 的对象,而不是 [100,100,100] 这个对象。所以,打印出来的 y 才没有100.

不过,如果你在下一个循环中访问 x,x 就已经是[100,100,100]了:

>>> for x in a:
...     print x
...     for y in x:
...             print y
...             x = [100,100,100]
...     print x
...     for y in x:
...             print y
...
[1, 2, 3]
1
2
3
[100, 100, 100]
100
100
100
#...... 省略

还有一个地方,我们在刚才的一个a[0] = d 的实验中发现,如果更改 a 的第一个元素,a[0] 这个对象确实是变了,但 a 这个对象的 id 没变。也就是说,如果在一个 for 循环中总是去访问一个可迭代的对象(iterable),那如果我们只改变这个对象的元素,而不是试图改变那个正在被访问的对象的 id,会有什么影响呢?

>>> a
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> for x in a:
...     print x
...     for y in x:
...             print y
...             x[2] = 8888
...     print x
...

因为,我们是改变了 x 中的一个元素,而不是给 x 赋值一个全新的对象,如果 y 继续访问 x 对象的话,是能够访问到被更改了的x[2]的,答案中会输出一些8888。

[1, 2, 3]
1
2
8888
[1, 2, 8888]
[4, 5, 6]
4
5
8888
[4, 5, 8888]
[7, 8, 9]
7
8
8888
[7, 8, 8888] 

其实,在改变 x 的时候,因为 a 中包含 x 对象,a 也被改变了。

>>> a
[[1, 2, 8888], [4, 5, 8888], [7, 8, 8888]]

Recall

  1. list 对象是一组对象的集合。改变 list 里面的对象,不会影响 list 对象本身的 id。原来的 id 代表的对象,还是可以被访问。
  2. for 循环一旦开始,就只能访问一个 iterable 对象。如果在 for 循环中更改对象,不能被访问。

为什么我就不能接受现实呢?

| Comments

为什么我们就不能接受现实呢?

声明,本文是一篇自我反思

最近又是双十一,还记得几年前的双十一,我和一个哈工大环境科学系毕业的同事聊到双十一要买什么。我们都开玩笑的说,感觉没什么可买的。也记得,我们在北京的雾霾中开玩笑的说,我觉得戴口罩不管用的。

我们是几乎同时来到一家创业公司工作,创业公司的待遇是比较低的,我们又是初出茅庐的新人(实际意思是,不能给公司带来太多价值的人)。总之,就是没有多少赚钱能力,也确实没钱的人。

今天,我所在的公司里(已经是另外一家公司)还是有同事正在经历他们职场中的第一个双十一,Ta也和其他同事进行了我们当年的谈话。

A:你双十一要买什么?
B:我什么都没买呢,因为算不清楚淘宝今年的优惠政策。
A:我也是,我不喜欢双十一的物流,太慢了。我受不了等待物流的那种感觉。

我对别人的对话不应该有任何评价,我也不会有。只是这段谈话让我想到了多年前的我。

多年前,我也觉得戴口罩防不了雾霾。但是,今天我还是买口罩。可能是因为怕了,也可能是因为兜里的钱至少够买口罩了。双十一我也会买点东西,可能确实是因为便宜,也可能是因为不用太担心月底的房租怎么交的问题了。

今天,如果我问两年前的自己为什么双十一不卖东西,为什么不带口罩。诚实一点回答,是因为没钱。没钱在意那些细节,能忍的就忍了。

自己对自己还是可以更诚实一点的。虽然对别人也应该诚实,但有时候,面子上的问题还是会带来心理上的尴尬,和生理上的不适,让自己匆匆忙忙的选择了行动,给出一个似是而非的答案,而不是给出真实的原因。总之,对自己更容易诚实一点。

如果我对自己比对别人诚实,很多行为的真实原因,就和我给别人的理由有所不同,甚至截然相反的了。

很多时候,我说我不喜欢做某件事,请不要相信。更真实的原因应该是我没有能力做,或者知道自己做不好。

很多时候,我说我不喜欢吃某家店,不是因为我真的不喜欢,我的口味其实很广,也很愿意尝新。更真实的原因是我这段时间可能没钱。

雷达里奥说,我们必须渴求真实,甚至不惜为了换取真实,而被羞辱。我得看清真实,我必须告诉自己到底发生了什么,是什么在决定我的选择。而不是随便给一个句子,就以为那是理由。

查理芒格在一次演讲中提到,他发现人们有「重视理由倾向」1,即如果你想做一件事情,只需要给出一个理由,哪怕是无意义的理由,人们都会觉得更合理。

我不做一件事,是因为我不喜欢。那你为什么不喜欢呢?我不喜欢是因为我不喜欢……这是多么天衣无缝的理由啊。

我最好不要用这样的理由让自己为自己的不行动感到心安理得。

真实的原因可能是很残酷的,残酷到清晰的指向一个点:我不行。至少现在还没有能力,所以没法做某些事情。而不是看起来的,我不喜欢,所以选择不做。

只有看到这一点,才能知道自己应该进步。

选择总是有的,完全没有选择的事情我知道的不多(除了时间不能停止,不能逆转之外,很多事情最后都能挤出几个选项来)。但对于我遇到的问题或者任务,选项不是我之前以为的,做,还是不做,更不是喜欢,还是不喜欢。而是学习着慢慢做好,还是允许自己放弃(或者暂时放弃)在这个领域的安全感。

今天我所面临的困境,多多少少都有自己的问题,都是自己能力上的欠缺所以导致的问题。这比「我不喜欢」更真实。

在这里,「我不喜欢」更可能是一种心理保护机制。是大脑对世界的描绘,而不是真实世界的情况。心理保护一下也挺好,不至于心情灰暗,四肢瘫痪。但在夜深人静的时候,我希望自己知道早上起来应该干什么,而不是心满意足的觉得我那小世界尽在掌握。


  1. 这个发现来自于《影响力》这本书 

手把手教你给 Linux 虚拟机扩容硬盘

| Comments

为什么我们要给 linux 虚拟机的硬盘做分区?
这是为了一个更大的需要,扩大一台虚拟机的硬盘。

大家都知道,如果硬盘不够用了,你的文件就没地方放了,云服务商提供的虚拟机也是这样的。虽然在云服务商那里,通过点几个键就能调来一块更大的硬盘,但是 linux 系统会不知道新的硬盘已经来了,正在运行的文件系统(filesystem)也没法使用多出来的硬盘空间1

如果想利用多出来的空间,就需要我们在 linux 上对硬盘重新做一次分区。

把硬盘变大

因为我们的目标是把硬盘分区变大,所以先要把硬盘的物理存储空间变大。如果你想把分区变小可以跳过这一步。

因为我使用的是 AWS,可以在控制台中进行这一扩容操作。其他云服务请查找用户指南。

把根卷挂到辅助虚拟机上,作为一个附加硬盘

如果你要分区的硬盘是根存储设备,你是无法在当前的机器上进行的。必须把要分区的硬盘挂载到一个新主机上,作为一块可处理的附加硬盘来分区。这里的新主机,我们成为辅助虚拟机(依据 AWS 文档中的称呼)。

那怎么做到这一点呢?

首先,把原来的主机关机。把硬盘分离出来。这在虚拟机上是很容易做到的,只需要点几个键,完全不需要螺丝刀拧螺丝。

接下来,强烈建议你在这个时候给硬盘做个备份。如果你也使用 AWS,就给这个硬盘建立一个快照

然后,启动一个新的linux 系统的虚拟机作为辅助虚拟机。这个辅助虚拟机需要自带一个根卷。当启动完成后,把你刚刚卸下来的那块硬盘附加上去

如何判断真的附加上去了呢?用 ssh 登录辅助虚拟机后,用 lsblk 命令来列举一下所有的块设备 (lsblk 的意思就是 list block)。你可能会看到这样的情景。

[ec2-user ~]$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  30G  0 disk
└─xvda1 202:1    0  30G  0 part /
xvdb    202:16   0  30G  0 disk /mnt
xvdf    202:80   0  35G  0 disk
└─xvdf1 202:81   0   8G  0 part

xvdf1这块分区,他只占了xvdf这个硬盘35G 空间中的8G,剩下的27个 G,全都没法用。我们要做的,接下来要做的,就是让 xvdf1 的分区占满整个硬盘,以便物尽其用。

在辅助虚拟机上,给附加硬盘做分区

AWS 的 Guide 中提供了两个分区方法,我使用了 parted 这个工具处理一个 msdos 的分区。以下只对此做一个说明,如果你想使用别的工具来分区,或者硬盘是别的分区格式,请仔细查看文档。

首先进入 parted。告诉 parted 你要操作哪个硬盘。比如,我们刚才的lsblk显示有一块叫做 xvdf 的硬盘有必要重新分区,所以我们就告诉 parted 是 /dev/xvdf(前面要加上/dev/前缀)。

sudo parted /dev/xvdf

接下来我们把 parted 的度量单位更改为扇区(s),在接下来的 parted 操作中,你看到的度量单位都会是 s。

(parted) unit s

然后我们来看看当前的分区情况是什么样的:

(parted) print

我得到的结果大致是:

Model: Xen Virtual Block Device (xvd)
Disk /dev/xvdf: 104857600s
Sector size (logical/physical): 512B/512B
Partition Table: msdos

Number  Start            End             Size             Type     File system  Flags
    1          2048s   35649535s  35647488s  primary    ext4            boot

因为我是 msdos 的分区格式,在接下来恢复分区时,要使用到的数据有 number, start, type, flags。

现在删除当前的分区,真的删除。我们即将建立一个新的分区。

(parted) rm 1

这里的1,就是刚刚显示的 number。然后建立新的分区,让新分区100%的使用硬盘的空间。

(parted) mkpart primary 2048s 100%

这里的mkpart接收3个参数,分别是我们在上面得到的 type, start ,以及我们自己想要的分区比例(这个100%是和 start 相对的 end,根据文档,也可以写成 -1, 来表示使用全部硬盘)。

最后,别忘了把 flag 还原。

(parted) set 1 boot on

这里的 1 就是我们新分区的 number, boot 就是原始的 flag,只不过在新分区中丢失了,我们还原回去。

最后,print 一下,来查看新的分区信息。

(parted) print

如果没有问题,就可以quit命令退出了。

现在,再来使用lsblk 就会发现xvdf1 占满了整个硬盘。

把卷挂载回到原来的虚拟机,作为根卷

分区我们已经弄完了,可是原来主机还停着,并且没有硬盘。这个时候我们应该把硬盘从辅助机器上卸载下来装回去。在装回去的时候请注意,云服务上会让你命名这个硬盘在系统中的名称。如果你当初分离出来的时候,硬盘叫/dev/sda1,装回去的时候,还得叫/dev/sda1

然后把原始虚拟机重新开机,ssh 登录后使用lsblk命令,看看新的分区是不是起了作用。再使用df -h命令查看文件系统(df 的意思就是 disk filesystem, -h 参数是 human 或者 human readable 的意思),检查/dev/xvda1的Size大小是不是你要的。

Recall

  1. 分离硬盘
  2. 在辅助机器上分区(使用 parted 工具中的 mkpart 命令建立一个使用整个硬盘的分区)
  3. 挂载会原来主机

这就好像是一个人在睡梦中被换了一个肾。我们也给虚拟机换了一个硬盘。


  1. 根据用户指南的第5小点, 或者这里的第一段 

在 Python 中的方法对象,和 Ruby 中 method 对象

| Comments

在 Python 中的方法对象,和 Ruby 中 method 对象

在 Ruby 中据说一切都是对象,连 class 都是对象。而方法也是一个对象:

> "a string".method(:length)
=> #<Method: String#length>

如果你要直接调用(call)这个方法,你会用:

2.3.4 :012 > "a string".length
 => 8
2.3.4 :013 > "a string".length()  # or this
 => 8

所以,要得到方法对象,你需要用 method 方法。

但是在 python 中,语法上就有 lengthlength() 的区别,前者就像是 Ruby 中的 method 方法,后者才是真正的调用方法。

今天,我定义了一个 Vector 类,里面又一个实例方法 magnitude。但是调用的时候,用了magnitude 而不是 magnitude(),从而返回结果出乎意料。

print Vector((-0.221,7.437)).magnitude # => <bound method Vector.magnitude of <__main__.Vector object at 0x10ed2bfd0>>

用 js 实现 ajax 文件上传

| Comments

用 js 实现 ajax 文件上传

很多时候,我们想让文件被拖拽到选择框中之后,就自动上传。这个功能可以用 ajax 来实现。如果想用 js 来完成这个 ajax,步骤应该如下:

  1. 从文件输入框获取文件
  2. 构建发往服务器的数据(form data)
  3. 构建 XMLHttpRequest 请求

0. 准备 html

首先,我们假想一个简单的 html:

<input id="user-avatar" type="file" name="user[avatar]">

如果不使用 ajax 提交,这个 input 会形成名字为user[avatar]的表单数据。而我们用 ajax 提交,也要用同样的数据格式。

那么,接下来,我们就来实现用 ajax 提交表单数据。

1. 从文件输入框获取文件

我们先要从 input 标签中获得文件,然后把才能把这个文件装入表单数据。

document.addEventListener('DOMContentLoaded', function () {
    document.getElementById('user-avatar').onchange = function () {
        var file = this.files[0];
        // Something else

    }
})

利用这段代码,我们能在 DOM 加载后,监听 input 是否有文件加载,如果有,就取出文件列表中的第一个文件this.files[0]。请注意,我们这里只考虑上传一个文件的过程。

你注意到了,在这段代码中有一段注释,我们要在注释的地方,写上构建表单数据的代码,和 xhr (XMLHttpRequest) 请求的代码。让我们一个一个来。

2. 构建表单数据

查看文档,可以看到说:

The FormData provides a way to easily construct a set of key/value pair representing form fields and their values, which can then be sent easily using the XMLHttpRequest.send() method.

短短的一句文档,用了两个 easily,那我们就看看他怎么的 easily.

var formData = new FormData();
formData.append('user[avatar]', file, file.name);

这样,我们就能把上一步拿到的文件,插入到表单数据中。这里用的FormData.append()方法的第一个参数,就是表单数据的名称,跟我们 input 标签的name是样的(所以你也可以用this.name传入这个参数)。

3. 构建 XMLHttpRequest 请求

在上一步引用的文档中已经告诉我们,FromData可以很容易的被XMLHttpRequest发送。但在发送之前,我们需要构建一个请求,让这个请求,有 url,可以读取返回结果。

var xhr = new XMLHttpRequest();
xhr.open('POST', '/users', true);
xhr.onload = function () {
    if (this.status === 200) {
        alert('Upload Success');
    } else {
        alert('An Error Occurred');
    }
}
xhr.send(formData);

这里的前几步都是在构建 xhr 请求,通过XMLHttpReques.open()方法,指定请求的 url 等,具体的参数可以查看文档。然后通过最后一步xhr.send(formData);发送到服务器。

总结

代码合起来是这样的:

html:

<input id="user-avatar" type="file" name="user[avatar]">

js:

document.addEventListener("DOMContentLoaded", function(){
    document.getElementById("user-avatar").onchange = function () {
        var file = this.files[0];
        var formData = new FormData();
        formData.append('user[avatar]', file, file.name);
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/users', true);
        xhr.onload = function () {
            if (this.status === 200) {
                alert('Success');
            } else {
                alert('An Error Occured!');
            }
        };
        xhr.send(formData);
    }
})

参考

Uploading Files with AJAX - Treehouse Blog

再次理解 controller action 是拿 instance variable 来渲染 view 的

| Comments

再次理解 controller action 是拿 instance variable 来渲染 view 的

我们在 controller 里面都写过这样的action:

def edit
    @user = User.find(params[:id])
end

def update
    @user = User.find(params[:id])
    if @user.update(user_params)
        redirect_to users_path
    else
        render :edit
    end
end

我对里面程序的执行步骤的理解发生了几次变化:

最开始,我以为,如果@user update 不成功,就会回退到 edit 页。

然后,当我看到 url 并不是 edit 页的 url, 就猜想了一番,后来理解了是 render 返回了利用 edit 页的模版渲染出的 html。但我以为这个 html 还是之前的 html,没有变。但其实不是的。

现在,应该说,render 方法,是拿着在update Action 定义的 @user 这个实例变量去找名为 edit 的模版文件,然后渲染出了 html。如果你在 edit 模版中使用的实例变量是@user,而 update 定义的实例变量不是 @user,比如:

def update
    @member = User.find(params[:id])
    if @memeber.update(user_params)
        redirect_to users_path
    else
        render :edit
    end
end

这里用了 @member 作为实例变量。尽管也是从 users 表中查出的对象,但如果这个时候update失败,程序跳入render :edit,你会发现,rails 会抛出异常说 undefined method ... for nil:NilClass ,这里的 nil,就是你在 edit 模版中使用 @user, 而没有在 controller 的 action 中定义 @user 的结果。

所以,controller action 中的实例变量名称要和 view 中的一样。Rails 的使用惯例也是写成一样的。

总结

不论是 Rails 默认的render( edit action 会去 render 名为 edit 的 view),还是自定义要 render 的 view(比如在 update action 中 render 名为 edit 的 view),都需要让 action 中的实例变量和 view 中的相同。这就导致了 render 相同 view 的 action,需要有同名的实例变量(update action 中需要与 edit action 有同名的@user,谁让他 render 了 edit 的 view 呢)。

利用 onchange 绑定 handler 到元素上的事件

| Comments

利用 onchange 绑定 handler 到元素上的事件

我们在之前的一篇文章中讲过,如何用 html5 的文件接口读取图片。当时我们为了监听 input 标签的事件,用 js 给 input 元素设定了一个 onchange 事件:

document.getElementById('upload').onchange = function () {
    // do something
};

当我们用这样的代码时,在 function 内,可以用 this.files 来获取文件列表。这是因为 this 就是我们的 input 元素,他有 files 这个属性可以调用。

但是,如果我们利用 input 标签的 onchange 事件,绑定到一个 js 函数,函数内部的 this 可就不是 input 元素了,请看下面的代码:

<input type="file" onchange="previewImage();">
<img src="" id="preview">

这个时候,如何在 previewImage 中打出 this,可以发现,this 其实是一个 window 对象,而不是 input 元素。

Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …}

这个时候,如果想要读取 files 列表接没那么容易了,出发你在 window 里面重新找到 input 元素,并读取。或者,为了能在 previewImage 方法中找到我们需要的files,直接把 files 传进来。所以代码可以这样写:

<input type="file" onchange="previewImage(this.files);">

这样就把 files 穿进去,而后在方法中,从参数里找到我们要的file。

function previewImage(files){
    var file = files[0];
    var reader = new FileReader();
    reader.onload = function (e) {
        document.getElementById('preview').src = e.target.result;
    };
    reader.readAsDataURL(file);
};

参考

Using files from web applications

可以不加花括号的传参

| Comments

这是7月1日的工作总结

rubocop教你写代码

这是一个例子,我在想要给一个方法传参时,比如user.go_back_home({start_time: Time.now, transportation: 'subway'}),我这么传,是因为我定义go_back_home方法是是这么写的:

def go_back_home(options)
    # do something
end

我以为只能把参数打包成一个单独的对象(即使是Hash对象)传进来。但是rubocop说,如果我要穿一个车hash进去,那个花括号是多余的,也就是说,可以这么写:user.go_back_home(start_time: Time.now, transportation: 'subway') 

你的第一次method_missing,你的第一次OpenStruct,你的第一次super

| Comments

你的第一次method_missing,你的第一次OpenStruct,你的第一次super

我是6月24日工作总结

method_missing就是没有方法前的补救

如果你有一个User实例,叫user,他去调用address方法。但是,你的User中并没有定义这个实例方法,会发生什么:

pry(main)> u.address
NoMethodError: undefined method `address' for #<User:0x007f99d2893780>
from /Users/apple/.rvm/gems/ruby-2.3.0/gems/activemodel-5.0.1/lib/active_model/attribute_methods.rb:433:in `method_missing'

第一行说的是没有这个方法,第二行说的是出错地址在一个叫做method_missing的方法中。

如果你在user中重写这个method_missing方法,那么就有机会给user.address这个调用一个返回值。比如:

def method_missing(method, *args, &block)
    if method == :address
        "My address is ..."
    end
end

这样,你在调用address就会有返回值。

pry(main)> user.address
=> "My address is ..."

但是,这里面有个问题,如果你去调用另外一个不存在的方法,比如school,他不会报错了,因为你覆盖了正常的method_missing:

pry(main)> u.school
=> nil

本来,他应该抛出NoMethodError的。那怎么办呢,情况下一节。

super让你回归父类方法

刚才我们提到因为你覆盖了method_missing,所以一些原本属于这个方法的属性被你弄没了。你想要的其实是给这个方法增加一判断机制,但却把其他的判断机制给弄没了。怎么办?

用super。比如,把刚才的方法改为:

def method_missing(method, *args, &block)
    if method == :address
        "My address is ..."
    else
        super
    end
end

看这个方法中else的部分。他是在说,如果不符合if的条件,就去else,而else说去执行你父类的方法吧。什么是父类呢?就是你User < ActiveRecord::Base时的那个被继承者。method_missing就在ActiveRecord::Base中写了很多代码,用来处理NoMethodError,如果你用super放过你不能处理的method_missing的部分,那么,剩下的方法还是正常的。

当然super还有别的用途,比如我讲给一上来就super,然后处理一大堆之后继续在子类中处理的情况,我们以后再聊。

软体实例OpenStruct

软体实例这个词是我刚造的,类比软体动物的无定型属性。

如果你有一个OpenStruct,比如a = OpenStruct.new,那么,无论a去调用什么,都不会是NoMethodError,顶多是nil。

    [1] pry(main)> a = OpenStruct.new
=> #<OpenStruct>
[2] pry(main)> a.to_array
=> nil
[3] pry(main)> a.to_test
=> nil
[4] pry(main)> a.address
=> nil

这对于我输出一个固定格式的json,里面又有一些字段是空的,有帮助。