1. 程式人生 > >誰告訴的你們Python是強型別語言!站出來,保證不打你!

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

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

  最近有些學員問我,Python到底是強型別語言,還是弱型別語言。我就直接脫口而出:Python是弱型別語言。沒想到有一些學員給我了一些文章,有中文的,有英文的,都說Python是強型別語言。我就很好奇,特意仔細研究了這些文章,例如,下面就是一篇老外寫的文章: https://wiki.python.org/moin/Why%20is%20Python%20a%20dynamic%20language%20and%20also%20a%20strongly%20typed%20language   其他中文的相關文章,大家可以去網上搜,一堆,這裡就不一一列舉了。   我先不說這些結論對不對,我先總結一下這些文章的核心觀點。這些文章將程式語言分為強型別、弱型別、動態型別和靜態型別。這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的主要原因。   其實過度強調靜態、動態、強型別、弱型別,意義並不大。以為程式語言以後的發展方向是靜態語言動態化,弱型別強型別化。都是互相滲透了,如果以後出現一種程式語言,同時擁有靜態和動態的特性,其實並不稀奇。例如,儘管變數型別不允許改變,但允許動態為物件新增成員。就和光一樣,既是光子(粒子),又是電磁波,也就是說光擁有波粒二象性! 程式語言也一樣,也會同時擁有靜動態二象性!