Python函數高級(命名空間、作用域、裝飾器)

一、名稱空間和作用域

1、命名空間(Namespace)

命名空間是從名稱到對象的映射,大部分的命名空間都是通過 Python 字典來實現的。

命名空間提供瞭在項目中避免名字沖突的一種方法。各個命名空間是獨立的,沒有任何關系的,所以一個命名空間中不能有重名,但不同的命名空間是可以重名而沒有任何影響。

1、一般有三種命名空間:

  • 內置名稱空間(built-in names):存放內置的名字,如len/eval/enumerate/bytes/max/min/sorted/map/filter....
  • 全局名稱空間(global names):模塊中定義的名稱,記錄瞭模塊的變量,包括函數、類、其它導入的模塊、模塊級的變量和常量。
  • 局部名稱空間(local names):函數內部的名字都是局部名稱空間,不同函數內部的名字互不幹涉。

2、命名空間查找順序:

如果找不到變量 runoob,它將放棄查找並引發一個 NameError 異常:

NameError: name 'runoob' is not defined。
  • 查找順序:假設我們要使用變量 runoob,則 Python 的查找順序為:局部的命名空間去 -> 全局命名空間 -> 內置命名空間
  • 執行順序:先內置(Python解釋器啟動的時候才會生成)-> 全局(文件執行的時候才會生成)-> 局部(函數調用的時候才會生成)

3、命名空間的生命周期:

命名空間的生命周期取決於對象的作用域,如果對象執行完成,則該命名空間的生命周期就結束。

因此,我們無法從外部命名空間訪問內部命名空間的對象。

如下圖所示,相同的對象名稱可以存在於多個命名空間中。

2、作用域:

作用域就是一個 Python 程序可以直接訪問命名空間的正文區域。

全局名稱空間和局部名稱空間中可能會存在名字相同的變量,但是這兩個變量互不影響。

Python 中,程序的變量並不是在哪個位置都可以訪問的,訪問權限決定於這個變量是在哪裡賦值的。

變量的作用域決定瞭在哪一部分程序可以訪問哪個特定的變量名稱。

Python的作用域一共有4種,分別是:

  • L(Local):最內層,包含局部變量,比如一個函數/方法內部。
  • E(Enclosing):包含瞭非局部(non-local)也非全局(non-global)的變量。比如兩個嵌套函數,一個函數(或類) A 裡面又包含瞭一個函數 B ,那麼對於 B 中的名稱來說 A 中的作用域就為 nonlocal。
  • G(Global):當前腳本的最外層,比如當前模塊的全局變量。
  • B(Built-in): 包含瞭內建的變量/關鍵字等。,最後被搜索

對於變量作用域,變量的訪問以: L –> E –> G –>B 的 規則查找。

在局部找不到,便會去局部外的局部找(例如閉包),再找不到就會去全局找,再者去內置中找。

舉例:

x = 1

def func():
    print(x)  #10

x = 10
func()

內置作用域是通過一個名為 builtin 的標準模塊來實現的,但是這個變量名自身並沒有放入內置作用域內,所以必須導入這個文件才能夠使用它。

在Python3.0中,可以使用以下的代碼來查看到底預定義瞭哪些變量:

import builtins
print(dir(builtins))

Python 中隻有模塊(module),類(class)以及函數(def、lambda)才會引入新的作用域,其它的代碼塊(如 if/elif/else/、try/except、for/while等)是不會引入新的作用域的,也就是說這些語句內定義的變量,外部也可以訪問,

如下代碼:實例中 msg 變量定義在 if 語句塊中,但外部還是可以訪問的。如果將 msg 定義在函數中,則它就是局部變量,外部不能訪問。

if True:
    msg = 'I am from Runoob'
print(msg)
# 'I am from Runoob'

3、全局變量和局部變量

定義在函數內部的變量擁有一個局部作用域,定義在函數外的擁有全局作用域。

局部變量隻能在其被聲明的函數內部訪問,而全局變量可以在整個程序范圍內訪問。調用函數時,所有在函數內聲明的變量名稱都將被加入到作用域中。

# 作用域註意點
x = 1

def f1():  # 定義階段x=1
    print(x)  #1

def f2():
    x = 2  #此x為f2函數的局部變量,f1無法直接訪問
    f1()

f2()

4、函數對象+作用域應用

def f1():
    def inner():
        print('from inner')
    return inner

f = f1()  # from inner   。把局部定義的函數inner()放在全局之中

def bar():
    f()

bar()

5、global關鍵字修改全局作用域中的變量

函數內可以訪問全局變量,但不能直接更新(修改)其值,可以加上 global 引用以更新變量值 :

x = 1

def f1():
    x = 2

    def f2():
        global x  # 修改全局
        x = 3

    f2()

f1()
print(x)  # 3

6、nonlocal關鍵字修改嵌套作用域中的變量。

如果要修改嵌套作用域(enclosing 作用域,外層非全局作用域)中的變量則需要 nonlocal 關鍵字瞭

x = 1

def f1():
    x = 2

    def f2():
        nonlocal x
        x = 3

    f2()
    print(x)  # 3

f1()

二、閉包函數

閉包:閉是封閉(函數內部函數),包是包含(該內部函數對外部作用域而非全局作用域的變量的引用)。

閉包指的是:函數內部函數對外部作用域而非全局作用域的引用。

def outter(x):
    x = 1

    def inner():
        print(x)

    return inner #返回的是函數名(函數對象)


f = outter(2)

f()  # 1
f()  # 1
f()  # 1
# 查看閉包的元素
print(f.__closure__[0].cell_contents)  # 1

閉包的意義:返回的函數對象,不僅僅是一個函數對象,在該函數外還包裹瞭一層作用域,這使得,該函數無論在何處調用,優先使用自己外層包裹的作用域。

應用領域:

延遲計算(原來我們是傳參,現在我們是包起來)、爬蟲領域。

import requests


def outter(url):
    def get():
        response = requests.get(url)
        print(f"done: {url}")

    return get


baidu = outter('https://www.baidu.com')
python = outter('https://www.python.org')

baidu()
baidu()

python()
python()

三、函數裝飾器

裝飾器指的是為被裝飾器對象添加額外功能。因此定義裝飾器就是定義一個函數,隻不過該函數的功能是用來為其他函數添加額外的功能。裝飾器的實現必須遵循兩大原則:

  • 不修改被裝飾對象的源代碼
  • 不修改被裝飾對象的調用方式

裝飾器其實就是在遵循以上兩個原則的前提下為被裝飾對象添加新功能。

不改變函數體代碼,並且不改變函數調用方式,它本質就是一個閉包函數。

def f1(x):
    def f2():
        print(x)  # 10
    return f2

f2 = f1()
f2()  # f2()

在不改變當前函數的情況下, 給其增加新的功能:

def log(pr):  # 將被裝飾函數傳入
    def wrapper():
        print("**********")
        return pr()  # 執行被裝飾的函數

    return wrapper  # 將裝飾完之後的函數返回(返回的是函數名)


@log
def pr():
    print("我是小小洋")


pr()

# **********
# 我是小小洋

回調函數和返回函數的實例就是裝飾器。

四、無參裝飾器

舉例:

import time


def index():
    print('welcome to index')
    time.sleep(1)


def time_count(func):
    # func = 最原始的index
    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print(f"{func} time is {start - end}")  #  time is -1.0038220882415771

    return wrapper


index = time_count(index)  # index為被裝飾函數index的內存地址,即index = wrapper
index()  # wrapper()

1、被裝飾函數有返回值:

如果原始的被裝飾函數index()有返回值的時候,wrapper()函數的返回值應該和index()的返回值相同,也就是說,我們需要同步原始的index()和wrapper()方法的返回值。

import time


def index():
    print('welcome to index')
    time.sleep(1)
    return 123


def time_count(func):
    # func = 最原始的index
    def wrapper():
        start = time.time()
        res1 = func()
        end = time.time()
        print(f"{func} time is {start - end}")  #  time is -1.0050289630889893
        return res1

    return wrapper


index = time_count(index)
res = index()
print(f"res: {res}")  #
res: 123

2、被裝飾函數需要傳參:

如果原始的被裝飾函數index()方法需要傳參,那麼我們之前的裝飾器是無法實現該功能的,由於有wrapper()=index(),所以給wrapper()方法傳參即可。

import time


def index():
    print('welcome to index')
    time.sleep(1)
    return 123


def home(name):
    print(f"welcome {name} to home page")
    time.sleep(1)
    return name


def time_count(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        end = time.time()
        print(f"{func} time is {start-end}") #  time is -1.0039079189300537
        return res

    return wrapper


home = time_count(home)

res = home('egon')
print(f"res: {res}") #res: egon

3、裝飾器模板

def deco(func):
    def wrapper(*args,**kwargs):
        res = func(*args,**kwargs)
        return res
    return wrapper

4、裝飾器語法糖:

在被裝飾函數正上方,並且是單獨一行寫上@裝飾器名

import time


def time_count(func): #裝飾器
    # func = 最原始的index
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        end = time.time()
        print(f"{func} time is {start-end}") # time is -1.0005171298980713
        return res

    return wrapper


@time_count  # home = time_count(home)
def home(name):
    print(f"welcome {name} to home page") #welcome egon to home page
    time.sleep(1)
    return name


res = home('egon')
print(f"res: {res}") #res: egon

五、帶參數的裝飾器

註意無參裝飾器隻套兩層。

import time

current_user = {'username': None}


def login(func):
    # func = 最原始的index
    def wrapper(*args, **kwargs):
        if current_user['username']:
            res1 = func(*args, **kwargs)
            return res1

        user = input('username: ').strip()
        pwd = input('password: ').strip()

        if user == 'nick' and pwd == '123':
            print('login successful')
            current_user['username'] = user
            res1 = func(*args, **kwargs)
            return res1
        else:
            print('user or password error')

    return wrapper


@login
def index():
    print('welcome to index')
    time.sleep(1)


res = index()

#username: nick 
#password: 123 
#login successful 
#welcome to index

我們首先看看三層閉包怎麼運用。

def f1(y):
    def f2():
        x = 1

        def f3():
            print(f"x: {x}")  # x: 1
            print(f"y: {y}")  # x: 1

        return f3
    return f2


f2 = f1(2)
f3 = f2()
f3()

3、有參三層裝飾器:

在函數中嵌入裝飾器

import time

current_user = {'username': None}


def auth(engine='file'):
    def login(func):
        def wrapper(*args, **kwargs):
            if current_user['username']:
                res = func(*args, **kwargs)
                return res

            user = input('username: ').strip()
            pwd = input('password: ').strip()

            if engine == 'file':
                print('base of file')
                if user == 'nick' and pwd == '123':
                    print('login successful')
                    current_user['username'] = user
                    res = func(*args, **kwargs)
                    return res
                else:
                    print('user or password error')
            elif engine == 'mysql':
                print('base of mysql, please base of file')
        return wrapper
    return login


@auth(engine='file')
def index():
    print('welcome to index')
    time.sleep(1)


res = index()

username: nick 
password: 123 
base of file 
login successful 
welcome to index

六、類裝飾器

沒錯,裝飾器不僅可以是函數,還可以是類,相比函數裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使用類裝飾器主要依靠類的__call__方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')

@Foo
def bar():
    print ('bar')

bar()

functools.wraps

使用裝飾器極大地復用瞭代碼,但是他有一個缺點就是原函數的元信息不見瞭,比如函數的docstring、__name__、參數列表,先看例子:

# 裝飾器
def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__      # 輸出 'with_logging'
        print func.__doc__       # 輸出 None
        return func(*args, **kwargs)
    return with_logging

# 函數
@logged
def f(x):
   """does some math"""
   return x + x * x

logged(f)

不難發現,函數 f 被with_logging取代瞭,當然它的docstring,__name__就是變成瞭with_logging函數的信息瞭。好在我們有functools.wraps,wraps本身也是一個裝飾器,它能把原函數的元信息拷貝到裝飾器裡面的 func 函數中,這使得裝飾器裡面的 func 函數也有和原函數 foo 一樣的元信息瞭。

from functools import wraps

def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 輸出 'f'
        print func.__doc__       # 輸出 'does some math'
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

七、裝飾器順序

一個函數還可以同時定義多個裝飾器,比如:

@a
@b
@c
def f ():
    pass

它的執行順序是從裡到外,最先調用最裡層的裝飾器,最後調用最外層的裝飾器,它等效於

f = a(b(c(f)))

八、裝飾器使用場景

現在我們來看一下裝飾器在哪些地方特別耀眼,以及使用它可以讓一些事情管理起來變得更簡單。

授權(Authorization)

裝飾器能有助於檢查某個人是否被授權去使用一個web應用的端點(endpoint)。它們被大量使用於Flask和Django web框架中。這裡是一個例子來使用基於裝飾器的授權:

from functools import wraps
 
def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated

日志(Logging)

日志是裝飾器運用的另一個亮點。這是個例子:

from functools import wraps
 
def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging
 
@logit
def addition_func(x):
   """Do some math."""
   return x + x
 
 
result = addition_func(4)
# Output: addition_func was called

到此這篇關於Python函數高級用法的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: