Python基礎教程之名稱空間以及作用域

前言

所謂“基礎不狠,人站不穩”,對於任何一種編程語言來說基礎往往都是重中之重,以Python為例,其中的兩大分水嶺就是函數編程和面向對象,而今天所要鞏固的知識點後續會多次使用,那就是名稱空間和作用域

名稱空間

什麼是名稱空間

在Python中名稱空間是用存儲對象和名字綁定關系的地方,那麼問題來瞭,什麼是對象,什麼是名字,什麼是綁定關系?

1)在目前,我們對於對象的認知可以暫時隻停留在人雲亦雲的“Python中一切都是對象”基礎上,函數是對象、類是對象、變量,模塊、所有一切都是對象,有這樣的認知就可以瞭,後續有機會將繼續補充;

2)名字,很簡單,每一次我們對模塊、變量、函數、類的定義都需要取名字,而這些名字都會放在名稱空間之中;

3)Python對於名字和數據之間給出瞭綁定關系,舉個例子,當我們在定義a = 6時,Python就自動將變量a這個名字與6這個對象給出瞭綁定關系,我們可以使用del語句將綁定關系解除。

明白瞭名稱空間是用於存儲對象和名字綁定關系的地方,那麼接下來就可以細致瞭解一下名稱空間可以分為哪幾類瞭:

1)內置名稱空間 —— 用於存放各種內置函數(built-in functions)、內置模塊(built-in modules),例如abs()就是內置函數,內置名稱空間可以在Python任何一處使用;

2)全局名稱空間 —— 全局名稱空間中的名字可以在同一個模塊中任意處使用;

3)局部名稱空間 —— 局部名稱空間中的名字僅僅隻能夠在函數內部使用。

名稱空間的意義

名稱空間最大的作用就是防止名字重復造成的引用不當,我們可以在全局名稱空間中定義一個a = 6同時也可以在局部名稱空間中定義一個a = 7,這兩者之間是不會產生任何沖突的,這就是名稱空間最大的作用,防止名字重復造成的引用不當。

名稱空間的查找順序

知道瞭名稱空間的意義,那麼肯定會有讀者意識到,我在全局定義一個a = 6,在局部定義一個a = 7,那麼接下來調用a這個名字的時候,Python究竟會從哪個空間開始尋找a所對應的對象呢?

我隻能說,這位讀者你很上道,我們將以實例解答這個問題;

a = 6              # 在全局名稱空間中定義一個a
b = 8            # 在全局名稱空間中定義一個b,為瞭測驗調用函數時能否找到全局中的b
def test():
    a = 7        # 在局部名稱空間中定義一個a
    return a,b
print(test())
print(a)    

(7,8)
6

從以上我們的測驗中,調用函數test時輸出的a將會是7,而當直接使用print(a)時輸出的a將會是6。

所以我們可以大膽的下結論:

1)當調用函數的時候,函數尋找名字的順序將會是 局部名稱空間—>全局名稱空間—>內置名稱空間;

2)當沒有調用函數,直接使用名字的時候查找順序就是 全局名稱空間 —>內置名稱空間;

3)隻要在某個名稱空間(局部也好、全局也罷)中找到瞭對應的名字,就停止尋找;

4)在不同名稱空間中定義相同名字是可行的,後續定義的並不會將原先覆蓋掉。

局部名稱空間詳解

在局部名稱空間中有一個非常神奇的事情,因為函數是可以相互嵌套的,在一個函數中嵌套另外一個函數是很正常的現象:

def test_1():           # 定義一個函數
    def test_2():       # 在test_1中定義一個嵌套函數
        print('球球好心人給個贊吧')
# 這是最簡單的函數嵌套,
# 但也是最不規范的函數嵌套,
# 因為如果不改進的話,則無法使用嵌套的test_2函數

以上就是最簡單形式的函數嵌套,那麼問題接踵而至,上文中說過局部命中空間是在函數中產生的,那麼如果我在一個函數中定義一個嵌套函數,是不是意味著我在局部名稱空間中創建瞭一個局部名稱空間?

對頭!

但是在術語上我們會稱test_2為最內部名稱空間,而test_1則是被我們稱為附屬函數名稱空間;

我們可以如此反復俄羅斯套娃:

def test_1():           # 定義一個函數
    def test_2():       # 在test_1中定義一個嵌套函數
        def test_3():   # 在內嵌函數test_2中再定義一個嵌套函數
            # 省略一萬層....
            print('球球好心人給個贊吧')
        print('球球好心人給個贊吧')

