写给Python用户的Scala指南

你可能不大需要Python

Jun 30, 2021

见习魔法师

此处Python特指Python3,Scala也特指Scala3。如果您特别喜欢Python2,并且不想使用Python3的话,可以关闭这个网页了。

事实上看见Scala可以不需要害怕,在许多情况下,Scala比python可能更加好用,简洁。

Python 并不是那么理想

事实上,python是一个相当老的语言,在许多的设计上有着不合理的地方。在大多数时候,Scala的代码的行为你能更好预料到。

什么?你不服?那么我们来看这样一个片段:

x = 0
f()
print(x)

足够简单吧。那么程序的输出是什么呢?答案非常简单,是114514。是不是你也猜到了?

因为源程序是这样的:

def f():
    global x
    x = 114514

x = 0
f()
print(x)

这个例子比较狡猾,但是也足以说明因为作用域和声明变量和定义变量是同一形式,会产生很多违背直觉的行为。但是相同形式的Scala,你可以完全相信,x是不会改变的。

var x = 0
f()
print(x)

如果使用val,就可以更加自信,这个变量是不可能发生改变的。

python这么设计就导致了,一般情况下闭包无法改变它捕获的变量,虽然python能读捕获的变量了,但是没法写,你直接赋值就会认为是一个全新的变量,除非使用nonlocal,又多一个关键字。

def counter(n: int) -> Callable[[], int]:
    def c():
        nonlocal n
        n += 1
        return n
    return c

def counter(n: Int): () => Int =
  var x = n
  () =>
    x += 1
    x

而Scala因为默认参数是不可变的,所以要声明一个可变变量,这个是可以接受的,除此之外,Scala阅读的困惑比python少了很多,天生表达式就是函数体,块表达式的值就是最后一个表达式,免去了理解什么是return,nonlocal的疑惑,更适合新手(迫真)。

为了维护方便,当然还是建议即使是写python也要把类型注释补上虽然python也不会检查,但是你既然写上了,那Scala又比Python简短明确太多,还能防止写出如此的代码:

yj: str = 114514

Python目前推荐的实践

避免对不同类型的对象使用同一个变量名

很赞,但是既然这样为何不索性直接使用一门静态类型语言呢?有时候区别可能仅仅在于多写一个val

字符串不可变类型相关的优化

字符串不可变确实是好文明,可是:

Best Practice

nums = map(str, range(20))
print("".join(nums))

Awesome Practice

print(0.until(20).mkString)

需要注意的一点是,Python的迭代器是可变的。如果我再加入第二个语句:

nums = map(str, range(20))
print("".join(nums))
print("".join(nums))

您猜怎么着?第二个输出是空白的,即使这两行代码是完全相同的,而Scala无论你使用多少遍,这个行为都是相同的。

val nums = 0 until 20
print(nums.mkString)
print(nums.mkString)
print(nums.mkString)
print(nums.mkString)
print(nums.mkString)
print(nums.mkString)

Best Practice

foo = 'foo'
bar = 'bar'

foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # 最好

Awesome Practice

val foo = 'foo'
val bar = 'bar'

foobar = s"$foo$bar"

在Scala中,字符串的插值器本质上是StringContext的语法糖,你可以通过给StringContext添加扩展方法的办法来增加字符串插值器,而且返回的结果不一定是字符串,这个比Python的f-string要更进一步。

Data Class

在python 3.7 中加入了data class,只需要使用这个注解便可以创建一个带有构造器,__repr__,不可变的数据类型。 嘿,但是你既然这样了,为何不直接使用Scala的case class呢?

from dataclasses import dataclass
import math

@dataclass
class Vec3:
    x: float
    y: float
    z: float

    def __add__(self, that: 'Vec3') -> 'Vec3':
        return Vec3(self.x + that.x, self.y + that.y, self.z + that.z)

    def norm(self) -> float:
        return math.sqrt(self.x ** 2 + self.y ** 2 + self.z)

print(Vec3('python', 'is', 'good') + Vec3('', ' not', ''))

真不错,感觉比起写构造器确实节省了很多代码。 欸等等,类型注解为啥是字符串字面量,莫非Python也支持这种高级功能?当然不是,因为在类型注解时,无法引用自身作为类型,于是PEP 484提出了一个“优雅”的解决方案,便是允许字符串字面量作为类型出现在注解中。然而在示例中,为啥Vec3传入了str呢?可能是我喝多了吧,但是这个代码又不会报错,管他呢。

