pandas 中有一个让人捉摸不透的警告:
有人说,你用了"链式赋值操作",你应该:
事实上,这样子也会出来警告:
警告信息真的让人无语。有一些无脑的教程会说,你得用 copy:
无缘无故为啥要复制一整份数据。难道每次赋值都要 copy ?
我在 pandas 专栏中也详细讲解了其中的原理,主要是驳斥了网络上一些无脑说法。今天我们换一个角度,尝试成为 pandas 作者,看看当时作者到底遇到了什么样的难题,使得他做出这样子设计。
为什么我不把文章发布在 pandas 专栏中?因为本文涉及的是 python 的非初级知识点
假设我们是 pandas 的作者,现在要设计数据表(DataFrame)的定义:
MyFrame
看看怎么使用
筛选是数据表常规操作,添加一个 where
的函数:
MyFrame
对象。 因为我们不希望后续操作会影响原来的数据现在可以条件筛选:
现在的问题是,筛选总是用 where
显得太啰嗦。在 python 中对集合的物件取出元素,是有专门的语法 [...]
。
对象[切片范围]
这不也是一种数据筛选的方式吗?我们自定义的类也能有这样子的语法支持吗?
从对象的角度来说,它只不过是数据与函数的结合体。显然语法中的 []
应该是一个函数。但 python 中是不可能如下定义函数名字的:
python 的作者心想,既然特殊符号不行,那就用比较不常用又合法的函数名字代替吧。所以,集合的物件取出元素的函数名字如下:
__getitem__(self,...)
,就表示当使用 语法 对象[0:-2]
会调用的函数where
函数(没必要重新实现一次)。现在我们的数据表可以这样子使用:
语义感满满。
同样的套路,我们可以实现赋值操作。首先实现一个 update
函数
重点来了, 索引赋值操作 同样有魔法方法:
__setitem__(self, keys, new_value)
update
函数即可看看使用方式就很容易理解这个魔法方法:
以下是魔法方法调用示意图:
可以想象,当年 pandas 作者做到这一步也是信心满满,这语义牛逼了!但不出意外的话,很快就会出意外!
许多初学者以为,索引赋值操作会执行2个步骤( 错误理解 ):
__getitem__
,得到了一个新的数据表现在我们自己实现了一遍就清楚知道,实际上代码只调用了 __setitem__
函数, 没有调用 __getitem__
,因此不会产生任何新的对象。
再看下面的代码:
这里就会执行2个步骤:
f1[cond]
,也就是执行了一次 __getitem__
,返回了一个全新的对象__setitem__
,完成赋值操作。注意,这一步执行的操作,不是作用在 f1 对象上如果代码换一种写法,就很容易理解:
此时,pandas 的作者有点绝望了。因为这是 python 的机制,他无法改变。唯一能做的,就是做一个警告,用于提醒用户。
此时他灵机一动,想到了一个简单可行的机制。
问题核心就在于:
既然是 __getitem__
的祸,那就从这里做手脚吧。
首先,在对象初始化的时候,给一个标志属性:
在 __getitem__
中,返回全新对象之前,修改新对象的 _shadow
属性:
接下来就很简单,在 __setitem__
里面,按需出警告:
实际使用:
这种警告机制的问题在于,大部分情况下,我们会无意识产生 "影子对象" 。比如最常见的一种情况:
你觉得这种设计思路是不是挺巧妙,同时又让人有点无语?