Python 語言特性:編譯+解釋、動態型別語言、動態語言

Juno3550發表於2021-04-26

1. 解釋性語言和編譯性語言

  • 1.1 定義
  • 1.2 Python 屬於編譯型還是解釋型?
  • 1.3 收穫

2. 動態型別語言

  • 2.1 定義
  • 2.2 比較

2. 動態語言(動態程式語言)

  • 3.1 定義
  • 3.2 Python 動態語言的體現
  • 3.3 __slots__()

 

 

1. 解釋性語言和編譯性語言

1.1 定義

計算機是不能夠識別高階語言的,所以當我們執行一個高階語言程式的時候,就需要一個“翻譯機”來從事把高階語言轉變成計算機能讀懂的機器語言的過程。這個過程分成兩類,一類是編譯,一類是解釋。

解釋型語言

程式執行前不需要先進行編譯,而是在執行時才通過直譯器對程式碼進行翻譯,翻譯一句然後執行一句,直至結束。

編譯型語言

編譯性語言寫的程式在被執行之前,需要一個專門的編譯過程,把程式編譯成為機器語言(二進位制程式碼)的檔案,比如 exe 檔案,此後再執行時就不用重新翻譯了,直接使用編譯後的結果檔案(exe 檔案)來執行就行。

因為編譯型語言在程式執行之前就已經對程式做出了“翻譯”,所以在執行時就少掉了“翻譯”的過程,因此效率比較高。但是我們也不能一概而論,一些解釋型語言也可以通過直譯器的優化來在對程式做出翻譯時對整個程式做出優化,從而在效率上超過編譯型語言。

此外,隨著 Java 等基於虛擬機器的語言的興起,我們又不能把語言純粹地分成解釋型和編譯型這兩種。用 Java 來舉例,Java 首先是通過編譯器編譯成位元組碼檔案(不是二進位制碼),然後在執行時通過直譯器(JVM)給解釋成機器程式碼才能在各個平臺執行,這同時也是 Java 跨平臺的原因。所以我們說 Java 是一種先編譯後解釋的語言。

總結:將由高階語言編寫的程式檔案轉換為可執行檔案(二進位制的)有兩種方式,編譯和解釋,編譯是在程式執行前,已經將程式全部轉換成二進位制碼,而解釋是在程式執行的時候,邊翻譯邊執行。

  • 編譯型語言:執行效率高;依靠編譯器,因此跨平臺性差些。 
  • 解釋型語言:執行效率低;依靠直譯器,因此跨平臺性好。 

1.2 Python 屬於編譯型還是解釋型?

其實 Python 和 Java 一樣,也是一門基於虛擬機器的語言,我們先來從表面上簡單地瞭解一下 Python 程式的執行過程。

當我們在命令列中輸入 python hello.py 時,其實是啟用了 Python 的“直譯器”,告訴“直譯器”要開始工作了。可是在“解釋”之前,其實執行的第一項工作和 Java 一樣,是編譯。

