通俗的理解Python裝飾器所有用法(Decorator)

前言

Python的底層代碼,以及各種第三方框架中,你會看到各種各樣的@符號,沒錯,他就是Python的裝飾器語法糖。

Python裝飾器看起來類似Java中的註解,OC中的Aspect框架,亦或是理解爲OC中Runtime的Hook操作,然鵝只是看起來而已。Python是通過@語法糖裏面的閉包來實現,iOS是Runtime底層交換方法來實現,再不改原先邏輯的情況下,在方法之前嵌入自己的邏輯,例如日誌,統計,預處理,清理,校驗等場景。Django中底層代碼大量用到了裝飾器,廣泛應用於緩存、權限校驗(如django中的@login_required和@permission_required裝飾器)

用法

用法很簡單,就三個步驟:

  • 先定義一個裝飾函數(可以是類,可以是函數)
  • 在定義你的業務函數,或者類
  • 最後把裝飾器裝到你的業務函數頭上
    根據上面的三個步驟,看看裝飾器的所有用法

閉包

首先介紹下閉包,應該都懂,危機百科的解釋:

在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。

官方就是不說人話,需要通俗的來介紹下,其實就是OC中的Block,看看Python中的存在形式

# 外部包裹
def decration():
    para = 'I am closure'

    # 嵌套一層 形成閉包
    def wrapper():
        print(para)
    return wrapper

# 獲取一個閉包
closure =  decration()

# 執行
closure()

para參數是局部變量,在decration執行後就被回收了。但是嵌套函數引用了這個變量,將局部變量封閉在嵌套函數中,形成閉包。

閉包就是引用自由變量的函數,這個函數保存了執行的上下文,可以脫離原本的作用於存在。

01.入門用法(不帶參數)

單個裝飾器

def logger(func):
    # args 元祖 ()  kwargs 字典 {} 關鍵字參數
    def wrapper(*args, **kwargs):
        print('我正在進行計算: %s 函數:'%(func.__name__))
        print('args = {}'.format(*args))
        print('args is ', args)
        print('kwargs is ', kwargs)
        result = func(*args, **kwargs)
        print('搞定,晚飯加個雞蛋')
        return result

    return wrapper


@logger
def add(a, b, x = 0):
    print("%s + %s = %s" % (a, b, a + b))
    return a + b

@logger
def multpy(a, b, x = 0):
    print("%s * %s = %s" % (a, b, a * b))
    return a * b

print(add(100, 200,x = 1))

print("*"*30)

print(multpy(10, 200))

/Users/mikejing191/Desktop/Python3Demo/venv/bin/python /Users/mikejing191/Desktop/Python3Demo/Demo5.py
我正在進行計算: add 函數:
args = 100
args is  (100, 200)
kwargs is  {'x': 1}
100 + 200 = 300
搞定,晚飯加個雞蛋
300
******************************
我正在進行計算: multpy 函數:
args = 10
args is  (10, 200)
kwargs is  {}
10 * 200 = 2000
搞定,晚飯加個雞蛋
2000

對於初學者看到這個@語法會有些困擾,其實其實上面那段代碼與下面的調用方式一樣:

def add(a, b, x = 0):
    print("%s + %s = %s" % (a, b, a + b))
    return a + b

wrapper =  logger(add)
wrapper(100,200)

仔細看的話,其實原函數被裝飾後,比如這個add已經被替換成wrapper的地址了,這樣外部打印func.__name__就會變了,這種類似KVO,雖然被監聽了,但是Apple把對應的實現隱藏了,不會暴露出新增的類kvo_xxxx,而會重寫class方法返回原方法,這裏Python也類似,這裏下面會有一個方法來隱藏。

多個裝飾器

def logger(func):
    # args 元祖 ()  kwargs 字典 {} 關鍵字參數
    print('日誌裝飾器')
    def wrapper_log(*args, **kwargs):
        print('我正在進行日誌打印: %s 函數:'%(func.__name__))
        print('日誌args is ', args)
        print('日誌kwargs is ', kwargs)
        result = func(*args, **kwargs)
        print('日誌搞定,晚飯加個雞蛋')
        return result

    return wrapper_log


