Scala 3 — 書籍

Scala 給 Python 開發人員

語言

本節提供 Python 和 Scala 程式語言的比較。它適用於了解 Python 並想學習 Scala 的程式設計師,特別是透過查看 Python 語言功能與 Scala 比較的範例來學習。

簡介

在進入範例之前,本第一節提供後續各節相對簡短的簡介和摘要。這兩種語言首先在高層級比較,然後在日常程式設計層級比較。

高層級相似性

在高層級,Scala 與 Python 共享這些相似性

  • 兩者都是高級程式語言,您不必擔心指標和手動記憶體管理等低階概念
  • 兩者都有相對簡單、簡潔的語法
  • 兩者都支援 函式程式風格
  • 兩者都是物件導向程式 (OOP) 語言
  • 兩者都有理解:Python 有清單理解,而 Scala 有 for 理解
  • 兩種語言都支援 lambda 和 高階函式
  • 兩者都可以搭配 Apache Spark 用於大數據處理
  • 兩者都有豐富的絕佳函式庫

高階差異

同樣在高階上,Python 和 Scala 之間的差異

  • Python 是動態型別,而 Scala 是靜態型別
    • 儘管是動態型別,但 Python 支援具有型別提示的「漸進式型別」,這些提示會由靜態型別檢查器(例如 mypy)檢查
    • 儘管是靜態型別,但 Scala 的功能(例如型別推論)讓它感覺像動態語言
  • Python 是直譯式,而 Scala 程式碼編譯成 .class 檔案,並在 Java 虛擬機器 (JVM) 上執行
  • 除了在 JVM 上執行之外,Scala.js 專案讓您可以將 Scala 用作 JavaScript 替換
  • Scala Native 專案讓您可以撰寫「系統」層級程式碼,並編譯成原生可執行檔
  • Scala 中的一切都是表達式if 陳述式、for 迴圈、match 表達式,甚至 try/catch 表達式等結構都有傳回值
  • Scala 慣用語預設偏好不變性:建議您使用不變變數和不變集合
  • Scala 對並行和平行程式設計有極佳的支援

程式設計層級的相似性

本節探討在日常撰寫程式碼時,您會在 Python 和 Scala 之間看到的相似性

  • Scala 的類型推論通常讓它感覺像動態類型語言
  • 這兩種語言都不使用分號來結束表達式
  • 這兩種語言都支援使用顯著縮排,而不是大括號和括號
  • 定義方法的語法很類似
  • 這兩種語言都有清單、字典 (映射)、集合和元組
  • 這兩種語言都有用於映射和篩選的理解
  • 這兩種語言都有極佳的 IDE 支援
  • 使用 Scala 3 的頂層定義,您可以將方法、欄位和其他定義放在任何地方
    • 其中一個不同點是,Python 可以在甚至未宣告單一方法的情況下運作,而 Scala 3 無法在頂層執行所有動作;例如,main 方法 (@main def) 是啟動 Scala 應用程式所必需的

程式設計層級的差異