熟悉 Java 的同學可以想一下我們在命令列中如何執行一個 Java 的程式:

  • javac hello.java(編譯的過程)
  • java hello(解釋的過程

只是我們在用 Eclipse 等 IDE 時,將這兩步給融合成了一步而已。其實 Python 也一樣,當我們執行 python hello.py 時,他也一樣執行了這麼一個過程,所以我們應該這樣來描述 Python,Python 是一門先編譯後解釋的語言。

1.2.1 簡述 Python 的執行過程

在說這個問題之前,我們先來說兩個概念,PyCodeObject 和 pyc 檔案。

當 Python 程式首次執行時,編譯的結果儲存在位於記憶體中的 PyCodeObject 中。當 Python 程式執行結束時,Python 直譯器則將 PyCodeObject 寫回到硬碟中的 pyc 檔案中。

當 Python 程式第二次執行時,首先程式會在硬碟中尋找 pyc 檔案,如果找到則直接載入,否則就重複上面的過程。

所以我們應該這樣來定位 PyCodeObject 和 pyc 檔案,我們說 pyc 檔案其實是 PyCodeObject 的一種持久化儲存方式。

也就是說儲存 pyc 檔案是為了下次再次使用該指令碼時避免重複編譯,以此來節省時間。因此,只執行一次的指令碼,就沒必要儲存其編譯結果 pyc 檔案,這樣只是浪費空間。下面舉例解釋。

示例 1:執行不含“import”關鍵字的程式碼

a.py 程式碼如下:

print("hello world")

執行 a.py:

E:\test>python a.py
hello world

此時我們可以發現,在執行所在目錄下並沒有產生 pyc 檔案,仍只有 a.py。

示例 2:執行含“import”關鍵字的程式碼

新增 b.py,程式碼如下:

import a

執行 b.py:

E:\test>python b.py
hello world

 

此時我們可以發現,pyc 檔案產生了。

1.2.2 pyc 檔案的目的是重用

編譯型語言的優點在於,我們可以在程式執行時不用解釋,而直接利用已經“翻譯”過的檔案。也就是說,我們之所以要把 py 檔案編譯成 pyc 檔案,最大的優點在於我們在執行程式時,不需要重新對該模組進行重新的解釋。

所以,我們需要編譯成 pyc 檔案的應該是那些可以重用的模組,這與我們在設計軟體時是一樣的目的。所以 Python 直譯器認為:只有 import 進來的模組,才是需要被重用的模組

這個時候也許有人會說,不對啊!你的這個問題沒有被解釋通啊,我的 test.py 不是也需要執行麼,雖然不是一個模組,但是以後我每次執行也可以節省時間啊!OK,我們從實際情況出發,思考下我們在什麼時候才可能執行 python xxx.py:

  1. 執行測試。
  2. 開啟一個 web 程式。
  3. 執行一個程式指令碼。

第一種情況(執行測試),這時哪怕所有的檔案都沒有 pyc 檔案都是無所謂的。

第二種情況(開啟一個 web 程式),我們試想一個 web 程式通常這樣執行:

然後這個程式就類似於一個守護程式一樣一直監聽著 8181/9002 埠,而一旦中斷,只可能是程式被殺死,或者其他的意外情況,那麼你需要恢復要做的是把整個的 Web 服務重啟。既然一直監聽著,把 PyCodeObject 一直放在記憶體中就足夠了,完全沒必要持久化到硬碟上。 

最後一種情況(執行一個程式指令碼),一個程式的主入口其實很類似於 Web 程式中的 Controller,也就是說,它負責的應該是 Model 之間的排程,而不包含任何的主邏輯在內,如在 http://www.cnblogs.com/kym/archive/2010/07/19/1780407.html 中所提到,Controller 應該就是一個 Facade(外觀模式),無任何的細節邏輯,只是把引數轉來轉去而已。做演算法的同學可以知道,在一段演算法指令碼中,最容易改變的就是演算法的各個引數,那麼這個時候給持久化成 pyc 檔案就未免有些畫蛇添足了。

所以我們可以這樣理解 Python 直譯器的意圖,Python 直譯器只把我們可能重用到的模組持久化成 pyc 檔案

1.2.3 pyc 的過期時間

說完了 pyc 檔案,可能有人會想到,每次 Python 的直譯器都把模組給持久化成了 pyc 檔案,那麼當我的模組發生了改變的時候,是不是都要手動地把以前的 pyc 檔案 remove 掉呢?

當然 Python 的設計者是不會犯這麼白痴的錯誤的。而這個過程其實就取決於 PyCodeObject 是如何寫入 pyc 檔案中的。

我們來看一下 import 過程的原始碼吧:

這段程式碼比較長,我們只看標註的程式碼,其實它在寫入 pyc 檔案的時候,寫了一個 Long 型變數,變數的內容則是檔案的最近修改日期,同理,我們再看下載入 pyc 的程式碼: 

不用仔細看程式碼,我們可以很清楚地看到原理,其實每次在載入 pyc 之前都會先檢查一下 py 檔案和 pyc 檔案儲存的最後修改日期,如果不一致則重新生成一份 pyc 檔案。 

1.3 收穫

其實瞭解 Python 程式的執行過程對於大部分程式設計師(包括 Python 程式設計師)來說意義都是不大的,那麼真正有意義的是,我們可以從 Python 的直譯器的做法上學到什麼,我認為有這樣的幾點:

  1. 其實 Python 是否儲存成 pyc 檔案和我們在設計快取系統時是一樣的,我們可以仔細想想,到底什麼是值得扔在快取裡的,什麼是不值得扔在快取裡的。
  2. 在跑一個耗時的 Python 指令碼時,我們如何能夠稍微壓榨一些程式的執行時間,就是將模組從主模組分開(雖然往往這都不是瓶頸)。
  3. 在設計一個軟體系統時,重用和非重用的東西是不是也應該分開來對待,這是軟體設計原則的重要部分。
  4. 在設計快取系統(或者其他系統)時,我們如何來避免程式的過期,其實 Python 的直譯器也為我們提供了一個特別常見而且有效的解決方案。

 

2. 動態型別語言

2.1 定義

動態型別語言

所謂動態型別語言,就是(變數、屬性、方法以及方法的返回值)型別的檢查(確定)是在執行時才做

即編譯時與型別無關。一般在變數使用之前不需要宣告變數型別,而變數的型別通常是由被賦的值的型別決定。 如 Php、Python 和 Ruby。

靜態型別語言

與動態型別語言正好相反,在編譯時便需要確定型別的語言。即寫程式時需要明確宣告變數型別。如 C/C++、Java、C# 等。

對於動態語言與靜態語言的區分,套用一句流行的話就是:Static typing when possible, dynamic typing when needed。

強型別語言

強制資料型別定義的語言。也就是說,一旦一個變數被指定了某個資料型別,如果不經過強制轉換,那麼它就永遠是這個資料型別了。因此強型別定義語言是型別安全的語言。

弱型別語言

資料型別可以被忽略的語言。它與強型別定義語言相反, 一個變數可以賦不同資料型別的值。

強型別定義語言在速度上可能略遜色於弱型別定義語言,但是強型別定義語言帶來的嚴謹效能夠有效的避免許多錯誤。

Python 屬於動態型別語言和弱型別語言。

2.2 比較

嚴格意義上,強型別與靜態型別不是一回事,同理弱型別和動態型別。

  • 強型別是指某門語言檢查兩種型別是否相容,如果不相容就丟擲一個錯誤或強制型別轉換,儘管這個說法並不是很嚴格。
  • 靜態型別強迫在型別結構的基礎上執行多型。判斷是否是一隻鴨子的依據,是其基因藍圖(靜態)還是因其叫聲和走路的姿態像一隻鴨子(動態)。

靜態型別語言:

  • 優點:在於其結構非常規範,突出顯示程式碼以便於除錯,方便型別安全。
  • 缺點:需要寫更多的型別相關程式碼(如宣告變數),不便閱讀(特別是當你看別人程式碼時,會連變數定義也看嗎?想必不會,看結構,看方法的含義想必才是本質)。

動態型別語言:

  • 優點:在於方便閱讀,不需要寫非常多的型別相關程式碼。
  • 缺點:自然是不便除錯,命名不規範時會造成讀不懂,不利於理解等。

在強型別、靜態型別語言的支持者,與動態型別、自由形式的支持者之間,經常發生爭執:

  • 前者主張,在編譯的時候就可以較早發現錯誤,而且還可增進執行時期的效能。
  • 後者主張,使用更加動態的型別系統,分析程式碼更為簡單,減少出錯機會,才能更加輕鬆快速的編寫程式。

  

3. 動態語言(動態程式語言)

3.1 定義

根據維基百科,動態(程式設計)語言的定義如下:

動態程式語言是高階程式語言的一個類別,在電腦科學領域已被廣泛應用。它是一類在執行時可以改變其結構的語言,例如新的函式、物件、甚至程式碼可以被引進,已有的類、函式也可以被刪除或是其他結構上的變化。

動態語言目前非常具有活力,例如 JavaScript 便是一個動態語言,除此之外如 PHP、Ruby、Python 等也都屬於動態語言,而 C、C++ 等語言則不屬於動態語言。

3.2 python動態語言的體現

動態語言是一門在執行時可以改變其結構的語言,這句話如何理解?

示例 1:執行過程中給(例項)物件新增屬性

class Person(object):

    def __init__(self, name=None, age=None):
        self.name = name
        self.age = age


Jack = Person("Jack",18)
print(Jack.age)    

在上述程式碼中,我們定義了 Person 類,然後建立了 Jack 物件,列印物件的 age 屬性,這沒毛病。現實中人除了名字和年齡,還會有其他屬性,例如身高和體重。我們嘗試列印一下身高屬性。

print(Jack.height)

毫無疑問,這會報錯,因為 Person 類中沒有定義 height 屬性。

但是如果在程式執行的時候新增 height 屬性,會發生什麼呢?

Jack.height = 170
print(Jack.height)  # 輸出結果:170

setattr(Jack, 'height', 170)
print(Jack.height)  # 輸出結果:170

在上述程式碼中,我們給 Jack 新增了 height 屬性,然後列印,沒有報錯,可以輸出結果。

我們再列印一下物件的屬性:

print(Jack.__dict__)  # 輸出結果:{'name': 'Jack', 'age': 18, 'height': 170}

本來物件是沒有 height 屬性,但是可以在程式執行過程中給例項物件動態繫結屬性,這就是動態語言的魅力。

需要注意:

Mia = Person('Mia', 18)
print(Mia.__dict__)  # 輸出結果:{'name': 'mia', 'age': 18}

Mia 物件居然沒有 height 屬性。為什麼?事實上我們只是給類示例動態地繫結了一個屬性,而不是給類繫結屬性,所以重新建立的物件是沒有 height 屬性的。如果想要給類新增,也是可以的。

示例 2:動態給類新增屬性

Person.height = None
Mia = Person("Mia", 18)

print(Mia.height)  # 輸出結果:None

示例 3:動態給物件新增方法

class Person(object):

    def __init__(self,name=None,age=None):
        self.name = name
        self.age = age

def speak_name(self):
    print(self.name)

Jack
= Person("Jack", 18) Jack.speak_name = speak_name Jack.speak_name(Jack) # Jack print(Jack.__dict__) # {'name': 'Jack', 'age': 18, 'speak_name': <function speak_name at 0x000001F86CAE1E18>} Mia = Person("Mia", 18) print(Mia.__dict__) # {'name': 'Mia', 'age': 18}

在上述程式碼中,物件 Jack 的屬性中已經成功新增了 speak_name 函式。但是!有沒有感覺 Jack.speak_name(Jack) 這個語句很彆扭。按習慣來說,應該 Jack.speak_name() 就行了。如果想要達到這種效果,應該要像下面這樣子做:

import types

Jack.speak_name = types.MethodType(speak_name, Jack)
Jack.speak_name()  # 輸出結果:Jack

其中 MethodType 用於繫結方法物件。

示例 4:動態給類新增方法

import types


class Person(object):
    def __init__(self, name=None, age=None):
        self.name = name
        self.age = age


def speak_ok(cls):
    print(OK)


Person.speak_name = types.MethodType(speak_ok, Person)
Person.speak_ok()  # OK

示例 5:動態刪除屬性/方法

Mia = Person("Mia", 18)
delattr(Mia,'height')  # 等價於 del Mia.height

print(Mia.__dict__)
# 輸出結果:{'name': 'mia', 'age': 18}

總結

  • 給例項物件新增屬性/方法:物件名.屬性/方法名 = xxxx
  • 給類物件新增屬性/方法:類名.屬性/方法名 = xxxx
  • 給例項/類物件刪除屬性/方法:
    • del 例項/類物件.屬性/方法名
    • delattr(例項/類物件, "屬性/方法名")

3.3 __slots__()

通過以上例子可以得出一個結論:相對於動態語言,靜態語言具有嚴謹性!所以,玩動態語言的時候,小心動態的坑!

如果我們想要限制例項物件的屬性怎麼辦?比如,只允許對 Person 的例項物件新增 name 和 age 屬性。

為了達到限制的目的,Python 允許在定義類的時候,定義一個 __slots__() 方法,來限制該例項物件能新增的屬性:

>>> class Person:
...     __slots__ = ("age", "name")
...
>>> p = Person()
>>> p.age = 12
>>> p.name = "xiaoming"
>>> p.hobby = "football"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'hobby'

注意:__slots__ 定義的屬性僅對當前類的例項物件起作用,對繼承的子類是不起作用的

>>> class Student(Person):
...     pass
...
>>> s = Student()
>>> s.hobby = "football"
>>>

 

相關文章