case class Vec3(x: Double, y: Double, z: Double):
  def +(that: Vec3): Vec3 = Vec3(x + that.x, y + that.y, z + that.z)
  def norm: Double = math.sqrt(x * x + y * y + z * z)
scala> Vec3("scala", "sucks", "")
1 |Vec3("scala", "sucks", "")
  |     ^^^^^^^
  |     Found:    ("scala" : String)
  |     Required: Double
1 |Vec3("scala", "sucks", "")
  |              ^^^^^^^
  |              Found:    ("sucks" : String)
  |              Required: Double
1 |Vec3("scala", "sucks", "")
  |                       ^^
  |                       Found:    ("" : String)
  |                       Required: Double

可以看出,Scala中重载操作符只用定义那个名字的方法就行了,这意味着你可以自定义操作符。Python你需要定义魔法方法,但是你能很快反应过来+-*/@ += -=都是什么魔法名字呢?很蓝的啦。

迁移到Scala

如果你是python的用户,并且你还喜欢写类型注解,那么迁移到Scala是相当简单的事情。

控制语句

在Scala中,if, match, while, for都是表达式,都具有返回值。

if a < b:
    v = a
elif a > 0:
    v = 0
else:
    v = b

v = a if a < b else (0 if a < 0 else b)

val v =
  if a < b then
    a
  else if a > 0 then
    0
  else b
可以看见,你只用把冒号变成then,elif变成else if就行了,其他都是一样的。你还能用上你怀念的大括号将then后面的包起来,甚至还能在最后加上end if来让语句看上去平衡一点。

python 固然也有if表达式,但是嵌套的可读性就不行了。

for i in range(114, 514 + 1): # Close range [114, 514]
    print(i)

table = [
    f'{x} * {y} = {x * y}' for x in range(1, 10) for y in range(1, 10) if x <= y
]

确实嵌套了for之后就变得难看了呢。

for i <- 114 to 514 do
  print(i)

val table = 
  for
    x <- 1 to 9
    y <- 1 to 9
    if x <= y
  yield s"$x * $y = ${x * y}"

可以看见,for表达式中Scala形式是相似的,只不过当生成Unit(即仅仅执行动作,返回一个无意义的值时),用do,想要收集返回值用yield

while语句只用把冒号变成do即可,遗憾的是,Scala的while不支持breakcontinue

链式调用

一个成熟的OOP语言不敢说自己不支持链式调用,python确实支持,但是多行要记得加反斜杠。

foo \
    .bar() \
    .hello() \
    .baz()

# The following will be syntax error.
foo \
    .bar() \
    # .hello() \
    .baz()

scala 没有这样的限制。

foo
  .bar()
  .hello
  .baz()

foo
  .bar()
  // .hello
  .baz()

lambda表达式

Scala的lambda表达式支持多行。

map(lambda x: x + 1, range(10))

def handle(x: int) -> int:
    y = x + 1
    return x * x + y * y
map(handle, range(10))

Scala的匿名函数支持多行(梅开二度)

(1 until 10).map(_ + 1)
(1 until 10).map { x =>
  val y = x + 1
  x * x + y * y
}
(1 until 10).map(x =>
    val y = x + 1
    x * x + y * y
)

OOP

Python提供了ABC为抽象类或者接口的定义提供了帮助,其中abstractmethod必须被覆写,否则运行时错误。终于有检查了,令人泪流满面。

from abc import ABC, abstractmethod

class Ikuable:
    def say(self) -> None:
        print("iku")

class ImuNoHito(ABC):
    @abstractmethod
    def name(self) -> str:
        pass

class Tadokoro(Ikuable, ImuNoHito):
    _instance  = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def name(self) -> str:
        return "Tadokoro Koji"

trait Ikuable:
  def say(): Unit = print("iku")

trait ImuNoHito:
  def name: String

object Tadokoro extends Ikuable, ImuNoHito:
  def name: String = "Tadokoro Koji"

没有实现的方法自然是虚的方法,为何要加注解呢?单例模式,用object就可以了。

其他

参见这里