在程式設計層級上,這些也是您在撰寫程式碼時每天會看到的差異

  • 使用 Scala 程式設計的感覺非常一致
    • valvar 欄位會一致地用於定義欄位和參數
    • 清單、映射、集合和元組的建立和存取方式都類似;例如,括號用於建立所有類型—List(1,2,3)Set(1,2,3)Map(1->"one")—就像建立任何其他 Scala 類別一樣
    • 集合類別 通常具有大多數相同的階層函數
    • 模式比對在整個語言中一致使用
    • 用於定義傳遞到方法中的函數的語法與用於定義匿名函數的語法相同
  • Scala 變數和參數使用 val(不可變)或 var(可變)關鍵字定義
  • Scala 慣用語法偏好不可變資料結構
  • 註解:Python 使用 # 作為註解;Scala 使用 C、C++ 和 Java 風格:///*...*//**...*/
  • 命名慣例:Python 標準是使用底線,例如 my_list;Scala 使用 myList
  • Scala 是靜態類型,因此您為方法參數、方法傳回值和其他地方宣告類型
  • 模式比對和 match 表達式在 Scala 中廣泛使用(並且會改變您編寫程式碼的方式)
  • Scala 大量使用特質;Python 中較少使用介面和抽象類別
  • Scala 的 脈絡抽象項推論提供了一系列不同的功能
    • 擴充方法 讓您可以使用明確的語法輕鬆地將新功能新增到類別
    • 多重相等性 讓您在編譯時將相等性比較限制為僅限於有意義的比較
  • Scala 具有最先進的開放原始碼函數式程式設計函式庫(請參閱 “Awesome Scala” 清單)
  • 由於物件、依名稱參數、中綴符號、可選括號、擴充方法、高階函數等功能,您可以建立自己的「控制結構」和 DSL
  • Scala 程式碼可以在 JVM 中執行,甚至可以編譯成原生映像(使用 Scala NativeGraalVM)以獲得高性能
  • 許多其他好處:伴隨類別和物件、巨集、數字文字、多個參數清單、交集類型、類型層級程式設計等

將功能與範例進行比較

有了這個簡介,以下各節將並排比較 Python 和 Scala 程式設計語言的功能。

註解

Python 使用 # 作為註解,而 Scala 註解語法與 C、C++ 和 Java 等語言相同

# 註解
// 註解
/* ... */
/** ... */

變數指派

這些範例示範如何在 Python 和 Scala 中建立變數。

建立整數和字串變數

x = 1
x = "Hi"
y = """foo
       bar
       baz"""
val x = 1
val x = "Hi"
val y = """foo
           bar
           baz"""

清單

x = [1,2,3]
val x = List(1,2,3)

字典/Map

x = {
  "Toy Story": 8.3,
  "Forrest Gump": 8.8,
  "Cloud Atlas": 7.4
}
val x = Map(
  "Toy Story" -> 8.3,
  "Forrest Gump" -> 8.8,
  "Cloud Atlas" -> 7.4
)

集合

x = {1,2,3}
val x = Set(1,2,3)

Tuple

x = (11, "Eleven")
val x = (11, "Eleven")

如果 Scala 欄位將會是可變的,請使用 var 而不是 val 來定義變數

var x = 1
x += 1

不過,Scala 的經驗法則是在變數特別需要變動時才使用 val

FP 風格記錄

Scala case 類似於 Python 凍結資料類別。

建構函式定義

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Person
  name: str
  age: int
case class Person(name: String, age: Int)

建立並使用一個實例

p = Person("Alice", 42)
p.name   # Alice
p2 = replace(p, age=43)
val p = Person("Alice", 42)
p.name   // Alice
val p2 = p.copy(age = 43)

OOP 風格類別和方法

此區段提供與 OOP 風格類別和方法相關功能的比較。

OOP 風格類別,主要建構函式

class Person(object)
  def __init__(self, name)
    self.name = name

  def speak(self)
    print(f'Hello, my name is {self.name}')
class Person (var name: String)
  def speak() = println(s"Hello, my name is $name")

建立並使用一個實例

p = Person("John")
p.name   # John
p.name = 'Fred'
p.name   # Fred
p.speak()
val p = Person("John")
p.name   // John
p.name = "Fred"
p.name   // Fred
p.speak()

單行方法

def add(a, b): return a + b
def add(a: Int, b: Int): Int = a + b

多行方法

def walkThenRun()
  print('walk')
  print('run')
def walkThenRun() =
  println("walk")
  println("run")

介面、特質和繼承

如果您熟悉 Java 8 和更新版本,Scala 特質類似於那些 Java 介面。Scala 中始終使用特質,而 Python 介面(協定)和抽象類別的使用頻率低得多。因此,此範例不會嘗試比較這兩者,而是說明如何使用 Scala 特質來建構模擬數學問題的小型解決方案

