Scala 3 — 书籍

面向 Python 开发人员的 Scala

语言

本节提供了 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)

字典/映射

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)

元组

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"你好,我的名字是 $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 为 1,如下所示:")
  print(x)
if x == 1 then
  println("x 为 1,如下所示:")
  println(x)

if、else if、else

if x < 0
  print("负数")
elif x == 0
  print("零")
else
  print("正数")
if x < 0 then
  println("负数")
else if x == 0 then
  println("零")
else
  println("正数")

if 返回一个值

min_val = a if a < b else b
val minValue = if a < b then a else b

if 作为方法的主体

def min(a, b)
  return a if a < b else b
def min(a: Int, b: Int): Int =
  if a < b then a else 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 to 2
  j <- 4 to 5
  k <- 1 until 10 by 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 to 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 to 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 to 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 = "too big"
val numAsString = i match
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "too big"

try、catch、finally

try
  print(a)
except NameError
  print("NameError")
except
  print("Other")
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 中的“缓冲区”是可以增长和收缩的序列。)

字典/映射

Python 字典类似于可变 Scala Map 类。但是,默认的 Scala 映射是不可变的,并且具有许多转换方法,可以让你轻松创建新映射。

字典/映射创建

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

访问字典/映射元素

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

带有 for 循环的字典/映射

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) 通过将 f 应用到集合 c 的所有元素(如 map),然后展平结果集合的元素,返回一个新集合。
c.map(f) 通过将 f 应用到集合 c 的所有元素,创建新集合。
c.reduce(f) 将“化简”函数 f 应用到 c 中的连续元素,以产生单个值。
c.sortWith(f) 返回由比较函数 f 排序的 c 版本。

一些常见的分组方法

方法 描述
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()
enum Color
  case Red, Green, Blue

值和比较

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

参数化枚举

不适用
enum Color(val rgb: Int)
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

用户定义的枚举成员

不适用
enum Planet(
    mass: Double,
    radius: Double
  ):
  case Mercury extends
    Planet(3.303e+23, 2.4397e6)
  case Venus extends
    Planet(4.869e+24, 6.0518e6)
  case Earth extends
    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 中,无需进行任何此类手动过程。

本页面的贡献者