嵌套函數中的查找順序

在前文中已經介紹過瞭關於嵌套函數所產生的附屬函數名稱空間、內部名稱空間,那麼如果在附屬函數名稱空間和內部名稱空間都定義一個相同名字,那麼查找順序是如何呢?

b = 10                    # 在全局定義一個b
def test_1():           # 定義一個函數
    def test_2():       # 在test_1中定義一個嵌套函數
        a = 6           # 在內部名稱空間中定義一個a
        return a,b
    a = 7                # 在附屬名稱空間中定義一個a
    b = 8               # 在附屬名稱空間中定義一個b
​​​​​​​print(test_1())            # 調用函數

如果真的如上文中這樣寫的話,那麼將不會輸出任何結果哦,因為我們隻調用瞭test_1,而作為嵌套的內部函數test_2沒有被使用到,想要使用嵌套函數的話,就隻能通過將嵌套函數作為返回值,返回出去

所以將代碼修改一下

b = 10                    # 在全局定義一個b
def test_1():           # 定義一個函數
    def test_2():       # 在test_1中定義一個嵌套函數
        a = 6           # 在內部名稱空間中定義一個a
        return a,b
    a = 7                # 在附屬名稱空間中定義一個a
    b = 8               # 在附屬名稱空間中定義一個b
    return test_2
print(test_1()())        # 調用函數

10
(6,8)

按照修改後,我們所得到的結果將會是6,當我們調用嵌套函數的時候,嵌套函數會從自身的局部空間中開始尋找是否有該名稱

就像調用嵌套函數test_2一般,它從自己的局部名稱空間開始尋找,找到瞭a = 6後就停止尋找

所以我們又可以下結論瞭:

1)當調用嵌套函數的時候,它的查找順序是 內部名稱空間—>附屬函數名稱空間—>全局名稱空間—>內置名稱空間;

2)找到對應的名字後就會停止尋找。

關於嵌套函數的使用

b = 10                    
def test_1():           
    def test_2():       
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2
print(test_1()())                # 仔細看一下調用函數的過程

為什麼調用函數過程中需要寫兩個括號test_1()(),而不是直接test_1()呢?

我們仔細看一下test_1函數的返回值,test_1的返回值是一個函數對象test_2,所以我們如果調用函數的話隻寫一個括號將會得到一個函數對象,也就是test_2

來實例示范一下;

b = 10                    
def test_1():           
    def test_2():       
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2
print(test_1(), type(test_1()))        # 打印輸出一下結果

<function test_1..test_2 at 0x000001A8E9981F30> <class ‘function’>
以上就是打印輸出的結果,代表瞭函數對象

可能有讀者想要唱反調瞭,我就是想直接寫一個test_1()就能夠直接得到想要的結果該怎麼辦呢?

我隻能說,這位看官你很有成為天才的潛力,因為懶才是人類進步的基石,這個需求可以實現,但是我們要這樣改代碼:

b = 10                    
def test_1():           
    def test_2():       
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2()
print(test_1())                # 調用函數

(6,8)

我們的確得到瞭想要的結果,仔細想一下為什麼呢?

還是因為返回值,函數test_1的返回值是test_2(),也就是說返回的結果是函數test_2運行後的結果,又開始俄羅斯套娃瞭,我拿到的返回值是另外一個函數的返回值!?

我隻能說沒錯,是這樣的。

但是用這個方法需要註意一點,那就是內嵌函數必須是無參數的!

b = 10                    
def test_1():           
    def test_2(c):           # 隨便定義一個參數
        a = 6           
        return a,b
    a = 7                
    b = 8               
    return test_2()
print(test_1())                # 調用函數

TypeError: test_1..test_2() missing 1 required positional argument: ‘c’
這將會報錯,給出的錯誤是函數test_2()缺失一個名為’c’的位置參數
所以想要使用這種方法還是需要註意下的,
但是換另外一種思路,如果內嵌函數需要參數,那麼我返回的時候先把參數定義不行麼?
這種方法的確是可行的,
但是如果這樣的話那不如直接使用默認參數,在定義的時候直接將參數c定義好

以上就是關於俄羅斯套娃的名稱空間的講解,接下來我們要介紹一下作用域瞭,如果能夠將名稱空間中的知識點李姐,那麼作用域也不過爾爾。

作用域

什麼是作用域

作用域是根據名稱空間所產生的,意思就是名字的作用范圍;