def statistics(func):
    print('統計裝飾器')
    def wrapper_static(*args, **kwargs):
        print('我正在進行統計: %s 函數:'%(func.__name__))
        print('統計args is ', args)
        print('統計kwargs is ', kwargs)
        result = func(*args, **kwargs)
        print('統計搞定,晚飯加個雞蛋')
        return result

    return wrapper_static


@logger
@statistics
def add(a, b):
    print("%s + %s = %s" % (a, b, a + b))
    return a + b


print(add(100, 200))


統計裝飾器
日誌裝飾器
我正在進行日誌打印: wrapper_static 函數:
日誌args is  (100, 200)
日誌kwargs is  {}
我正在進行統計: add 函數:
統計args is  (100, 200)
統計kwargs is  {}
100 + 200 = 300
統計搞定,晚飯加個雞蛋
日誌搞定,晚飯加個雞蛋
300

和上面的單個裝飾器類似,只是多疊加了一個,可以看到我們這裏的loggerstatics上面,按正常理解,先裝飾logger,再裝飾statics,但是Python這裏的規則是這樣的:

根據日誌分析下,首先編譯器遇到@logger和@statistics,這裏是會有代碼執行的,比如兩個裝飾器的第一句打印,是在裝飾器代碼執行到就調用,不需要調用被裝飾的函數。裝飾的前提是裝飾器的下一句代碼是方法函數,纔會裝飾,因此先跳過@logger,然後@statistics就會對func函數進行裝飾,因此先執行裝飾statistics,然後返回的值就是wrapper_static函數,再執行裝飾logger,執行的時候就是先執行裝飾logger裏面的inner函數,然後在執行裝飾2裏面的wrapper_log函數,好比一個東西,包裝的時候由內到外,執行的時候由外到內,這就是多層裝飾的邏輯

湊活看下畫了個抽象的圖,w1和w2分別代表logger和statistics,inner就是分別對應裝飾器裏面的閉包:
在這裏插入圖片描述
可以看到最終我們原函數的指針地址只想的是最外層logger的閉包函數地址。

注意點:這裏的閉包函數返回的都是閉包,要等函數實際調用的時候纔會觸發,但是有些寫法是不需要閉包的,比如Django中的Admin註冊,這就有點不同,他會在裝飾器執行到的時候直接觸發內部代碼,因此,你腦洞多大,裝飾器的功能就有多大

from .models import BlogType, Blog

@admin.register(BlogType)
class BlogTypeAdmin(admin.ModelAdmin):
    list_display = ('type_name',)

# 裝飾函數
def register(*models, site=None):
    """
    Register the given model(s) classes and wrapped ModelAdmin class with
    admin site:

    @register(Author)
    class AuthorAdmin(admin.ModelAdmin):
        pass

    The `site` kwarg is an admin site to use instead of the default admin site.
    """
    from django.contrib.admin import ModelAdmin
    from django.contrib.admin.sites import site as default_site, AdminSite

    def _model_admin_wrapper(admin_class):
        if not models:
            raise ValueError('At least one model must be passed to register.')

        admin_site = site or default_site

        if not isinstance(admin_site, AdminSite):
            raise ValueError('site must subclass AdminSite')

        if not issubclass(admin_class, ModelAdmin):
            raise ValueError('Wrapped class must subclass ModelAdmin.')

        admin_site.register(models, admin_class=admin_class)

        return admin_class
    return _model_admin_wrapper

02.進階用法(帶參數)

看完入門,應該對裝飾器有個大概的瞭解,不過是不能接受參數的裝飾器,這不搞笑呢,對應裝飾器,只能執行固定的邏輯,不能被參數控制,這是不能忍的,而且你看過其他項目,可以看到大部分裝飾器是帶有參數的。
那麼裝飾器的傳參如何實現,這個就需要多層嵌套了,看下實際案例:

def american():
    print("I am from America.")

def chinese():
    print("我來自中國。")

有個需求,給他們兩根據不同國家,自動加上打招呼的功能。


def say_hello(contry):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            if contry == 'china':
                print('你好!')
            elif contry == 'america':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@say_hello('america')
def american():
    print("I am from America.")


@say_hello('china')
def chinese():
    print("我來自中國。")


@say_hello('japanese')
def japanese():
    print('I am from jp')



american()
chinese()
japanese()

Hello!
I am from America.
你好!
我來自中國。

em。。。實屬牛逼。。。。。。。。

但是又有點懵逼,包了一層,內部的wrapperfunc是怎麼穿進去的?
其實去掉@語法,我們來恢復下調用邏輯:

def american():
    print("I am from America.")


decoration = say_hello('china')
wrapper =  decoration(american)
wrapper()

em。。。好像一點也不牛逼。。。。。。。。

裝飾器這一語法體現了Python中函數是第一公民,函數是對象、是變量,可以作爲參數、可以是返回值,非常的靈活與強大。

03.高階用法(不帶參數的類裝飾器)

以上是基於函數實現的裝飾器,在閱讀別人的代碼的時候,經常還能發現基於類實現的裝飾器。

__call__內置函數

絕大多數裝飾器都是基於函數和 閉包 實現的,但這並非製造裝飾器的唯一方式。事實上,Python 對某個對象是否能通過裝飾器( @decorator)形式使用只有一個要求:decorator 必須是一個“可被調用(callable)的對象。

class Foo():
    def __call__(self, *args, **kwargs):
        print('Hello Foo')

class Bar():
    pass

print(callable(Foo))
print(callable(Foo()))
print(callable(Bar))
print(callable(Bar()))
True
True
True
False

要實現基於類的裝飾器,必須理解__call__內置函數的作用。

import sys
class MKJ(object):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def __call__(self, *args, **kwargs):
        print('當前類名:%s'%self.__class__.__name__)
        print('當前函數名稱:%s'%sys._getframe().f_code.co_name)
        print('當前參數:',args)
        
m =  MKJ('mikejing')
m('Faker', 'Deft')

當前類名:MKJ
當前函數名稱:__call__
當前參數: ('Faker', 'Deft')

call()
官方定義:Called when the instance is “called” as a function; if this method is defined, x(arg1, arg2, …) is a shorthand for x.call(arg1, arg2, …).
它是在“實例被當成函數調用時”被調用。

舉個例子,實例如果是m = MKJ(),那麼,當你寫下m()的時候,該實例(即m)的創建者MKJ類(注意:此處提到的創建者既有可能是類,也有可能是元類)中的__call__()被調用。
如果這個實例是一個類,那麼它的創建者就是一個元類,如果這個實例是一個對象,那麼它的創建者就是一個類。

明白了__call__的用法,就可以實現最基本的不帶參數的類裝飾器,代碼如下:

import sys
class logger(object):
    def __init__(self, func):
        super().__init__()
        print('裝飾類開始')
        self.func = func

    def __call__(self, *args, **kwargs):
        print('當前類名:%s'%self.__class__.__name__)
        print('當前函數名稱:%s'%sys._getframe().f_code.co_name)
        print('裝飾函數名稱:%s' % self.func.__name__)
        print('當前參數:',args)
        self.func(*args, **kwargs)



@logger
def say(sm):
    print('say:%s'%sm)


say('hello!')

print(say) # <__main__.logger object at 0x108323160>

# 輸出如下
裝飾類開始
當前類名:logger
當前函數名稱:__call__
裝飾函數名稱:say
當前參數: ('hello!',)
say:hello!

說明:

  1. 當我們把logger類作爲裝飾器的時候,首先會默認創建logger的實例,可以試試先不調用say('hello),可以看到logger實例的__init__方法被調用,被裝飾的函數作爲參數被傳遞進來。func變量指向了say的函數體。
  2. 此時say函數相當於重新指向了logger創建出來的實例對象的地址
  3. 當調用say()的時候,就相當於調用這個對象類的__call__方法
  4. 爲了能夠在__call__中調用會原來say函數,所以在__init__中需要一個實例變量保存原函數的引用,所有才有了self.func = func,從而在__cal__中取出原函數地址和參數,進行回調

印證的話可以打開這個裝飾器關閉裝飾器打印一下say看下函數和對象的轉換

# 關閉
# @logger
def say(sm):
    print('say:%s'%sm)

print(say)
# 輸入如下
<function say at 0x1011b8268>

# 打開
@logger
def say(sm):
    print('say:%s'%sm)

print(say)
# 輸出如下
<__main__.logger object at 0x103233160>

04.高階用法(帶參數的類裝飾器)

還是用上面的logger函數,由於日誌可以分爲很多級別infowarningdebug等類型的日誌。這個時候就需要給類裝飾器傳入參數。回顧下函數裝飾器,對於傳參或者不傳參,只是外部在包一層與否,整體邏輯沒什麼變化,但是如果類裝飾器帶參數,就和不帶參就有很大不同了。

__init__:該方法不再接受裝飾函數,而是接受傳入參數。__call__:接受被裝飾函數,實現裝飾邏輯。

import sys
class logger(object):
    def __init__(self, level):
        super().__init__()
        print('裝飾類開始')
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print('[%s級別]--當前函數:'%(self.level),sys._getframe().f_code.co_name)
            print('[%s級別]--裝飾函數名稱:%s'%(self.level,func.__name__))
            print('[%s級別]--當前參數:'%(self.level), args)
            func(*args, **kwargs)
        return wrapper


@logger('WARNING')
def say(sm):
    print('say:%s'%sm)


say('Hello')

# 日誌如下
裝飾類開始
[WARNING級別]--當前函數: wrapper
[WARNING級別]--裝飾函數名稱:say
[WARNING級別]--當前參數: ('Hello',)
say:Hello

第二種帶參數的類裝飾器其實有點奇怪,__init__方法裏面沒有了func參數,其實按正常邏輯來看,理解起來其實不容易記憶,但是你強行記憶也行。em…

05.高階用法(偏函數和類實現)

絕大部分裝飾器都是基於函數和閉包來實現的,但是並非只此一種,看了上面的類裝飾器,我們來實現一個與衆不同,但是底層框架都大量使用的方式(類和偏函數實現),這種方式就是擴展的不帶參數類函數裝飾器。

import time
import functools
class DelayFunc:
    def __init__(self, durations, func):
        super().__init__()
        self.durations = durations
        self.func = func
        print('1111')

    def __call__(self, *args, **kwargs):
        print('please waite for %s seconds...'%self.durations)
        time.sleep(self.durations)
        return self.func(*args, **kwargs)


    def no_delay_call(self, *args, **kwargs):
        print('call immediately 。。。。')
        return self.func(*args, **kwargs)


def delay(durations):
    # Deley 裝飾器,推遲某個函數執行,同時提供no_delay_call不等待調用
    # 此處爲了避免額外函數,直接使用 functools.partial 幫助構造 具體參見另一個博客介紹,這裏的作用我會在下面簡單通俗介紹下
    return functools.partial(DelayFunc, durations)

@delay(3)
def add(a, b):
    return a + b

print(add(100,200))
# print(add.no_delay_call(200,300))

這裏涉及到一種俗稱偏函數的東西functools.partial,可以參見我的另一篇博客介紹,這裏簡單介紹下怎麼理解。首先定義了一個類DelayFunc,做成裝飾器的前提是callable也就是實現__call__方法,按不帶參數的類裝飾器,如果做成傳參形式,上面有介紹,需要改動正常的類參數,現在按照此種方式進行擴展。定義一個函數deley,我們把它當做裝飾器,類裝飾器裝飾其實把類實例化,可以看到deley函數返回的應該是一個類,這裏能看到用到了functools.partial,該方法先理解爲綁定DelayFunc類,暫時先綁定一個durations參數,那麼我們看到初始化方法裏面還有個參數是Func,這個就是我們最終裝飾的時候自帶的參數,所以當你看到以下使用的時候

@delay(3)
def add(a, b):
    return a + b

delay返回的綁定一半的類和參數,然後再傳輸add作爲func進行實例化,此時add指向的不再是簡單的函數地址,而是指向了新的類的實例。最終調用add(100,200)的時候執行__call__

至此,我們瞭解了函數裝飾器,類裝飾器的兩種實現,分別有帶參數和不帶參數的區別。最後一種是偏函數實現的類裝飾器,一共五種。

06.類裝飾器的優勢

那麼類裝飾器比函數裝飾器有哪些優勢:

  • 實現有狀態的裝飾器時,操作類屬性比操作閉包內變量更符合直覺、不易出錯

  • 實現爲函數擴充接口的裝飾器時,使用類包裝函數,比直接爲函數對象追加屬性更易於維護

  • 更容易實現一個同時兼容裝飾器與上下文管理器協議的對象(參考 unitest.mock.patch)

07.可選優化裝飾器(使用 wrapt 第三方模塊編寫更扁平的裝飾器)

  1. 實現帶參數的裝飾器時,層層嵌套的函數代碼特別難寫、難讀
  2. 因爲函數和類方法的不同,爲前者寫的裝飾器經常沒法直接套用在後者上

看一下生成隨機數注入爲函數參數的裝飾器

import random


def random_number(min_num, max_num):
    def wrapper(func):
        def decorated(*args, **kwargs):
            num = random.randint(min_num, max_num)
            return func(num, *args, **kwargs)

        return decorated

    return wrapper


@random_number(0, 99)
def print_number(num):
    print(num)


print_number()

@random_number 裝飾器功能看上去很不錯,但它有着我在前面提到的兩個問題:嵌套層級深、無法在類方法上使用。如果直接用它去裝飾類方法,會出現下面的情況:

class Foo:
    @random_number(0, 99)
    def print_number(self, num):
        print(num)


print_number()
<__main__.Foo object at 0x10bdc12b0>

Foo類實例中的 print_number方法將會輸出類實例 self ,而不是我們期望的隨機數 num。

之所以會出現這個結果,是因爲類方法(method)和函數(function)二者在工作機制上有着細微不同。如果要修復這個問題, random_number 裝飾器在修改類方法的位置參數時,必須聰明的跳過藏在 *args 裏面的類實例 self 變量,才能正確的將 num 作爲第一個參數注入。

這時,就應該是 wrapt模塊閃亮登場的時候了。 wrapt 模塊是一個專門幫助你編寫裝飾器的工具庫。利用它,我們可以非常方便的改造 random_number 裝飾器,完美解決“嵌套層級深”和“無法通用”兩個問題,

import random
import wrapt


def random_number(min_num, max_num):
    @wrapt.decorator
    def wrapper(wrapperd, instance, args, kwargs):
        # 參數含義:
        # - wrapped:被裝飾的函數或類方法
        # - instance:
        #   - 如果被裝飾者爲普通類方法,該值爲類實例
        #   - 如果被裝飾者爲 classmethod 類方法,該值爲類
        #   - 如果被裝飾者爲類/函數/靜態方法,該值爲 None

        # - args:調用時的位置參數(注意沒有 * 符號)
        # - kwargs:調用時的關鍵字參數(注意沒有 ** 符號)
        num = random.randint(min_num, max_num)
        # 無需關注 wrapped 是類方法或普通函數,直接在頭部追加參數
        args = (num, ) + args
        return wrapperd(*args, **kwargs)
    return wrapper


@random_number(0, 99)
def print_number(num):
    print(num)


class Foo:
    @random_number(0, 99)
    def print_number(self, num):
        print(num)


print_number()


Foo().print_number()

這就是使用了wrapt後的有點,如果不習慣,還是使用上述的一些裝飾器即可

  1. 嵌套層級少:使用 @wrapt.decorator 可以將兩層嵌套減少爲一層
  2. 更簡單:處理位置與關鍵字參數時,可以忽略類實例等特殊情況
  3. 更靈活:針對 instance 值進行條件判斷後,更容易讓裝飾器變得通用

08.裝飾類的裝飾器

Python中單例的實現,有一種就是用單例實現的

instances = {}
def singleton(cls):
	def get_instance(*args, **kw):
		cls_name = cls.__name__
		print('===== 1 ====')
		if not cls_name in instances:
			print('===== 2 ====')
			instance = cls(*args, **kw)
			instances[cls_name] = instance
		return instances[cls_name]
	return get_instance

@singleton
class User:
	_instance = None
	def __init__(self, name):
		print('===== 3 ====')
		self.name = name
u1 = User('mkj1')
u1.age = 100
u2 = User('mkj2')
print(u1 == u2)
print(u2.age)
# 日誌如下
===== 1 ====
===== 2 ====
===== 3 ====
===== 1 ====
True
100

09.wraps 裝飾器有啥用

上面介紹了多種裝飾器,而且還引入了functools庫,除了用到partitial函數,還有個裝飾器wraps,看看到底有啥用。


def say_hello(contry):
    def wrapper(func):

        def inner_wrapper(*args, **kwargs):
            if contry == 'china':
                print('你好!')
            elif contry == 'america':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@say_hello('china')
def american():
    print("I am from America.")


print(american.__name__)
american()

# 打印日誌
inner_wrapper
你好!
I am from America.

可以看到,按我們上面的分析,其實american已經不再指向原來的函數地址,因此打印出來的名字也變了。理論上沒什麼問題,但是有時候你定位Bug的時候會很噁心,因此,我們會看到大量的庫用到了系統提供的wraps裝飾器

import functools

def say_hello(contry):
    def wrapper(func):
        @functools.wraps(func)
        def inner_wrapper(*args, **kwargs):
            if contry == 'china':
                print('你好!')
            elif contry == 'america':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@say_hello('china')
def american():
    print("I am from America.")


print(american.__name__)
american()
# 打印如下
american
你好!
I am from America.

方法是使用 functools .wraps 裝飾器,它的作用就是將 被修飾的函數(american) 的一些屬性值賦值給 修飾器函數(inner_wrapper) ,最終讓屬性的顯示更符合我們的直覺。

準確的來看,functools .wraps也是一個偏函數對象partial

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

可以看到該裝飾器也是和我們上面演示的一樣,使用了partial偏函數,其中綁定的類或者方法是update_wrapper,其中該方法實際上接收四個參數,這裏我們傳了三個,用作裝飾器,默認會把第四個參數,被裝飾的函數inner_wrapper作爲wrapper首參數進行裝飾初始化。

wrapper.__wrapped__ = wrapped

底層實現中會把原函數的屬性全部賦值給修飾器函數inner_wrapper,最終調用__name__的時候,雖然指針被已經指向被裝飾的函數,但是通過再次裝飾,屬性會被原函數一樣打印出來。

10.裝飾器實戰

1.裝飾器使我們的代碼可讀性更高
2.代碼結構更加清晰,代碼冗餘降低

下面是一個實現控制函數運行超時的裝飾器,如果超時,就會拋出異常。

import signal
import functools

class TimeoutException(Exception):
    def __init__(self, error='Timeout waiting for response from Cloud'):
        Exception.__init__(self, error)


def timeout_limit(timeout_time):
    def wraps(func):

        def handler(signum, frame):
            raise TimeoutException()

        @functools.wraps(func)
        def deco(*args, **kwargs):
            signal.signal(signal.SIGALRM, handler)
            signal.alarm(timeout_time)
            return func(*args, **kwargs)
            signal.alarm(0)
        return deco
    return wraps


@timeout_limit(1)
def add(x):
    r = 0
    for a in range(0, x):
        r += a
    return r

print(add.__name__)
print(add(10))

print(add(100000000))

該功能可以看到執行add(10)的時候正常輸出,但是執行add(10000000)的時候由於超時,就會拋出異常崩潰,實現了我們給函數進行裝飾的功能。

總結:

  1. 裝飾器實現方式很多種,除了常用的函數+閉包,也可以用類來裝飾,概括爲一切callable的對象都可以被用來實現裝飾器
  2. 混合使用函數和類,能更好的實現裝飾器
  3. 裝飾器只是一種語法糖,你也可以自己拆開來一步步寫,它不是裝飾器模式。
  4. 裝飾器會改變原函數的所有信息(例如簽名),我們需要functools.wraps來進行交換回去
  5. 類裝飾器帶參數的可以用偏函數partial來實現更優雅的方案,推薦使用

參考文獻:
Python 工匠:使用裝飾器的技巧
搞懂裝飾器所有用法
Python中*args 和**kwargs的用法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章