為什麼是鍊式呼叫?
鍊式調用,或者也可以稱為方法鏈(Method Chaining),從字面意思上來說就是將一些列的操作或函數方法像鍊子一樣穿起來的 Code 方式。
我最開始感知鍊式呼叫的「美」,還要從使用 R 語言的管道操作符開始。
library(tidyverse) mtcars %>% group_by(cyl) %>% summarise(meanmeanOfdisp = mean(disp)) %>% ggplot(aes(x=as.factor(cyl), y=meanOfdisp, fill=as.factor(seq(1,3))))+ geom_bar(stat = 'identity') + guides(fill=F)
對 R user 來說,對這一段程式碼很快就能明白整個流程步驟是怎麼樣的。這一切都是透過符號%>%(管道操作符)談起。
透過管道操作符,我們可以將左邊事物傳遞給下一個事物。這裡我將mtcars資料集傳遞到group_by 函數中,然後將得到後的結果再傳遞到summarize函數,最後傳遞到ggplot函數中進行視覺化繪製。
如果我沒有學會鍊式調用,那麼最開始學習R 語言的我一定是這樣寫:
library(tidyverse) cyl4 <p>如果不使用管道操作符,那麼我將會進行不必要的賦值,並且覆蓋原有的資料對象,但其實當中產生的cyl#、data 其實最後都只是為graph 這一張圖片所服務的,因此導致的問題就是程式碼會變得冗餘。 </p><p>鍊式呼叫在極大程度簡潔程式碼的同時,也提高了程式碼的可讀性,能夠很快速地了解到每一步都是在做什麼。這種方式對於做資料分析或處理資料時是十分有用,減少創建不必要的變數時,能夠以快速、簡單的方式進行探索。 </p><p>你能在很多地方看到鍊式呼叫或管道操作的身影,這裡我舉除了 R 語言以外的兩個典型例子。 </p><p>一個是Shell 語句:</p><pre class="brush:php;toolbar:false">echo "`seq 1 100`" | grep -e "^[3-4].*" | tr "3" "*"
在shell 語句中使用「|」管道操作符能夠快速地實現鍊式調用,這裡我首先是列印1-100的所有整數,然後將其傳入到grep方法中,提取由3 或4 開頭的所有部分,然後將這部分傳入到tr 方法中,並對數字包含3 的部分用星號替換。結果如下:
另外一個是Scala 語言:
object Test { def main(args: Array[String]): Unit = { val numOfseq = (1 to 100).toList val chain = numOfseq.filter(_%2==0) .map(_*2) .take(10) } }
在這段範例中,首先numOfseq 這個變數包含了從1-100 的所有整數,然後從chain部分開始,我首先在numOfseq的基礎上呼叫了filter 方法,用以篩選這些數字中為偶數的部分,其次在呼叫map 方法,將這些被篩選出來的數乘以2,最後使用take 方法從新構成的數字中取出前10個數,這些數共同賦值給了chain 變數。
透過以上的敘述,相信你能對鍊式調用有一個初步的印象,但是一旦你掌握了鍊式調用,那麼除了會讓你的程式碼風格有所改變以外,你的程式設計思維也會有不一樣的提升。
Python 中的鍊式呼叫
在Python 中實作一個簡單的鍊式呼叫就是透過建構類別方法並傳回物件本身或傳回歸屬類別(@classmethod )
class Chain: def __init__(self, name): self.name = name def introduce(self): print("hello, my name is %s" % self.name) return self def talk(self): print("Can we make a friend?") return self def greet(self): print("Hey! How are you?") return self if __name__ == '__main__': chain = Chain(name = "jobs") chain.introduce() print("-"*20) chain.introduce().talk() print("-"*20) chain.introduce().talk().greet()
在這裡我們建立一個Chain 類,需要傳遞一個name 字串參數進行實例物件的創建;當中這個類別裡有三個方法,分別是introduce、talk以及greet。
由於每次回傳的是self 自身,那麼我們就可以源源不斷地呼叫物件歸屬類別中的方法,結果如下:
hello, my name is jobs -------------------- hello, my name is jobs Can we make a friend? -------------------- hello, my name is jobs Can we make a friend? Hey! How are you?
在Pandas 中使用鍊式調用
前面鋪墊了這麼多終於談到有關於Pandas 鍊式呼叫部分
Pandas 中的大部分方法都很適合使用鍊式方法進行操作,因為經過API 處理後返回的往往還是Series 類型或DataFrame 類型,所以我們可以直接就調用相應的方法,這裡我以我在今年2 月份左右給別人做案例演示時爬取到的華農兄弟B 站視頻數據為例。可以透過連結進行取得。
數據欄位資訊如下所示,裡面有300 條數據,並且20 個欄位:
欄位資訊
但在使用在這部分資料之前,我們還需要對這部分資料進行初步的清洗,這裡我主要選取了以下欄位:
aid:影片對應的av 號
comment:評論數
play:播放量
title:標題
# video_review:彈幕數
length 播放量长度只显示了分秒,但是小时并未用「00」来进行补全,因此这里我们一方面需要将其补全,另一方面要将其转换成对应的时间格式
链式调用操作如下:
import re import pandas as pd # 定义字数统计函数 def word_count(text): return len(re.findall(r"[\u4e00-\u9fa5]", text)) tidy_data = ( pd.read_csv('~/Desktop/huanong.csv') .loc[:, ['aid', 'title', 'created', 'length', 'play', 'comment', 'video_review']] .assign(title = lambda df: df['title'].str.replace("华农兄弟:", ""), title_count = lambda df: df['title'].apply(word_count), created = lambda df: df['created'].pipe(pd.to_datetime, unit='s'), created_date = lambda df: df['created'].dt.date, length = lambda df: "00:" + df['length'], video_length = lambda df: df['length'].pipe(pd.to_timedelta).dt.seconds ) )
这里首先是通过loc方法挑出其中的列,然后调用assign方法来创建新的字段,新的字段其字段名如果和原来的字段相一致,那么就会进行覆盖,从assign中我们可以很清楚地看到当中字段的产生过程,同lambda 表达式进行交互:
1.title 和title_count:
原有的title字段因为属于字符串类型,可以直接很方便的调用str.* 方法来进行处理,这里我就直接调用当中的replace方法将「华农兄弟:」字符进行清洗
基于清洗好的title 字段,再对该字段使用apply方法,该方法传递我们前面实现定义好的字数统计的函数,对每一条记录的标题中,对属于\u4e00到\u9fa5这一区间内的所有 Unicode 中文字符进行提取,并进行长度计算
2.created和created_date:
对原有的created 字段调用一个pipe方法,该方法会将created 字段传递进pd.to_datetime 参数中,这里需要将unit时间单位设置成s秒才能显示出正确的时间,否则仍以 Unix 时间错的样式显示
基于处理好的created 字段,我们可以通过其属于datetime64 的性质来获取其对应的时间,这里 Pandas 给我们提供了一个很方便的 API 方法,通过dt.*来拿到当中的属性值
3.length 和video_length:
原有的length 字段我们直接让字符串00:和该字段进行直接拼接,用以做下一步转换
基于完整的length时间字符串,我们再次调用pipe方法将该字段作为参数隐式传递到pd.to_timedelta方法中转化,然后同理和create_date字段一样获取到相应的属性值,这里我取的是秒数。
2、播放量趋势图
基于前面稍作清洗后得到的tidy_data数据,我们可以快速地做一个播放量走势的探索。这里我们需要用到created这个属于datetime64的字段为 X 轴,播放量play 字段为 Y 轴做可视化展示。
# 播放量走势 %matplotlib inline %config InlineBackend.figure_format = 'retina' import matplotlib.pyplot as plt (tidy_data[['created', 'play']] .set_index('created') .resample('1M') .sum() .plot( kind='line', figsize=(16, 8), title='Video Play Prend(2018-2020)', grid=True, legend=False ) ) plt.xlabel("") plt.ylabel('The Number Of Playing')
这里我们将上传日期和播放量两个选出来后,需要先将created设定为索引,才能接着使用resample重采样的方法进行聚合操作,这里我们以月为统计颗粒度,对每个月播放量进行加总,之后再调用plot 接口实现可视化。
链式调用的一个小技巧就是,可以利用括号作用域连续的特性使整个链式调用的操作不会报错,当然如果不喜欢这种方式也可以手动在每条操作后面追加一个\符号,所以上面的整个操作就会变成这样:
tidy_data[['created', 'play']] \ .set_index('created') \ .resample('1M') .sum() .plot( \ kind='line', \ figsize=(16, 8), \ title='Video Play Prend(2018-2020)', \ grid=True, \ legend=False \ )
但是相比于追加一对括号来说,这种尾部追加\符号的方式并不推荐,也不优雅。
但是如果既没有在括号作用域或未追加\ 符号,那么在运行时 Python 解释器就会报错。
3、链式调用性能
通过前两个案例我们可以看出链式调用可以说是比较优雅且快速地能实现一套数据操作的流程,但是链式调用也会因为不同的写法而存在性能上的差异。
这里我们继续基于前面的tidy_data操作,这里我们基于created_date 来对play、comment和video_review进行求和后的数值进一步以 10 为底作对数化。最后需要得到以下结果:
统计表格
写法一:一般写法
一般写法
这种写法就是基于tidy_data拷贝后进行操作,操作得到的结果会不断地覆盖原有的数据对象
写法二:链式调用写法
链式调用写法
可以看到,链式调用的写法相比于一般写法而言会快上一点,不过由于数据量比较小,因此二者时间的差异并不大;但链式调用由于不需要额外的中间变量已经覆盖写入步骤,在内存开销上会少一些。
结尾:链式调用的优劣
从本文的只言片语中,你能领略到链式调用使得代码在可读性上大大的增强,同时以尽肯能少的代码量去实现更多操作。
当然,链式调用并不算是完美的,它也存在着一定缺陷。比如说当链式调用的方法超过 10 步以上时,那么出错的几率就会大幅度提高,从而造成调试或 Debug 的困难。比如这样:
(data .method1(...) .method2(...) .method3(...) .method4(...) .method5(...) .method6(...) .method7(...) # Something Error .method8(...) .method9(...) .method10(...) .method11(...) )
以上是Python怎麼實現鍊式調用的詳細內容。更多資訊請關注PHP中文網其他相關文章!