在上文之中我們其實已經或多或少涉及到瞭作用域瞭。

b = 10                            # 這個全局變量的作用域就是該模塊中的全部范圍        
def test_1():           
    def test_2():       
        a = 6           
        return a,b                # 正因為全局變量的作用於是全部范圍,才能夠返回b
    a = 7                
    b = 8               
    return test_2
print(test_1()())

或多或少讀者對於這個作用域已經有些許瞭解,

我直接將結論擺出:

內置名稱空間 —— 其作用域是Python中的所有模塊,能夠在所有的模塊中使用;
全局名稱空間 —— 其作用域是該模塊的所有范圍,能夠在模塊內隨意使用;
局部名稱空間 —— 其作用域僅僅在於該函數內部,隻能夠在函數內部使用。

可能正是因為作用域的不同,所以查找順序也會不同,作用域越大的名稱空間反而查找的優先級越低,

正如上文中的,即使全局和局部中都有a這個名字,調用函數的時候也會先從局部開始。

global語句

不同的名稱空間可以定義相同的名字,這樣不會有任何沖突,可這也意味著,當我們在局部名稱空間的時候是無法修改全局中的名字綁定關系,於是Python提供瞭一個方法去解決這個問題:

a = 10               # 定義一個全局語句

def test():
    global a        # 使用global語句,聲明我是用的名字a全局名稱空間中的那個a
    a = 5
    return a

print(a)            # 先打印輸出一下沒有調用函數前的a是什麼
print(test())        # 輸出一下函數中的結果
print(a)            # 看一下全局中的a是否發生瞭改變

10
5
5

所以我們可以知道,使用global語句後,我們使用的名字都將會是全局名稱空間的

nonlocal語句

既然在局部可以修改全局名稱空間中的名字綁定關系,那麼在內部名稱空間是否可以修改附屬函數名稱空間中的綁定關系呢?

答案顯然是可以的,但是需要使用到nonlocal語句。

def test_1():
    def test_2():				# 在test_1裡定義一個嵌套函數test_2
        a = 15

        def test_3():			# 俄羅斯套娃一波
            nonlocal a			# 使用nonlocal,聲明接下來使用的a是附屬名稱空間的a,所以究竟是test_2中的還是test_1中的?
            a = 5
            print('調用test_3對a這個名字的綁定關系進行更改')

        test_3()
        print(f'輸出附屬函數名稱空間中的a:{a}')

    a = 10
    test_2()
    print(f'輸出最外部函數的a值:{a}')


test_1()

調用test_3對a這個名字的綁定關系進行更改
輸出附屬函數名稱空間中的a:5
輸出最外部函數的a值:10

由此我們可以知道,在調用nonlocal聲明使用的a是函數test_2中的,而不是test_1中的

因此我們可以得出的結論是:

1)在使用nonlocal語句的過程中,僅僅隻會向上尋找到對應名字修改一次

題目題目

還是老規矩,寫一個題目對上述內容進行測試

def discount(price,rate):
    final_price = price * rate
    old_price = 6
    print('old_price的值',old_price)
    return final_price

old_price = float(input('請輸入價格'))
rate = float(input('請輸入折扣率'))
print(discount(old_price,rate))
print('old_price的值',old_price)

在上述代碼中,假設我輸入的old_price是100,rate是0.6
那麼請問兩個問題

print(discount(old_price,rate))中會輸出的值是多少?
print('old_price的值',old_price)輸出的值是多少?

小結

  1. 命名空間是一個名字(變量)和對象的映射表。
  2. 作用域是指命名空間的作用范圍,或者說管轄區域。
  3. 變量的查找遵循 LEGB 原則,先從基層(最內層函數找),然後到市委(外層函數)…,再到省委(模塊命名空間),最後到中央(builtin 命名空間)。
  4. 各個命名空間相互獨立,創建時間和生命周期各不相同。
  5. global 用於在函數內創建和修改全局變量。
  6. nonlocal 用於在內層函數修改外層函數局部變量。
  7. 沒有聲明 global 和 nonlocal,嘗試修改全局變量或外層函數局部變量,實際上隻會在函數或者內層函數創建一個新的局部變量,同名的全局變量或者外層函數局部變量不會受影響。

能夠看到這裡的都是一條漢子!

總結

到此這篇關於Python基礎教程之名稱空間以及作用域的文章就介紹到這瞭,更多相關Python名稱空間及作用域內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: