誰告訴的你們Python是強類型語言!站出來,保證不打你!

 1. 真的能用隱式類型轉換作為強弱類型的判斷標準嗎?

 
最近有些學員問我,Python到底是強類型語言,還是弱類型語言。我就直接脫口而出:Python是弱類型語言。沒想到有一些學員給我了一些文章,有中文的,有英文的,都說Python是強類型語言。我就很好奇,特意仔細研究了這些文章,例如,下面就是一篇老外寫的文章:
 
其他中文的相關文章,大家可以去網上搜,一堆,這裡就不一一列舉了。
 
我先不說這些結論對不對,我先總結一下這些文章的核心觀點。這些文章將程式語言分為強類型、弱類型、動態類型和靜態類型。這4個概念的解釋如下:
 
強類型:如果一門語言不對變數的類型做隱式轉換,這種程式語言就被稱為強類型語言 ;
弱類型:與強類型相反,如果一門語言對變數的類型做隱式轉換,那我們則稱之為弱類型語言;
動態類型:如果一門語言可以在運行時改變變數的類型,那我們稱之為動態類型語言;
靜態類型:與動態類型相反,如果一門語言不可以在運行時改變變數的類型,則稱之為靜態類型語言;
 

 

 

 其實這些概念就涉及到程式語言的兩個特性:隱式類型轉換和類型固化。
 
所謂類型固化,就是指一旦變數在初始化時被確定了某個數據類型(如整數類型),那麼這個變數的數據類型將永遠不會變化。
 
關於動態類型和靜態類型,在本文的後面再來討論,這裡先探討強類型和弱類型。
 
現在姑且認為這個結論沒問題。強類型就是不允許做隱式類型轉換。OK,我們看看用這個隱式類型轉換來判斷強類型和弱類型是否合理。
 
在這些文章中,給出了很多例子作為證據來證實這個結論,其中最典型的例子是在Python語言中,int + string是不合法的,沒錯,確實不合法。如執行1 + ‘abc’會拋出異常。當然,還有人給出了另一個例子:string / int也是不合法的,如執行’666′ / 20會拋出異常,沒錯,字元串與整數的確不能直接相除。那你怎麼不用乘號舉例呢?如’abc’ * 10,這在Python中可是合法的哦,因為這個表達式會將’abc’複製10份。為何不用我大乘號來舉例,難道瞧不起我大乘號嗎? 這是運算符歧視?

 PS:雖然’abc’ * 10沒有做類型轉換,但這裡說的是乘號(*),儘管目前Python不支援’abc’ * ’10’的操作,但已有也有可能會支援’abc’ * ’10’,也就是將’10’轉換為10,這就發生了類型轉換。

 
另外,難道沒聽說過Python支援運算符重載嗎?通過運算符重載,可以讓兩個類型完全不同的變數或值在一起運算,如相加,看下面的例子:
 
class MyClass1:
    def __init__(self,value):
        self.value = value

class MyClass2:
    def __init__(self,value):
        self.value = value
my1 = MyClass1(20)
my2 = MyClass2(30)
print( my1 + my2)
 
如果執行這段程式碼,100%會拋出異常,因為MyClass1和MyClass2肯定不能相加,但如果按下面的方式修改程式碼,就沒問題了。
class MyClass1:
    def __init__(self,value):
        self.value = value
    def __add__(self,my):
        return self.value + my.value
class MyClass2:
    def __init__(self,value):
        self.value = value
    def __add__(self,my):
        return self.value + my.value

my1 = MyClass1(20)
my2 = MyClass2(30)

print( my1 + my2)

 

 這段程式碼對MyClass1和MyClass2進行了加法運算符重載,這樣兩個不同類型的變數就可以直接相加了,從表面上看,好像是發生了類型轉換,但其實是運算符重載。
 
當然,運算符重載也可能會使用顯式類型轉換,如下面的程式碼允許不同類型的值相加。
class MyClass1:
    def __init__(self,value):
        self.value = value
    def __add__(self,my):
        return str(self.value) + str(my.value)
class MyClass2:
    def __init__(self,value):
        self.value = value
    def __add__(self,my):
        return str(self.value) + str(my.value)

my1 = MyClass1(20)
my2 = MyClass2("xyz")

print( my1 + my2)
 
其實這段程式碼也就相當於int + string形式了,只是用MyClass1和MyClass2包裝了一層。可能有很多同學會說,這能一樣嗎?明顯不是int + string的形式,ok,的確是不太一樣。
 
可惜目前Python還不支援內建類型(如int、str)的運算符重載,但不能保證以後不支援,如果以後Python要是支援內建類型運算符重載,那就意味著可以重載str類的__add__方法了,目前str類定義在builtins.py文件中,裡面已經預定義了很多可能被重載的運算符。當然,目前Python是直接將這些運算符方法固化在解析器中了,例如,__add__方法是只讀的,不能修改。如下面的Python程式碼相當於a + “ok”。
 
a = "abc"
print( a.__add__("ok"))
 
但你不能用下面的程式碼覆蓋掉str類的__add__方法。
 
def new_add(self, value):
    return str(self) + str(value)
str.__add__ = new_add   # 拋出異常
 
執行這段程式碼會拋出如下圖的異常,也就是說,目前Python的內建類型,如str,是不能動態為其添加新的成員或覆蓋以前的成員的。

 

 但現在不能,不代表以後不能。如果以後Python支援覆蓋內建類型的運算符,那麼int + string就可以讓其合法化。不過可能還會有同學問,就算內建類型支援運算符重載,那不還需要使用顯式類型轉換嗎?是的,沒錯,需要類型轉換。

 

 

 現在我們先來談談類型轉換,先用另外一個被公認的弱類型程式語言JavaScript為例。在JS中,1 + ‘abc’是合法的、’444’/20也是合法的,所以就有很多人認為js是弱類型語言,沒錯,js的確是弱類型語言。但弱類型確實是根據1 + ‘abc’和’444’/20得出來的?

 
有很多人認為,JavaScript不做類型檢查,就直接將1和’abc’相加了!你是當真的?如果不做類型檢查,那麼js怎麼會知道如何將1和’abc’相加,為啥不將1當做1.0呢?其實不管是什麼類型的程式語言,數據類型檢測都是必須的,不管是js、還是Python,或是Java,內部一定會做數據類型檢測,只是檢測的目的不同而已。在Python中,進行數據類型檢測後,發現不合規的情況,有時會自動處理(如int+float),有時乾脆就拋出異常(如int + string)。而在Java中就更嚴格了,在編譯時,發現不合規的情況,就直接拋出編譯錯誤了。在js中,發現不合規的情況,就會按最大可能進行處理,在內部進行類型轉換。對,不是不管數據類型了,而是在內部做的數據類型轉換。那麼這和通過Python的運算符重載在外部做類型轉換有什麼區別呢?只是一個由編譯器(解析器)內部處理的,一個是在外部由程式設計師編寫程式碼處理的!而且就算Python不會支援內建類型的運算符重載,那麼也有可能直接支援int + string的形式。因為目前Python不支援,所以正確的Python程式碼不可能有int + string的形式。所以如果以後支援int + string的形式,也可以完全做到程式碼向下兼容。就算Python未來不支援int + string形式,那麼我自己做一個Python解析器(例如,我們團隊現在自己做的Ori語言,支援類型隱式轉換,不過實際上是生成了其他的程式語言,也就是語言之間的轉換,這是不是代表Ori是弱類型語言呢?),完全兼容Python的程式碼,只不過支援int+string形式,那麼能不能說,我的這個Python版本是弱類型Python呢?這很正常,因為像C++這種語言也有很多種實現版本,Python同樣也可以擁有,只不過目前沒多少人做而已,但不等於沒有可能。
 
如果Python真這麼做了,那麼能不能說Python又從強類型語言變成了弱類型語言呢?如果大家認為一種語言的類型強弱是可以隨著時間變化的,那麼我無話可說!
 
總之,需要用一種確定不會變的特性來表示強弱類型才是最合適的。通常來講,某種語言的變數一旦數據類型確定了,就不允許變化了,這種才可以稱為強類型,強大到類型一言九鼎,類型一旦確定,就不允許變了,而Python顯然不是,x = 20; x = ‘abc’;同樣是合法的,x先後分別是int和str類型。
 
PS:這裡再給大家一個表,通常程式語言中確定類型是否兼容,就是通過類似的表處理的。這個表主要用於內建類型,如果是自定義類型,需要通過介面(實現)和類(繼承)類確定類型是否兼容。
 
 
int
float
str
int
True
True
False
float
True
True
False
str
False
False
True
 
 
這個表只給出了3個數據類型:int、float和str。根據業務不同,這個表可以有多種用途,例如,賦值,是否可以進行運算等。這裡就只考慮進行加法運算。 其中True表示允許進行加法運算,False表示不允許進行加法運算,很顯然,如果是int + int形式,第1個操作數可以從第1列查找,第2個操作數可以從第1行查找,找到了(1,1)的位置,該位置是True,所以int + int是合法的,int + float,float + float、str + str的情形類似,如果遇到int + str,就會找到(1,3)或(3,1),這兩個位置都是False,就表明int + str是不合法的。其實Python和JavaScript都進行到了這一步。只不過Python就直接拋出了異常,而JS則嘗試進行類型轉換,但都需要進行類型檢測。因為類型轉換需要確定數據類型的優先順序,優先順序低的會轉換為優先順序高的類型,如str的優先順序比int高,所以int會轉換為str類型。float比int高,所以int會轉換為float類型,這就涉及到另外一個類型優先順序表了。
 
根據這個表可知,程式語言只是在遇到類型不合規的情況下處理的方式不同,這就是編譯器(解析器)的業務邏輯了,這個業務邏輯隨時可能變(通常不會影響程式的向下兼容),所以是不能用這一特性作為強弱語言標識的,否則強類型和弱類型語言就有可能會不斷切換了,因為程式語言會不斷進化的。
 
2. 為什麼應該用類型固化作為強弱類型的標識
 
那麼為什麼可以用類型固化作為強弱類型的標識呢?因為類型固化通常是不可變的,那麼為什麼是不可變的呢?下面用Python來舉例:
 
下面的Python程式碼是合法的。x從int變成了str,類型並沒有固化,所有Python是弱類型語言。
 
x = 20
x = 'abc'

 

那麼有沒有可能Python以後對類型進行固化呢?從技術上來說,完全沒問題,但從程式碼兼容性問題上,將會造成嚴重的後果。因為類型沒固化屬於寬鬆型,一旦類型固化,屬於嚴格型。以前已經遺留了很多寬鬆的程式碼,一旦嚴格,那麼就意味著x = ‘abc’將會拋出異常,就會造成很多程式無法正常運行。所以如果Python這麼做,就相當於一種新語言了,如PythonX,而不能再稱為Python了。就像人類進化,無論從遠古的尼安德特人,還是智人,或是現代各個國家的人,無論怎麼進化,都需要在主線上發展,例如,都有一個腦袋,兩條腿,兩個胳膊。當然,可能細節不同,如黑眼睛,黃頭髮等。你不能進化出兩個頭,8條腿來,當然可以這麼進化,但這個就不能再稱為人了,就是另外一種生物了。

 

 

 現在再看一個相反的例子,如果一種程式語言(如Java)是強類型的,能否以後變成弱類型語言呢?
 
