List (清單/串列)、Array (陣列)、Tuple (元組)、Dictionary (字典) 在 C# 都有,接觸 Python 之初,它們卻常讓我迷惑,尤其結合 For Comprehension (推導式) 後,對我成了一團能約略猜出用意但無法徒手寫出來的半模糊語法。

後來想想,關鍵應出在 Python 追求簡潔,常以符號、保留字(關鍵字)取代函式,有時符號還可以省略。而同樣動作,C# 多半是靠函式、屬性或建構式完成,容易望文生意(但後來語法糖也愈來愈多)。理解 Python 程式時,靠字面記憶及猜意思的慣性反成阻礙。加上同一結構有好幾種等效寫法,前幾次遇到以為是新玩意兒,多看幾次才意識「啊,原來這就等於 XXX」。

舉幾個例子:

  • 在 Python 你可以用 y, x = x, y 交換變數,乍看很謎,但理解成「Tuple (x, y) 解構(Destructuring) 存回原變數,再省略 Tuple 括號」便會豁然開朗。
  • 要怎麼比對 Dictionary 是否存在某 Key 值,我起初想試著找 ContainsKey()、Has() 之類的 API,後來學到 Python 風格會用 "some-key" in dict
  • 要取出字串第 3 到第 5 個字元,應該是靠 Substring() 或 Mid() 之類的函式吧?不,寫 mystr[3:6] 就成了。
  • Python 物件導向思維不如 C#、JavaScript 強烈。舉個例子:想取得字串長度,我直覺會猜字串物件有個 Length 之類的屬性,但 Python 是用 len(str_var)

有時覺得,若我沒寫過 C#、JavaScript,或許初學 Python 還會更順利些。(反過來想,若寫慣 Python 的人要跨足其他語言,也會被慣性所絆吧)
但後期學到較複雜觀念,想成「這相當於 C#/JavaScript 的 OOO 」便能快理解,這時有 C#/JavaScript 經驗再變成加分條件。

走過這段,改變心態接受用符號、關鍵字、語法取代建構物件、呼叫方法的慣性思維,C# 老人學 Python 倒也能漸入佳境。

這篇筆記先整理 List 與 For Comprehension。

List 清單/串列

Python 的 List 可想像成 C# 的 List<object>,用來儲放任意型別,但多了一些方便簡潔的用法。常用操作如下:

  • 算長度用 len(list_var)
  • 負數索引可從後方取元素 list_var[-2] (倒數第 2 個)
  • 檢查元素是否存在用 'find' in [1, 2, 3];另一種做法是 list_var.index('find'),但要注意,找不到時會拋 ValueError
  • 計算出現次數 list_var.count('target')
  • 新增一個 list_var.append('new')、新增多個 list_var.extend([4,5])
  • 在特定位置插入新元素 list_var.insert(1, 'elem-at-2nd-pos')
  • 移除元素 list_var.pop()(最後一個)、list_var.pop(2)(第三個)、list_var.remove('target')(移除比對相符的第一筆)
  • 清除 list_var.clear()
  • 排序 list_var.sort()(本身順序會被改變)、list_var.sort(reverse = True)list_var.reverse()(反向排序)、sorted(list_var)(傳回排序過的新串列)
    自訂比較邏輯 sorted(list_var, key = len) 用 len() 比較
  • 串接字串 ', '.join(list_var)
  • 有些函式凡是實作 Iterable 特性的都可以用,例如:sorted('hello') 會得到 ['e', 'h', 'l', 'l', 'o']
    參考:sorted()、sum()、any() (任一為 True)、all() (需全部為 True)
  • 複製
    a = [1,2,3]
    b = a     
    a is b       # 指向同物件,得到 True
    id(a), id(b) # 得到 (2480536168704, 2480536168704) 相同位址
    b[-1] = 0    # 會改變 a 的內容
    b = a.copy() # 淺複製,深複製可用 .deepcopy()
    a == b       # 內容相同,True
    a is b       # 不同物件,False
    
  • List 加法可串接,乘法會重複元素
    a = [1, 2]
    b = [3, 4]
    print(a + b) # [1, 2, 3, 4]
    print(a * 2) # [1, 2, 1, 2]  
    
  • 切片(Slice):適用所有可迭代(Iterable)物件,如字串、List、Tuple、Dictionary、Set...。切片可選取指定範圍元素,進行讀取、置換,非常簡潔好用。
    a = [1, 2, 3, 4]
    print(a[1:3]) # [2, 3]
    a[-1] = 'END' # [1, 2, 3, 'END']
    a[1:3] = ['a', 'b', 'c'] # [1, 'a', 'b', 'c', 'END']
    print(a)
    
  • 開箱(Unpacking):將 List、Tuple 中的特定元素轉成變數,類似 JavaScript 的解構(Destructuring)
    # 基本用法(變數個數必須與元素數量一致)
    x, y = [1, 2] # List 開箱,x=1, y=2
    a, b = (3, 4) # Tuple 開箱,a=3, b=4
    # 用星號通吃剩餘元素
    x, y, *others = [1, 2, 3, 4, 5] # x=1, y=2, others=[3, 4, 5]
    x, y, *_ = [1, 2, 3, 4, 5] # 不需要的部分用底線
    # 取頭尾
    head, *_, tail = [1, 2, 3, 4] # head=1, tail=4
    first, second, *rest, last = [1, 2, 3, 4, 5] # first=1, second=2, rest=[3, 4], last=5
    # 套用 List, Tuple
    _, [_, a], (_, b) = [1, [2, 3], (4, 5)] # a=3, b=5
    
    
  • 開箱運算子(Unpacking Operator):展開 Iterable 物件,相當於 JavaScript 的 ... 語法
    a = [1, 2]
    b = [3, 4]
    n = [*a, ' ', *b] # [1, 2, ' ', 3, 4]
    

