資料科學中 Python 的常用語法(基礎篇)

這幾天在讀這本 Data Science from Scrach (PDF連結),是一本很棒、淺顯易懂的資料科學入門書籍。其中一個章節介紹了 Python 的基礎語法和資料科學常用的進階語法,我覺得介紹得很好,既簡潔又清晰,因此我將其翻譯並整理在這裡,作為我的筆記與備忘。
資料科學中常用的 Python 語法(基礎篇)
資料科學中常用的 Python 語法(進階篇)

本章主要著重於介紹在資料處理中,非常實用的 Python 基礎語法和功能(基於 Python 2.7)。

空白字元與格式

許多程式語言使用括號來控制程式碼區塊,但 Python 則是用縮排來達成這件事:

for i in [1, 2, 3, 4, 5]:  
    print i          # "for i" 迴圈的第一行  
    for j in [1, 2, 3, 4, 5]:  
        print j      # "for j" 迴圈的第一行  
        print i + j  # "for j" 迴圈的最後一行  
    print i          # "for i" 迴圈的最後一行  
print "done looping"  

這讓 Python 的程式碼非常容易閱讀,但也代表你必須時刻留意排版格式。括號內的空白字元會被忽略,這在撰寫長表達式時非常實用:

long_winded_computation = (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20)  

也能讓程式碼更容易閱讀:

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

多行陳述式

你可以用一個反斜線 \ 來表示程式碼連接中斷在兩行之間(但這種寫法很少見):

two_plus_three = 2 + \
                 3  

模組(Modules)

無論是 Python 內建的模組,還是自行下載的第三方模組,都需要透過手動匯入才能使用。

  1. 直接匯入整個模組:
import re  
my_regex = re.compile("[0-9]+", re.I)  

這裡匯入的 re 模組是用於正規表達式(regular expression)的。匯入模組後,就可以直接將模組名稱作為前綴(例如 re.),來呼叫其提供的具體功能。

  1. 如果欲匯入的模組名稱在你的程式碼中已經被使用,可以將模組匯入時對應到另一個名稱:
import re as regex  
my_regex = regex.compile("[0-9]+", regex.I)  
  1. 如果你「很壞」,你可以將整個模組都匯入到目前的命名空間(namespace),這可能會在不經意間覆蓋你已經定義好的變數:
match = 10  
from re import *  # `re` 模組中也有一個 `match` 函式  
print match       # 會輸出 `match` 函式  

但相信你不會這麼做,因為你是一個好人。

四則運算(Arithmetic)

Python 2.7 預設使用整數除法,所以 $5 / 2 = 2$。但很多時候我們並不想要整數除法,這時可以匯入這個模組:

from __future__ import division  

匯入後,就會得到 $5 / 2 = 2.5$。
整數除法:$5 // 2 = 2$。

函式(Functions)

函式定義

函式是一種能夠接收零個或多個輸入,並返回特定輸出結果的規則。在 Python 中,我們用 def 函式名稱(參數) 的方式來定義一個函式:

def double(x):  
    """你可以在這裡寫一些關於函式功能的解釋  
    例如,該函式會將輸入內容乘以 2"""  
    # 函式主體寫在這裡,記得要縮排  
    return x * 2  

函式使用

在 Python 中,函式被視為「一級物件(first-class objects)」,這代表我們可以將函式指定給一個變數,也可以將它作為參數傳遞給其他函式或變數:

def apply_to_one(f):  
    """呼叫函式 f 並將 1 作為函式參數"""  
    return f(1)  
my_double = double          # `double` 指向上一節定義的函式  
x = apply_to_one(my_double) # `x` 等於 2  

匿名函式

我們也可以透過 lambda 來建立匿名函式:

y = apply_to_one(lambda x: x + 4)     # 等於 5  

雖然可以將 lambda 指定給其他變數,但大多數人會建議你還是盡量使用 def 來定義函式:

another_double = lambda x: 2 * x      # 不建議  
def another_double(x): return 2 * x   # 建議做法  

補充說明:

函式參數傳遞

函式參數可以定義預設值。如果呼叫函式時不帶參數,就會使用預設值;如果帶有參數,則會傳遞指定的值:

def my_print(message="my default message"):  
    print message  
my_print("hello")     # 輸出 "hello"  
my_print()            # 輸出 "my default message"  

有時候直接透過參數名稱來指定參數也很好用:

def subtract(a=0, b=0):  
    return a - b  
subtract(10, 5)   # 回傳 5  
subtract(0, 5)    # 回傳 -5  
subtract(b=5)     # 與上一個相同,回傳 -5  

字串(Strings)

你可以使用單引號或雙引號來建立字串(引號必須配對):

single_quoted_string = 'data science'  
double_quoted_string = "data science"  

用反斜線 \ 來表示跳脫字元(escape character),例如:

tab_string = "\t"      # 表示跳格符號(tab)  
len(tab_string)        # 等於 1  

當你想要使用反斜線本身(例如用於 Windows 目錄路徑或正規表達式)時,可以透過使用原始字串 r"" 來定義:

not_tab_string = r"\t" # 表示字元 '\' 和 't'  
len(not_tab_string)    # 等於 2  

利用三個雙引號來建立多行字串:

multi_line_string = """這是第一行  
這是第二行  
這是第三行"""  

例外處理(Exception Handling)

當程式出錯時,Python 會拋出一個「例外(exception)」。如果我們不對其進行處理,程式將會終止執行。要捕捉例外,可以使用 tryexcept 陳述式:

try:  
    print 0 / 0  
except ZeroDivisionError:  
    print "不能除以 0"  

儘管在其他程式語言中,例外常被視為不好的現象,但在 Python 中,適當地處理例外反而會讓你的程式碼更簡潔、更乾淨。

列表(Lists)

建立列表

列表是簡單的有序集合,也是 Python 中最基礎的資料結構(類似其他程式語言中的陣列,但列表具有一些額外的特性)。你可以這樣建立一個列表:

integer_list = [1, 2, 3]  
heterogeneous_list = ["string", 0.1, True]  
list_of_lists = [ integer_list, heterogeneous_list, [] ]  
list_length = len(integer_list)   # 等於 3  
list_sum = sum(integer_list)      # 等於 6  

存取列表中的值

你可以透過方括號來索引列表中的值:

x = range(10)       # 列表 `x = [0, 1, ..., 9]`  
zero = x[0]         # 等於 0,列表的索引從 0 開始  
one = x[1]          # 等於 1  
nine = x[-1]        # 等於 9,列表中的最後一個元素  
eight = x[-2]       # 等於 8,列表中的倒數第二個元素  
x[0] = -1           # 目前的列表 `x = [-1, 1, 2, 3, ..., 9]`  

截取列表(Slicing)

可以用方括號來截取列表:

first_three = x[:3]                  # [-1, 1, 2]  
three_to_end = x[3:]                 # [3, 4, ..., 9]  
one_to_four = x[1:5]                 # [1, 2, 3, 4]  
last_three = x[-3:]                  # [7, 8, 9]  
without_first_and_last = x[1:-1]     # [1, 2, ..., 8]  
copy_of_x = x[:]                     # [-1, 1, 2, ..., 9]  

你可以用 in 關鍵字來檢查某個元素是否存在於列表中:

1 in [1, 2, 3]        # True  
0 in [1, 2, 3]        # False  

這種元素查找方式的效率很低。只有在列表非常小,或者你不在意查找時間的情況下,才建議使用。

拼接列表

在 Python 中,要拼接兩個列表非常容易:

x = [1, 2, 3]  
x.extend([4, 5, 6])   # 目前的 `x = [1,2,3,4,5,6]`  

如果你不想修改原列表 x,你可以使用「加號」運算子來建立一個新的列表:

x = [1, 2, 3]  
y = x + [4, 5, 6]     # 目前的 `y = [1, 2, 3, 4, 5, 6]`;`x` 沒有變化  

常常會用這種方式一次在列表中加入一個元素:

x = [1, 2, 3]  
x.append(0)           # 目前的 `x = [1, 2, 3, 0]`  
y = x[-1]             # 等於 0  
z = len(x)            # 等於 4  

列表解構(Unpacking)

如果你知道列表中有多少個元素,那麼很容易就能解構這個列表:

x, y = [1, 2]         # 目前的 `x = 1`, `y = 2`  

如果等式兩邊的元素數目不一致,你將會得到一個「值錯誤(ValueError)」。因此,我們更常用底線 _ 來忽略列表中不需要的部分:

_, y = [1, 2]         # 目前的 `y == 2`,而第一個元素被忽略  

元組(Tuples)

列表和元組非常相似,唯一的區別是元組中的元素不能被修改(immutable)。

建立元組

你可以使用圓括號或不加任何括號來建立元組:

my_tuple = (1, 2)  
other_tuple = 3, 4  
my_list[1] = 3        # 目前的 `my_list` 為 `[1, 3]`  
try:  
    my_tuple[1] = 3  
except TypeError:  
    print "無法修改元組"  

利用元組能夠很方便地從函式中獲取多個回傳值:

def sum_and_product(x, y):  
    return (x + y),(x * y)  
sp = sum_and_product(2, 3)    # 等於 `(5, 6)`  
s, p = sum_and_product(5, 10) # `s = 15`, `p = 50`  

元組(和列表)都支援同時指定多個元素:

x, y = 1, 2       # 目前的 `x = 1`, `y = 2`  
x, y = y, x       # 這是 Python 中交換兩個變數值的方法;目前的 `x = 2`, `y = 1`  

字典(Dictionaries)

建立字典

Python 中另一種基礎資料結構是字典(dictionary),它能讓你透過鍵(key)快速取得對應的值(value):

empty_dict = {}                       # 非常 Python 風格的空字典定義  
empty_dict2 = dict()                  # 較不 Python 風格的空字典定義  
grades = { "Joel" : 80, "Tim" : 95 }  # 字典儲存  

查找字典元素

你可以用方括號加上鍵(key)來查找對應的值:

joels_grade = grades["Joel"]          # 等於 80  

如果要查找的鍵不在字典中,將會返回一個「鍵值錯誤(KeyError)」:

try:  
    kates_grade = grades["Kate"]  
except KeyError:  
    print "找不到 Kate 的成績!"  

你可以透過 in 關鍵字來檢查鍵是否存在於字典中:

joel_has_grade = "Joel" in grades     # True  
kate_has_grade = "Kate" in grades     # False  

字典有一個方法可以返回預設值,當要查找的鍵不在字典中時,它會返回設定的預設值(而不是拋出例外):

joels_grade = grades.get("Joel", 0)   # 等於 80  
kates_grade = grades.get("Kate", 0)   # 等於 0  
no_ones_grade = grades.get("No One")  # 回傳預設值 `None`  

修改字典

你可以用方括號來建立、修改字典中的鍵值對(key-value pair):

grades["Tim"] = 99                    # 取代舊的值  
grades["Kate"] = 100                  # 增加一個鍵值對  
num_students = len(grades)            # 等於 3  

我們將會經常像這樣使用字典來表達資料的結構:

tweet = {  
    "user" : "joelgrus",  
    "text" : "Data Science is Awesome",  
    "retweet_count" : 100,  
    "hashtags" : ["#data", "#science", "#datascience", "#awesome", "#yolo"]  
}  

除了查找特定的鍵,我們還可以像這樣操作所有的鍵、值或鍵值對:

tweet_keys = tweet.keys()             # 得到一個鍵(key)的列表  
tweet_values = tweet.values()         # 得到值的列表  
tweet_items = tweet.items()           # 得到 `(鍵, 值)` 元組  
"user" in tweet_keys                  # 回傳 True,但這是使用效率較低的列表 `in` 查找  
"user" in tweet                       # 更 Python 風格的用法,使用效率較高的字典 `in` 查找  
"joelgrus" in tweet_values            # True  

字典中的鍵是唯一的,而且列表不能作為字典的鍵。如果你需要一個多部分的鍵,你可以使用元組,或者透過某種方式將鍵轉換成字串。

內建字典(defaultdict)

如果你正試圖統計一個文件(document)中每個詞彙出現的頻率,一個顯而易見的做法是建立一個字典,以詞彙作為鍵,頻率作為對應的值。然後遍歷文件,遇到已經出現過的詞彙就讓字典中對應的鍵值遞增 1,遇到未出現過的詞彙就在字典中加入一個新的鍵值對:

word_counts = {}  
for word in document:  
    if word in word_counts:  
        word_counts[word] += 1  
    else:  
        word_counts[word] = 1  

當然,你也可以像這樣用「先嘗試再處理例外」的方式,來處理一個缺失的鍵:

word_counts = {}  
for word in document:  
    try:  
        word_counts[word] += 1  
    except KeyError:  
        word_counts[word] = 1  

第三個方法是使用 get(),這個方法對於處理缺失的鍵表現得非常出色:

word_counts = {}  
for word in document:  
    previous_count = word_counts.get(word, 0)  
    word_counts[word] = previous_count + 1  

內建字典 defaultdict 就跟普通字典一樣,唯一的區別是,當你試圖在字典中查找一個不存在的鍵時,defaultdict 會利用你提供的函式(或型別)自動建立一個鍵值對。為了使用 defaultdict,你需要從 collections 模組中匯入它:

from collections import defaultdict  
word_counts = defaultdict(int)        # `int()` 會產生 0  
for word in document:  
    word_counts[word] += 1  

在列表、普通字典甚至自定義的函式中,defaultdict 都很好用:

dd_list = defaultdict(list)           # `list()` 會產生一個空列表  
dd_list[2].append(1)                  # 目前的 `dd_list` 為 `{2: [1]}`  
dd_dict = defaultdict(dict)           # `dict()` 會產生一個空字典  
dd_dict["Joel"]["City"] = "Seattle"   # 目前的 `dd_dict` 內容為 `{ "Joel" : { "City" : "Seattle"}}`  
dd_pair = defaultdict(lambda: [0, 0]) # 建立了一個鍵對應值為列表的字典  
dd_pair[2][1] = 1                     # 目前的 `dd_pair` 內容為 `{2: [0,1]}`  

這種方法非常實用,以後當我們要從字典中獲取某些鍵值結果時,就無需再檢查鍵是否存在了。

計數器(Counter)

計數器 Counter 可以直接將一組值轉換成類似字典的物件,其中鍵是這組值中的某個元素,對應的值則是該元素出現的次數。這在建立直方圖(histogram)時會經常用到:

from collections import Counter  
c = Counter([0, 1, 2, 0]) # `c` (大約) 為 `{ 0 : 2, 1 : 1, 2 : 1 }`  

這樣我們就有了一個很方便地統計詞頻的方法:

word_counts = Counter(document)  

計數器還有一個很常用的方法 most_common(),可以直接取得最高頻率的幾個詞彙和對應的頻率:

# 輸出前 10 個最高頻的詞彙以及它們的計數值  
for word, count in word_counts.most_common(10):  
    print word, count  

集合(Sets)

Python 中另一種資料結構是集合(Set),集合是一組不重複元素的收集。
你可以這樣建立一個集合並向其中加入元素:

