从类型系统的角度来看,Python、Java和Golang之间有哪些不同之处
最初
背景
我一直在使用Python,但在转职的新岗位上,主要使用Java和Golang。我意识到在开发中要注意Python所没有的特定类型的好处,以及对Java和Golang的接口差异并不太了解,所以我进行了一些调查。
这篇文章介绍了Python、Java和Golang之间的差异,并在文章末尾明确指出了Golang和Java的接口差异,并尝试用Python进行了重写。
目前,有一类特定的读者
-
- 動的型付け言語しか使ったことない人
-
- JavaとGolangの違いがはっきり分からない人
- 型を意識したことがあまりない人
简单的实施
首先,尝试用Python、Java和Golang编写一个最简单的类。从下面的例子可以看出,即使是不熟悉Python的人,也可以轻松理解,因为它非常简单。
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
return f”{self.name} : 汪汪!”
dog = Dog(“旺财”)
print(dog.bark()) # 旺财 : 汪汪!
若使用Java,则结果如下。
private String name;
public Dog(String name) {
this.name = name;
}
public String bark() {
return this.name + ” : 汪汪!”;
}
public static void main(String[] args) {
Dog dog = new Dog(“小狗”);
System.out.println(dog.bark()); // 小狗 : 汪汪!
}
}
下一个用Golang编写。
包 main
导入 “fmt”
type Dog struct {
name string
}
func NewDog(name string) *Dog {
return &Dog{name}
}
func (d *Dog) Bark() string {
return d.name + ” : 汪汪!”
}
func main() {
dog := NewDog(“旺财”)
fmt.Println(dog.Bark()) // 旺财 : 汪汪!
}
Python和Java是面向对象的,而Golang则不是传统的面向对象,因为它没有类(class)。
类型系统
有几种方式可以对类型系统进行分类。
-
- 動的型付け vs. 静的型付け
-
- 強い型付け vs. 弱い型付け
- Structural Type System(構造的型付け) vs. Nominal Type System (記名的型付け)
首先,我们将从易于理解的动态类型和静态类型开始解释。
没有形态的语言
機械語和Shell腳本並不具有型別的概念。
动态类型的编程语言
型的概念在语言中有,但类型兼容性检查在运行时进行。
例如:Ruby、Python、Perl、JavaScript、Lisp等。
静态类型编程语言
在语言中,存在着类型的概念,并在执行之前进行类型一致性检查。
型檢查
需要明确地描述类型,并且仅需要进行类型兼容性检查的语言,例如:C、Java。
类型推理
不需要明确地描述类型(也可以不描述),而是需要在推断类型的同时检查一致性的语言。
例如:OCaml、Haskell、F#
类型安全
如果程序P的类型检查成功,则称其为类型安全(也称为强类型),它满足在P执行时不会发生由于类型不匹配引起的错误。
强类型 ɡ ɡ ɡ)
可以检测到类型不一致。
动态强类型
Python和Ruby等
a = "1"
b = 5
a+b # エラー
强类型且静态
Golang(Go语言),Java等
package main
import "fmt"
func main() {
a := "1"
b := 5
fmt.Println(a + b) // エラー
}
弱类型
有时无法检测到类型不匹配。
动态且弱类型
只需一种选择,以下是对此进行本地化的中文释义:
PHP、JavaScript等
var x = 1
var y = '9'
x + y // '19'
强类型化的、静态的
C和C++等
#include <stdio.h>
int main(void){
int num = 1;
char chr = 'a';
printf("%d", num + chr); //98
}
鸭子打字
鸭子类型是动态类型语言中常用的类型划分方式,常被应用于Ruby和Python等语言中。
如果它走起来像鸭子,叫起来也像鸭子,那它一定是鸭子。 – Dave Thomas
尝试在Python中实践鸭子类型。
class Dog:
def Bark(self):
print("Bow wow?")
class Bird:
def sing(self):
print("Chun chun?")
class Robot:
def Bark(self):
print("Woooo?")
def roll_call(animal):
if hasattr(animal, 'Bark'):
animal.Bark()
else:
print("This is not animal.❌")
dog = Dog()
bird = Bird()
robot = Robot()
roll_call(dog) # Bow wow?
roll_call(bird) # This is not animal.❌
roll_call(robot) # Woooo?
因为鸟没有Bark()方法,所以被判断为不是动物,然而除此之外,它被视为一种动物。
好处
当使用鸭子类型时,只需要具备调用方法,调用方不需要了解类型等前提知识。
-
- 柔軟性・拡張性が高くなる
- 依存が少なくなる
缺点。
无论在何种情况下,都存在着权衡利弊的情况。在鸭子类型中,有可能无意中将具有意外类型的特定函数传入。换句话说,存在如下的不利之处。
- 型の整合性によるエラーが起きやすくなり、危険性が伴う
名义类型系统
例如 C# 和 Java,都是像 Java 一样,重视命名,并将部分类型关系明确声明的类型系统。
在Java中,无法声明类似 {fst: Nat, snd: Nat} 这样没有名称的类型。
在声明局部变量、字段和方法的参数时,必须要有名称。
优势
-
- 再帰型の取り扱いが簡単
値の型がそれぞれ名前で保存されるだけ
リフレクションやマーシャリング、画面出力などで活用できる(structuralな型システムでも、値に型を示すタグを埋め込めばいいわけだが、あまり活用されていない。)
型検査後も、特に実行時に型情報を活用できる
型検査、特にsubtypingの検査が自明なまでに簡単
明示的にsubtypingを宣言するため、誤りが生じにくい
「偽の包摂関係」を防止できる
(ある型の値をその型とは完全に別だが、構造的には互換がある型が期待されている場所に用いるようなプログラムを型検査機が拒絶し損ねるという問題)を防止することができる。(ただし、単一コンストラクタデータ型や抽象型などもっといい方法がある)
缺點
-
- 名前的型システムでは、型名とその定義についてのテーブルを常に扱う必要があり、そのせいで定義も証明もより冗長となってしまいがち
-
- 研究ではStructural Subtypingの方が人気
-
- 発展的な機能(パラメータ多相)、抽象データ型、ユーザ定義の型演算子、ファンクタなどの型抽象に関する強力な仕組み)は、名前的型システムと相性が悪い(List(T)のような型は複合的な型であり、原始的な名前として扱うことができない。List(T)の挙動を見るには、Listの定義を参照する必要がある)
- コードが冗長になりやすい
名义类型划分
如果名字相同,那么 Nominal Typing 就是一种具备互换性的类型机制。
名义子类型
Subtype的意思是
Subtype是指部分类型。部分类型指的是is-a关系中的元素。类的继承与面向对象有相似之处。Subtype大多数情况下具有可替换性。也就是说,满足里氏替换原则。
设 φ(x) 是关于类型为 T 的对象 x 可证明的性质。那么,对于类型 T 的子类型 S 的对象 y,φ(y) 必须为真。B. Liskov 和 J. Wing,《关于子类型的行为观念》
再用更简单易懂的方式来表达就是
「子类型」的对象是具有另一种类型(”超类型”)对象的所有行为以及额外功能的对象。在这里需要的是以下所示替代性质的东西:对于类型S的每个对象o1,存在类型T的对象o2,如果对于T定义的所有程序P,在将o1替换为o2时不会改变其行为,则S是T的子类型。B. Liskov,《数据抽象和层次结构》,Liskov 1988,第25页,3.3. 类型层次结构。
具体举例来说,就像以下的Java例子一样,当int <: double(int型是double型的子类型)时,将int型对象T转换为double型不会有任何问题,但将double型视为int型是有风险的。
public class Main {
public static void main(String args[]) {
int x = 100;
double y;
y = x;
System.out.println(y); // 100.0
double a = 100;
int b;
b = a;
System.out.println(y); // コンパイルエラー
}
}
结构类型系统
TypeScript、OCaml、ML等编程语言中,类型系统以类型的结构为基础直接定义了子类型关系,而不是依赖于名称的本质。
优点
-
- 少なくとも再帰型がなければ、構造的型システムのほうが少し整然・洗練されている
- 構造的なほうでは、型式は独立したものとなり、型式の意味を理解するために必要な情報が備わっている
缺点
- 再帰型が定義しにくい
结构类型
如果结构相同,则视为相同类型。
签名的定义是用于识别方法的信息,
从名义的观点来说,签名和结构的观点定义的签名有不同的含义。
具体地说,前者的签名包括方法名,而后者不包括方法名。
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 引数にDog型を明示する
const printDogName = (dog: Dog) => console.log(dog.name);
printDogName(new Dog("ポチ")); // OK ?
printDogName(new Cat("タマ")); // OK ? 構造が一緒なので同じクラスとみなされる。
构造子类型(構造的部分型)
Structural Subtyping在中文中被称为静态鸭子类型。
关于静态鸭子类型,在后面给出了Golang接口的例子。
混合式的类型化
最近,有很多语言同时具有结构性质和名义性质,比如Golang和Scala等。
Golang 是一种结构化语言吗?
Golang 看起来是一种名义类型(Nominal Typing)。
在下面的例子中,即使类型结构相同,但由于类型名称不同,所以会产生错误。
package main
import "fmt"
func main() {
type t string
var foo t = "abc"
var bar string
fmt.Printf("%T\n", foo) // main.t
fmt.Printf("%T\n", bar) // string
bar = foo // cannot use str1 (variable of type t) as string value in assignment
}
然而,不像Java那样需要明确地为类型命名,也可以使用无名类型如下所示。
package main
import "fmt"
func main() {
fullname := struct {
FirstName string
LastName string
}{
FirstName: "John",
LastName: "Doe",
}
fmt.Printf("%T\n", fullname)
}
其他例子:https://go.dev/play/p/Q5wSbFPNbx7
然后,关于是否符合里斯科夫替换原则,在Golang中无法像Java一样进行隐式类型转换。
package main
import "fmt"
func main() {
var x int = 100
var y float64
y = x
fmt.Println(y) // エラー
}
那么,我们来考虑一下接口。
package main
import “fmt”
type factory interface {
createFace() string
}
type Hero struct {
name string
}
func (h Hero) createFace() string {
return h.name
}
type Anpanman struct {
Hero
punch string
kick string
}
type Shokupanman struct {
Hero
punch string
}
type Batako struct{}
func (Batako) changeFace(f factory) {
fmt.Println(f.createFace(), “新しい顔よ〜!”)
}
func main() {
a := Anpanman{
Hero: Hero{name: “アンパンマン”},
punch: “アンパンパンチ”,
kick: “アンパンキック”,
}
s := Shokupanman{
Hero: Hero{name: “食パンマン”},
punch: “食パンパンチ”,
}
b := Batako{}
b.changeFace(a) // アンパンマン 新しい顔よ〜!
b.changeFace(s) // 食パンマン 新しい顔よ〜!
}
“`
使用Golang进行鸭子类型
顺便说一下,尽管Golang是一种动态类型语言,但常常被称为是鸭子类型。
我认为这是因为它采用了结构子类型(静态鸭子类型)的缘故。
刚刚用Python
package main
import "fmt"
type Animal interface {
Bark()
}
type Dog struct{}
func (d Dog) Bark() {
fmt.Println("Bow wow?")
}
type Bird struct{}
func (b Bird) sing() {
fmt.Println("Chun chun?")
}
type Robot struct{}
func (r Robot) Bark() {
fmt.Println("Woooo?")
}
func runBark(animal Animal) {
animal.Bark()
}
func main() {
dog := Dog{}
bird := Bird{}
robot := Robot{}
runBark(dog) // Bow wow?
runBark(bird) // エラー❌
runBark(robot) // Woooo?
}
只有不符合Python行为的bird才会出现错误。这就是为什么Golang被称为鸭子类型的原因。尽管Golang是具名化的,但它也具有结构子类型化(静态鸭子类型化)。
界面
在Golang和Java中有interface,但在Python中不存在。
根据不同的语言,它也被称为protocol、trait等。
有一个以契约为例子来解释interface的简明说明。
interface只是对象操作名称和类型的集合。
使用界面的好处和坏处
优点
能够进行实现的切换
测试变得更加容易
可以进行多重继承
缺点
可能会变得冗长。
Golang和Java的Interface之间有何差异?
首先,作为最大的不同之一,Java中需要明确使用implements关键字。
相比之下,Golang中的接口是结构化的,就像之前提到的鸭子类型示例一样,因此不需要明确声明。
在业务中,很多情况下建议使用Java编写接口。然而,在Golang中,编写接口往往被认为是冗长的。
在Java中,接口本身被视为基础类型,用于表示它是什么样的类。
Golang被称为静态鸭子类型,而Golang的接口定义了应该满足的行为,并通过满足这些行为来表示约束。
尝试用Python编写Golang和Java中的接口
用interface来重写之前简单的类。Golang的interface和Java的显式interface的目的都是表达出行为,就像鸭子类型一样,但它们并不完全相同。当然,在用Python写的时候也完全不一样。
Java的接口
String bark();
}
Java的类Dog实现了Animal {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String bark() {
return this.name + ” : 汪汪!”;
}
public static void main(String[] args) {
Animal dog = new Dog(“小黑”);
System.out.println(dog.bark()); // 小黑 : 汪汪!
}
}
这可以用Python编写,并使用ABC来完成。
从abc库中导入ABC和abstractmethod
class Animal(ABC):
@abstractmethod
def bark(self):
pass
class Dog(Animal):
def __init__(self, name):
self.name = name
def bark(self):
return f”{self.name} : wangwang!”
dog = Dog(“旺财”)
print(dog.bark()) # 旺财 : wangwang!
当使用ABC来实现与Java中的public class Dog implements Animal相同的功能时,需要明确使用class Dog(Animal)来指定。
Golang的接口
package main
import “fmt”
type Animal interface {
Bark() string
}
type Dog struct {
name string
}
func NewDog(name string) *Dog {
return &Dog{name}
}
func (d *Dog) Bark() string {
return d.name + ” : 汪汪!”
}
func main() {
dog := NewDog(“旺财”)
fmt.Println(dog.Bark()) // 旺财 : 汪汪!
}
Golang 的 interface 是 structural 的。前面的 Java 例子是 nominal 的。
Python 使用 typing 的 Protocol 来实现这一点。Protocol 在 Python 中实现了 Structural Subtyping(静态鸭子类型)。
从typing导入Protocol
class Animal(Protocol):
def bark(self) -> str:
pass
class Dog:
def __init__(self, name: str):
self.name = name
def bark(self) -> str:
return f”{self.name} : 汪汪!”
dog = Dog(“旺财”)
print(dog.bark()) # 旺财 : 汪汪!
不需要像ABC那样明确声明为类Dog(Animal)一样。
最终,
因为这篇文章变得太长了,所以省略了使用界面的DI(依赖反转)等示例。但是在下一篇文章中,我想要谈论DI和更实用的例子。
文献引用
-
- 《Types and Programming Languages》(The MIT Press)硬皮版-2002年1月4日
-
- 接口(Interface)类型是由一组方法签名定义的。这意味着什么?
-
- 接口作为“合约”的问题点——对“接口默认实现”的 C# 的引入(前篇)
-
- 什么是接口(Interface)?| 使用接口的方法
-
- 接口有什么好处?
-
- https://peps.python.org/pep-0544/
-
- Python 中协议(鸭子类型)和 ABC(抽象)的区别
-
- 【Java】考虑使用接口的实际例子
-
- 为什么要使用接口?
-
- 为什么可以说 Go 采用了鸭子类型?从数据结构详细解释
-
- 通过Go的实现示例理解鸭子类型
-
- Python 中协议(鸭子类型)和 ABC(抽象)的区别
-
- 结构子类型与名义子类型比较理解
-
- 结构子类型
-
- 你很擅长泛型,是吗?或者说,“约束造就结构”
-
- 无需预先声明接口类型
-
- 以嵌入类型的方式作为结构体(Go中不使用继承)
-
- 程序语言论
-
- 静态类型推断与动态类型检查
-
- 编程语言-关于动态和静态类型强度、弱度的问题
-
- 类型推断和类型检查,静态类型和动态类型,强类型和弱类型
-
- 编程语言的比较
-
- 类型检查和类型推断概述
-
- 关于Go语言的接口和鸭子类型
-
- 鸭子类型是面向对象特有的多态吗?
-
- 在编写Python之前要了解的各种数据类型基础知识
-
- 结构类型与名义类型系统
-
- 为什么Go的类型系统不具备变量性?
-
- PEP 544-协议:结构子类型(静态鸭子类型)
-
- 介绍部分子类型化的幽灵类型
-
- 存活于Swift中的结构类型
- Go算不算面向对象语言?