For Comprehension 串列推導式

這是我心中最具 Python 風格的快捷語法,與 C# LINQ 語法異曲同工,但簡潔度更勝一籌。

一開始看 Github Copilot 示範用 .. for ... in .. if .. 處理各種資料篩選組裝有些摸不著頭緒,熟悉後我很快就愛上它惹。

直接看幾則應用,應該能很快上手。

  • 加工轉換產生新清單
    a = [1, 2, 3]
    n = [f'No.{n}' for n in a] # ['No.1', 'No.2', 'No.3']
    # C# a.Select(o => $"No.{o}").ToList();
    
  • 加上篩選條件
    a = [1, 2, 3, 4, 5]
    n = [f'No.{n}' for n in a if n % 2 == 1] # ['No.1', 'No.3', 'No.5']
    # C# a.Where(o => o % 2 == 1).Select(o => $"No.{o}").ToList();
    
  • 也可以用來產生 Dictionary,語法為 for 搭配 key: value 產生鍵值資料對:
    d = { n: f"item{n}" for n in range(10) }
    # {0: 'item0', 1: 'item1', 2: 'item2', 3: 'item3', ... }
    
  • For Comprehension 本質是一個產生器(Generator)物件,具有惰性求值(Lazy Evaluation)的特性,當寫成 [n for n in range(10000)] 會展開生成 List,若寫成 (n for n in range(10000)) 是一個產生器,在某些應用情境可一次只吐一個數字,不用一次產生 10000 個元素,減少記憶體耗用增進效能。熟悉 C# 的同學可想成 IEnumerable<T> 靠 yield return 省時省 CPU 省 RAM,應能馬上理解。
    from sys import getsizeof
    list = [n for n in range(10000)]
    print(getsizeof(list)) # 耗用 85176 bytes
    print(sum(list)) # 49995000
    gen = (n for n in range(10000))
    print(getsizeof(gen)) # 耗用 112 bytes
    print(sum(gen)) # 49995000
    

This blog post explores the initial confusion and eventual clarity experienced when transitioning from C# to Python, especially regarding data structures and list comprehensions. Key takeaways include understanding Python’s use of symbols and keywords over functions, and how Python’s syntax can simplify operations like swapping variables, checking for dictionary keys, slicing strings, and more. The post also covers Python’s list operations and comprehensions, highlighting their similarities to C# LINQ but with more concise syntax.


Comments

# by JD

感覺新一點的C# syntax也是差不多的 交換變數可以用ValueTuple寫法 SubString 可以用Range

# by ChrisTorng

奇怪,我在 Edge 裡看程式碼,每一個程式碼區塊第一行的 a 都沒看到,後來才發現是有水平捲軸,預設都捲往右,導致第一行第一個字元不見,第二行之後都縮排一個字,但我檢查前面也沒字。 如前面所說,新版 C# 也都陸續導入類似語法。在 csproj 裡加上 <LangVersion>Latest</LangVersion> 就會出現。連同 .NET Framework 也可以用很多新語法,參考 https://stu.dev/csharp8-doing-unsupported-things/ 或 https://stackoverflow.com/questions/56651472/does-c-sharp-8-support-the-net-framework。然後 Analyzer 也會針對新語法提出建議,因此很多也不用自己學,它會提醒,而且都可以選擇(全部) 自動套用新語法。有可能是我將程式碼分析功能全開的關係: <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <EnableNETAnalyzers>true</EnableNETAnalyzers> <AnalysisLevel>latest</AnalysisLevel> <AnalysisMode>All</AnalysisMode>

# by Johnny

Python 最容易搞倒老 dev 的在於它的所謂 duck type 幸好一旦搞懂就好辦了

Post a comment