s = set()  
s.add(1)          # `s` 為 `{ 1 }`  
s.add(2)          # `s` 為 `{ 1, 2 }`  
s.add(2)          # `s` 為 `{ 1, 2 }`  
x = len(s)        # 等於 2  
y = 2 in s        # 等於 True  
z = 3 in s        # 等於 False  

使用集合的兩大理由是:

第一,集合中的 in 運算非常高效。當一個資料集中的元素數量非常龐大時,以集合的形式來查找元素顯然比列表更合適:

stopwords_list = ["a","an","at"] + hundreds_of_other_words + ["yet", "you"]  
"zip" in stopwords_list               # 失敗,需要逐一檢查每個元素  
stopwords_set = set(stopwords_list)  
"zip" in stopwords_set                # 查找成功,而且速度很快  

第二,用集合來獲取一組資料中不重複的元素非常方便:

item_list = [1, 2, 3, 1, 2, 3]  
num_items = len(item_list)            # 6  
item_set = set(item_list)             # {1, 2, 3}  
num_distinct_items = len(item_set)    # 3  
distinct_item_list = list(item_set)   # [1, 2, 3]  

不過實際上,集合的使用頻率還是沒有字典和列表高。

條件陳述式

在絕大多數程式語言中,你都可以像這樣用 if 來表示條件分支:

if 1 > 2:  
    message = "if only 1 were greater than two…"  
elif 1 > 3:  
    message = "elif stands for 'else if'"  
else:  
    message = "when all else fails use else (if you want to)"  

你也可以像這樣將條件分支陳述式寫在同一行中,但這種寫法很少見:

parity = "even" if x % 2 == 0 else "odd"  

迴圈陳述式

while 迴圈

Python 中的 while 迴圈:

x = 0  
while x < 10:  
    print x, "is less than 10"  
    x += 1  

for 迴圈

更常用的是使用 for-in 迴圈:

for x in range(10):  
    print x, "is less than 10"  

更複雜的邏輯表達式可以使用 continuebreak 陳述式:

for x in range(10):  
    if x == 3:  
        continue          # 直接進入下一輪迴圈  
    if x == 5:  
        break             # 完全退出迴圈  
    print x  

結果將會輸出 0、1、2 和 4。

布林值與真值(Truthiness)

Python 中的布林變數 Booleans 用法和其他程式語言差不多,唯一的區別是首字母必須大寫:

one_is_less_than_two = 1 < 2      # 為 `True`  
true_equals_false = True == False # 為 `False`  

Python 使用 None 來表示一個值不存在,類似於其他程式語言中的 null

x = None  
print x == None        # 輸出 `True`,但這種寫法不夠優雅  
print x is None        # 輸出 `True`,這種寫法更優雅  

Python 允許你用其他值來代替布林值,以下這些都等價於 False

類似地,也有許多等價於 True 的值,這讓你非常方便地判斷空列表、空字串以及空字典等等。

當然,如果你無法預期結果,可能會在使用過程中出錯:

s = some_function_that_returns_a_string()  
if s:  
    first_char = s[0]  
else:  
    first_char = ""  

一個更簡潔的做法,其效果等同於上面的做法:

first_char = s and s[0]  

如果第一個值為真(True),將會回傳第二個值,否則回傳第一個值。

類似地,如果 x 可能是一個數字也可能為空,那麼這樣可以得到一個肯定為數字的 x

safe_x = x or 0  

Python 中還有 all() 函式,當每個元素都為 True 時,它會回傳 True。而 any() 函式,只要有一個元素為 True 就會回傳 True。例如,對於一個所有元素都為「真」的列表,all() 函式將會回傳 True,否則將會回傳 False

all([True, 1, { 3 }])       # True  
all([True, 1, {}])          # `False`,`{}` 等價於 `False`  
any([True, 1, {}])          # True  
all([])                     # `True`,不存在一個等價於 `False` 的元素  
any([])                     # `False`,不存在一個等價於 `True` 的元素  

延伸閱讀:
資料科學中常用的 Python 語法(進階篇)