看下面的Java程式碼:
int x = 20;
x = "200";  // 出錯

 

 其實從技術上和兼容性上這麼做是沒問題的。但也會有很多其他問題,如編譯器(或運行時)的處理方式完全不同,我們知道,類型固化的程式要比類型不固化的程式運行效率高,因為類型不固化,需要不斷去考慮類型轉換的問題。而且在空間分配上更麻煩,有可能會不斷分配新的記憶體空間。例如,對於一個數組來說,js和python(就是列表)是可以動態擴容的,其實這個方式效率很低,需要用演算法在合理的範圍內不斷分配新的記憶體空間,而Java不同,數組一旦分配記憶體空間,是不可變的,也就是空間固化(類似於類型固化),這樣的運行效率非常高。
 
所以一旦程式語言從類型固化變成類型不固化,儘管可以保證程式碼的兼容性,但編譯器或運行時的內部實現機理將完全改變,所以從本質上說,也是另外一種程式語言了。就像人類的進化,儘管從表面上符合人類的所有特徵。但內部已經變成生化人了,已經不是血肉之軀了,這也不能稱為人類了。
 
所以無論往哪個方向變化,都會形成另外一種全新的程式語言,所以用類型固化來作為強弱類型標識是完全沒有問題的。
 
 
3. C++、Java、Kotlin是強類型語言,還是弱類型語言
 
 
我看到網上有很多文章將C++歸為弱類型語言。其實,這我是頭一次聽說C++有人認為是弱類型語言,是因為C++支援string+int的寫法嗎?沒錯,C++是支援這種寫法,但直接這麼寫,語法沒問題,但不會得到我們期望的結果,如下面的程式碼:
 
std::cout << "Hello, World!" + 3 << std::endl; 
 
這行程式碼並不會輸出Hello,World!3,要想輸出正常的結果,需要進行顯式類型轉換,程式碼如下:
 
std::cout << "Hello, World!" + std::to_string(3) << std::endl;
 
儘管C++編譯器支援string+int的寫法,但得不到我們期望的結果,所以C++的string和int相加需要進行轉換。因此,僅僅通過string+int或類似的不同類型不能直接在一起運算來判斷語言是否是強類型和弱類型的規則是站不住腳的。而且C++也支援運算符重載,也就意味著可以讓”abc” + 4變成不合法的。
 
那麼Java是強類型還是弱類型呢?Java是強類型語言,因為很多文章給出了下面的例子(或類似):
 
“666” / 4;
 
是的,這個表達式會出錯,但你不要忘了,Java支援下面的表達式:
 
“666” + 4;
 
這行表達式輸出了6664,為啥不用加號(+)舉例呢?前面歧視Python的乘號,現在又歧視Java里的加號嗎?其實這是因為前面描述的類型優先順序問題,由於string的優先順序高於int,因此4會轉換為”4″。所以”666″ / 4其實會也會發生隱式類型轉換,變成”666″/”4″,兩個字元串自然不能相除了,而”666″ + 4會變成”666″ + “4”,兩個字元串當然可以相加了。這就是個語義的問題,和強弱類型有毛關係。
 
 
所以嗎?Java是強類型語言沒錯,但判斷依據錯了。
 
Kotlin是強類型還是弱類型呢?答案是Kotlin是強類型語言。不過Kotlin支援運算符重載,看下面的程式碼。
 
class MyClass(var value: Int) {
    operator fun plus(other: Int): Int {
        return value + other;
    }
}
fun main() {
    var my: MyClass = MyClass(200);
    print(my + 20);  // 輸出220
}
 
 
我們都知道,Kotlin也是JVM上的一種程式語言(儘管可以生成js,但需要用Kotlin專有API),而Java是不支援運算符重載的,在同一個運行時(JVM)上,有的語言支援運算符重載,有的語言不支援運算符重載。從這一點就可以看出,運算符來處理兩側的操作數,只不過是個語法糖而已。想讓他支援什麼樣的運算都可以,如,”abcd” / “cd”,其實也可以讓他合法化,例如,語義就表示去掉分子以分母為後綴的子字元串,如果沒有該後綴,分子保持不變,所以,”abcd”/”cd”的結果就是”ab”,而”abcd”/”xy”的結果還是”abcd”,語法糖而已,與強弱類型沒有半毛錢關係。
 