trait Adder:
  def add(a: Int, b: Int) = a + b

trait Multiplier:
  def multiply(a: Int, b: Int) = a * b

// create a class from the traits
class SimpleMath extends Adder, Multiplier
val sm = new SimpleMath
sm.add(1,1)        // 2
sm.multiply(2,2)   // 4

還有許多其他方法可以使用特質與類別和物件,但這能讓您稍微了解如何使用它們將概念整理成行為的邏輯群組,然後視需要合併它們以建立完整的解決方案。

控制結構

本節比較 Python 和 Scala 中的控制結構。兩種語言都有 if/elsewhilefor 迴圈和 try 等建構。Scala 也有 match 表達式。

if 敘述,一行

if x == 1: print(x)
if x == 1 then println(x)

if 敘述,多行

if x == 1
  print("x is 1, as you can see:")
  print(x)
if x == 1 then
  println("x is 1, as you can see:")
  println(x)

if、else if、else

if x < 0
  print("negative")
elif x == 0
  print("零")
否則
  print("正數")
如果 x < 0 然後
  println("負數")
否則如果 x == 0 然後
  println("零")
否則
  println("正數")

if 傳回值

min_val = a 如果 a < b 否則 b
val minValue = 如果 a < b 然後 a 否則 b

if 作為方法的主體

def min(a, b)
  如果 a < b 然後傳回 a 否則傳回 b
def min(a: Int, b: Int): Int =
  如果 a < b 然後 a 否則 b

while 迴圈

i = 1
while i < 3
  print(i)
  i += 1
var i = 1
while i < 3 do
  println(i)
  i += 1

for 迴圈與範圍

for i in range(0,3)
  print(i)
// 偏好
for i <- 0 until 3 do println(i)

// 也可以用
for (i <- 0 until 3) println(i)

// 多行語法
for
  i <- 0 until 3
do
  println(i)

for 迴圈與清單

for i in ints: print(i)

for i in ints
  print(i)
for i <- ints do println(i)

for 迴圈,多行

for i in ints
  x = i * 2
  print(f"i = {i}, x = {x}")
for
  i <- ints
do
  val x = i * 2
  println(s"i = $i, x = $x")

多個「範圍」產生器

for i in range(1,3)
  for j in range(4,6)
    for k in range(1,10,3)
      print(f"i = {i}, j = {j}, k = {k}")
for
  i <- 1 到 2
  j <- 4 到 5
  k <- 1 到 10 步長 3
do
  println(s"i = $i, j = $j, k = $k")

帶有防護措施的產生器 (if 表達式)

for i in range(1,11)
  if i % 2 == 0
    if i < 5
      print(i)
for
  i <- 1 到 10
  if i % 2 == 0
  if i < 5
do
  println(i)

每行多個 if 條件

for i in range(1,11)
  if i % 2 == 0 and i < 5
    print(i)
for
  i <- 1 到 10
  if i % 2 == 0 && i < 5
do
  println(i)

理解

xs = [i * 10 for i in range(1, 4)]
# xs: [10,20,30]
val xs = for i <- 1 到 3 yield i * 10
// xs: Vector(10, 20, 30)

match 表達式

# 從 3.10 開始,Python 支援結構模式配對
# 您也可以使用字典來獲得基本的「switch」功能
match month
  case 1
    monthAsString = "January"
  case 2
    monthAsString = "February"
  case _
    monthAsString = "Other"
val monthAsString = month match
  case 1 => "January"
  case 2 => "February"
  _ => "Other"

switch/match

# 僅限 Python 3.10
match i
  case 1 | 3 | 5 | 7 | 9
    numAsString = "odd"
  case 2 | 4 | 6 | 8 | 10
    numAsString = "even"
  case _
    numAsString = "太大"
val numAsString = i match
  case 1 | 3 | 5 | 7 | 9 => "奇數"
  case 2 | 4 | 6 | 8 | 10 => "偶數"
  case _ => "太大"

try、catch、finally

try
  print(a)
except NameError
  print("NameError")
except
  print("其他")
finally
  print("Finally")
try
  writeTextToFile(text)
catch
  case ioe: IOException =>
    println(ioe.getMessage)
  case fnf: FileNotFoundException =>
    println(fnf.getMessage)
finally
  println("Finally")

比對運算式和模式比對是 Scala 程式設計體驗中很重要的一部分,但這裡只顯示少數幾個 match 運算式功能。請參閱 控制結構 頁面,以取得更多範例。

集合類別

本節比較 Python 和 Scala 中可用的 集合類別,包括清單、字典/對應、集合和元組。

清單

Python 有清單,而 Scala 則有許多不同的特殊可變和不可變序列類別,視您的需求而定。由於 Python 清單是可變的,因此最直接可與 Scala 的 ArrayBuffer 相比。

Python 清單與 Scala 序列

a = [1,2,3]
// 使用不同的序列類別
// 視需要而定
val a = List(1,2,3)
val a = Vector(1,2,3)
val a = ArrayBuffer(1,2,3)

存取清單元素

a[0]
a[1]
a(0)
a(1)
// 就像其他所有方法呼叫

更新清單元素

a[0] = 10
a[1] = 20
// ArrayBuffer 可變
a(0) = 10
a(1) = 20

合併兩個串列

c = a + b
val c = a ++ b

遍歷串列

for i in ints: print(i)

for i in ints
  print(i)
// 偏好
for i <- ints do println(i)

// 也可以用
for (i <- ints) println(i)

Scala 的主要序列類別為 ListVectorArrayBuffer。當您需要不可變序列時,ListVector 是主要類別,當您需要可變序列時,ArrayBuffer 是主要類別。(Scala 中的「緩衝區」是可以增長和縮小的序列。)

字典/Map

Python 字典類似於可變 Scala Map 類別。然而,預設的 Scala map 是不可變的,並具有多種轉換方法,讓您可以輕鬆建立新的 map。

字典/Map 建立

my_dict = {
  'a': 1,
  'b': 2,
  'c': 3
}
val myMap = Map(
  "a" -> 1,
  "b" -> 2,
  "c" -> 3
)

存取字典/map 元素

my_dict['a']   # 1
myMap("a")   // 1

使用 for 迴圈的字典/Map

for key, value in my_dict.items()
  print(key)
  print(value)
for (key,value) <- myMap do
  println(key)
  println(value)

Scala 有其他專門的 Map 類別,以滿足不同的需求。

集合

Python 集合類似於可變 Scala Set 類別。

集合建立

set = {"a", "b", "c"}
val set = Set(1,2,3)

重複元素

set = {1,2,1}
# set: {1,2}
val set = Set(1,2,1)
// set: Set(1,2)

Scala 有其他專門的 Set 類別,可滿足不同的需求。

元組

Python 和 Scala 元組也很相似。

元組建立

t = (11, 11.0, "Eleven")
val t = (11, 11.0, "Eleven")

存取元組元素

t[0]   # 11
t[1]   # 11.0
t(0)   // 11
t(1)   // 11.0

集合類別的方法

Python 和 Scala 有好幾個相同的常見函數方法可以使用

  • map
  • filter
  • reduce

如果您習慣在 Python 中使用 lambda 表達式來使用這些方法,您會看到 Scala 在其集合類別的方法中採用了類似的方法。為了展示此功能,這裡有兩個範例清單

numbers = [1,2,3]           // python
val numbers = List(1,2,3)   // scala

這些清單用於下表,顯示如何對其套用對應和篩選演算法。

使用內包對應

x = [i * 10 for i in numbers]
val x = for i <- numbers yield i * 10

使用內包篩選

evens = [i for i in numbers if i % 2 == 0]
val evens = numbers.filter(_ % 2 == 0)
// 或
val evens = for i <- numbers if i % 2 == 0 yield i

使用內包對應和篩選

x = [i * 10 for i in numbers if i % 2 == 0]
val x = numbers.filter(_ % 2 == 0).map(_ * 10)
// 或
val x = for i <- numbers if i % 2 == 0 yield i * 10

對應

x = map(lambda x: x * 10, numbers)
val x = numbers.map(_ * 10)

篩選

f = lambda x: x > 1
x = filter(f, numbers)
val x = numbers.filter(_ > 1)

Scala 集合類別方法

Scala 集合類別有超過 100 個函式方法可簡化您的程式碼。在 Python 中,有些函式會在 itertools 模組中提供。除了 mapfilterreduce 之外,下列列出 Scala 中其他常用的方法。在這些方法範例中

  • c 指集合
  • p 是謂詞
  • f 是函式、匿名函式或方法
  • n 指整數值

以下是可用的部分篩選方法

方法 說明
c1.diff(c2) 傳回 c1c2 中元素的差異。
c.distinct 傳回 c 中的唯一元素。
c.drop(n) 傳回集合中除了前 n 個元素之外的所有元素。
c.filter(p) 傳回集合中謂詞為 true 的所有元素。
c.head 傳回集合的第一個元素。(如果集合為空,則擲回 NoSuchElementException。)
c.tail 傳回集合中除了第一個元素之外的所有元素。(如果集合為空,則擲回 UnsupportedOperationException。)
c.take(n) 傳回集合 c 的前 n 個元素。

以下是幾個轉換器方法

方法 說明
c.flatten 將集合的集合(例如清單的清單)轉換為單一集合(單一清單)。
c.flatMap(f) 透過對集合 c 的所有元素套用 f(類似 map),然後壓平結果集合的元素,來傳回新的集合。
c.map(f) 透過對集合 c 的所有元素套用 f,來建立新的集合。
c.reduce(f) c 中的連續元素套用「簡化」函數 f,以產生單一值。
c.sortWith(f) 傳回 c 的版本,該版本已根據比較函數 f 排序。

一些常見的分組方法

方法 說明
c.groupBy(f) 根據 f,將集合分割成集合的 Map
c.partition(p) 根據謂詞 p,傳回兩個集合。
c.span(p) 傳回兩個集合的集合,第一個集合由 c.takeWhile(p) 建立,第二個集合由 c.dropWhile(p) 建立。
c.splitAt(n) 透過在元素 n 處分割集合 c,來傳回兩個集合的集合。

一些資訊和數學方法

方法 說明
c1.containsSlice(c2) 如果 c1 包含序列 c2,則傳回 true
c.count(p) 計算 cptrue 的元素數量。
c.distinct 傳回 c 中的唯一元素。
c.exists(p) 如果集合中任一元素的 ptrue,則傳回 true
c.find(p) 傳回第一個符合 p 的元素。元素以 Option[A] 傳回。
c.min 傳回集合中最小的元素。(可能會擲出 java.lang.UnsupportedOperationException。)
c.max 傳回集合中最大的元素。(可能會擲出 java.lang.UnsupportedOperationException。)
c slice(from, to) 傳回從元素 from 開始,到元素 to 結束的元素區間。
c.sum 傳回集合中所有元素的總和。(需要為集合中的元素定義 Ordering。)

以下是幾個範例,說明這些方法如何作用於清單

val a = List(10, 20, 30, 40, 10)      // List(10, 20, 30, 40, 10)
a.distinct                            // List(10, 20, 30, 40)
a.drop(2)                             // List(30, 40, 10)
a.dropRight(2)                        // List(10, 20, 30)
a.dropWhile(_ < 25)                   // List(30, 40, 10)
a.filter(_ < 25)                      // List(10, 20, 10)
a.filter(_ > 100)                     // List()
a.find(_ > 20)                        // Some(30)
a.head                                // 10
a.headOption                          // Some(10)
a.init                                // List(10, 20, 30, 40)
a.intersect(List(19,20,21))           // List(20)
a.last                                // 10
a.lastOption                          // Some(10)
a.slice(2,4)                          // List(30, 40)
a.tail                                // List(20, 30, 40, 10)
a.take(3)                             // List(10, 20, 30)
a.takeRight(2)                        // List(40, 10)
a.takeWhile(_ < 30)                   // List(10, 20)

這些方法顯示了 Scala 中的常見模式:物件上可用的函數式方法。這些方法都不會變異初始清單 a;相反地,它們會傳回註解後顯示的資料。

還有許多其他可用方法,但希望這些說明和範例能讓您了解預建集合方法中可用的功能。

列舉

本節比較 Python 和 Scala 3 中的列舉。

建立列舉

from enum import Enum, auto
class Color(Enum)
    RED = auto()
    GREEN = auto()
    BLUE = auto()
列舉 Color
  情況 Red、Green、Blue

值和比較

Color.RED == Color.BLUE  # False
Color.Red == Color.Blue  // false

參數化列舉

N/A
列舉 Color(val rgb: Int)
  情況 Red   延伸 Color(0xFF0000)
  情況 Green 延伸 Color(0x00FF00)
  情況 Blue  延伸 Color(0x0000FF)

使用者定義的列舉成員

N/A
列舉 Planet(
    mass: Double,
    radius: Double
  ):
  情況 Mercury 延伸
    Planet(3.303e+23, 2.4397e6)
  情況 Venus 延伸
    Planet(4.869e+24, 6.0518e6)
  情況 Earth 延伸
    Planet(5.976e+24, 6.37814e6)
  // 更多行星 ...

  // 欄位和方法
  private final val G = 6.67300E-11
  def surfaceGravity = G * mass /
    (radius * radius)
  def surfaceWeight(otherMass: Double)
    = otherMass * surfaceGravity

Scala 獨有的概念

Scala 中還有其他概念,目前在 Python 中沒有等效的功能。請追蹤以下連結以取得更多詳細資料

  • 情境抽象相關的大部分概念,例如 擴充方法類型類別、隱含值
  • Scala 允許多個參數清單,這啟用了部分應用函數等功能,以及建立您自己的 DSL 的能力
  • 建立您自己的控制結構和 DSL 的能力
  • 多重宇宙相等性:在編譯時控制相等比較有意義的能力
  • 中綴方法
  • 巨集

Scala 和虛擬環境

在 Scala 中,不需要明確設定等同於 Python 虛擬環境的東西。預設情況下,Scala 建置工具會管理專案相依性,讓使用者不必考慮手動安裝套件。例如,使用 sbt 建置工具,我們在 build.sbt 檔案中的 libraryDependencies 設定內指定相依性,然後執行

cd myapp
sbt compile

會自動解析該特定專案的所有相依性。下載的相依性位置在很大程度上是建置工具的實作細節,使用者不必直接與這些下載的相依性互動。例如,如果我們刪除整個 sbt 相依性快取,在下次編譯專案時,sbt 會自動再次解析並下載所有必要的相依性。

這與 Python 不同,在 Python 中,相依性預設安裝在系統範圍或使用者範圍的目錄中,因此若要取得每個專案的隔離環境,必須建立對應的虛擬環境。例如,使用 venv 模組,我們可以為特定專案建立一個虛擬環境,如下所示

cd myapp
python3 -m venv myapp-env
source myapp-env/bin/activate
pip install -r requirements.txt

這會將所有相依性安裝在專案的 myapp/myapp-env 目錄下,並變更 shell 環境變數 PATH,以從 myapp-env 查詢相依性。在 Scala 中,不需要任何這些手動程序。

本頁面的貢獻者