此处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
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不支持break
和continue
。
链式调用
一个成熟的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
就可以了。
其他
参见这里