4. 靜態語言和動態語言
 
現在來說說靜態語言和動態語言。 有人說可以用是否實時(在運行時)改變變數類型判別是靜態語言還是動態語言,沒錯,變數類型的實時改變確實是動態語言的特徵之一,但並不是全部。動態語言的另一些特徵是可以隨時隨地為類【或其他類似的語法元素】(主要是指自定義的類,有一些語言可能不支援對內建類型和系統類進行擴展)添加成員(包括方法、屬性等)。
 
例如,下面的JavaScript程式碼動態為MyClass類添加了一個靜態方法(method1)和一個成員方法(method2)。
 

class MyClass {

}
//  動態添加靜態方法
MyClass.method1 = function () {
    console.log('static method');
}

MyClass.method1()         
var my = new MyClass();
//  動態添加成員方法
my.method2 = function () {
    console.log('common method')
}
my.method2()
 
Python動態添加成員的方式與JavaScript類似,程式碼如下:
 
class MyClass:
    pass

def method1():
    print('static method')
    # 動態添加靜態方法
MyClass.method1 = method1

MyClass.method1()         
my = MyClass()

def method2():
    print('common method')
# 動態添加靜態方法
my.method2 = method2
my.method2()
 
還有就是數組的動態擴容(根據一定的演算法,並不是每一次調用push方法都會增加記憶體空間),如JavaScript的程式碼:
a = []
a.push("hello")
a.push(20)
a.push("world")
console.log(a)
 
Python的數組(列表)擴容:
a = []
a.append('world')
a.append(20)
a.append("hello")
print(a)
 
當然,動態語言還有很多特性,這裡就不一一介紹了。
 
這些特性在靜態語言(如Java、C++)中是無法做到的。在靜態語言中,一個類一旦定義完,就不能再為類動態添加任何成員和移除任何成員,除非修改類的源程式碼。
 
所以說,靜態和動態其實涵蓋了多個方面,如類型固化,動態擴展、數組擴容等。而強類型和弱類型的特性其實只能算靜態和動態的特性之一。也就是說,說一種語言是靜態語言,其實已經包含了這種語言的變數類型一旦確定不可改變的事實,也就是靜態語言一定是強類型的程式語言。
 
如果單獨強調強類型,其實就相當於下面這句話:
 
這個人是一個男人,而且是一個男演員。
 
這句話看起來沒毛病,也能看懂,但其實是有語病的。因為前面已經說了這個人是一個男人了,後面就沒必要強調是男演員了,而只需要按下面說的即可:
 
這個人是一個男人,而且是一個演員。
 
現在來總結一下:
 
應該用固定不變的特性來標識一種語言的特性。而語言是否支援隱式類型轉換,這只是編譯器或運行時的內部業務邏輯,相當於語法糖而已,是隨時可以改變的。而類型固化,動態擴展、數組擴容,這些涉及到程式語言的根本,一旦改變,就變成了另外一種語言了,所以通常用這些特性標識語言的特性。通常來講,靜態語言的效率會高於動態語言。因為,這些動態特性會讓程式有更大負擔,如類型不固定,就意味著可能會為新的類型分配新的記憶體空間,動態擴展和數組擴容也意味著不斷進行邊界檢測和分配新的記憶體空間(或回收舊的記憶體空間)。這就是為什麼C++、Java、C#等程式語言的性能要高於js、Python的主要原因。
 
其實過度強調靜態、動態、強類型、弱類型,意義並不大。以為程式語言以後的發展方向是靜態語言動態化,弱類型強類型化。都是互相滲透了,如果以後出現一種程式語言,同時擁有靜態和動態的特性,其實並不稀奇。例如,儘管變數類型不允許改變,但允許動態為對象添加成員。就和光一樣,既是光子(粒子),又是電磁波,也就是說光擁有波粒二象性! 程式語言也一樣,也會同時擁有靜動態二象性!