# VBA: 用戶自定義函數 (UDFs)
本教程將幫助您快速了解如何編寫用戶自定義函數。
>[info]注意
·UDF目前僅在Windows上可用。
·有關如何控制參數行為和返回值的詳細信息,請查看[轉換器和選項](converters.md)。
·有關可用裝飾器及其選項的全面概述,請查看相應的API文檔:[api](api.md)。
## 一次性Excel準備
1) 在`文件>選項>信任中心>信任中心設置>宏設置`下啟用`信任訪問VBA項目對象模型`
2. 通過命令提示符安裝加載項:`xlwings addin install`(參見[加載項](addin.md))。
## 工作簿準備
啟動新項目的最簡單方法是在命令提示符下運行`xlwings quickstart myproject`(請參閱[命令行客戶端](command_line.md))。 這會自動將xlwings引用添加到生成的工作簿。
## 一個簡單的UDF
默認的插件設置需要一個Python源文件,其方式與`quickstart`一樣:
* 在與Excel文件相同的目錄中
* 與Excel文件同名,但使用`.py`結尾而不是`.xlsm`。
或者,您可以通過xlwings功能區中的`UDF Modules`指向特定模塊。
假設您有一個工作簿`myproject.xlsm`,那么您可以在`myproject.py`中編寫以下代碼:
~~~
import xlwings as xw
@xw.func
def double_sum(x, y):
"""返回兩個參數之和的兩倍"""
return 2 * (x + y)
~~~
* 現在單擊xlwings選項卡中的`Import Python UDFs`以獲取對`myproject.py`所做的更改。
* 在單元格中輸入公式`= double_sum(1,2)`,您將看到正確的結果:

* 文檔字符串(三引號)將在Excel中顯示為函數描述。
>[info]注意
·如果更改函數參數或函數名稱,則只需重新導入函數。
·自動拾取實際函數中的代碼更改(即,在下一次計算公式時,例如,由`Ctrl-Alt-F9`觸發),但導入的模塊中的更改不會。 這是Python導入工作方式的行為。 如果要確保所有內容都處于新鮮狀態,請單擊`Restart UDF Server`。
·當函數導入Excel時,`@xw.func`裝飾器僅由xlwings使用。 它告訴xlwings它應該創建一個VBA包裝函數的函數,否則它對函數在Python中的行為沒有影響。
## 數組公式:提高效率
在Excel中調用一個大數組公式比調用許多單元格公式更有效,因此使用它們通常是個好主意,特別是如果遇到性能問題。
您可以將Excel范圍作為函數參數傳遞,而不是單個單元格,它將以列表的形式顯示在Python中。
例如,可以編寫以下函數,將1添加到Range內的每個單元格:
~~~
@xw.func
def add_one(data):
return [[cell + 1 for cell in row] for row in data]
~~~
要在Excel中使用此公式,
* 再次單擊`Import Python UDFs`
* 填寫`A1:B2`范圍內的值
* 選擇范圍`D1:E2`
* 輸入公式`=add_one(A1:B2)`
* 按`Ctrl + Shift + Enter`創建一個數組公式。 如果您正確地完成了所有操作,您將看到括號括起來的公式,如此屏幕截圖所示:

### 數組維數:ndim
上面的公式存在它期望“二維”輸入的問題,例如 一個`[[1, 2], [3, 4]]`形式的嵌套列表。 因此,如果將公式應用于單個單元格,則會出現以下錯誤:`TypeError: 'float' object is not iterable`('float'對象不可迭代)。
要強制Excel始終為您提供二維數組,無論參數是單個單元格,列/行還是二維范圍,您都可以像這樣擴展上面的公式:
~~~
@xw.func
@xw.arg('data', ndim=2)
def add_one(data):
return [[cell + 1 for cell in row] for row in data]
~~~
## 使用NumPy和Pandas的數組公式
通常,您需要在UDF中使用NumPy數組或Pandas DataFrame,因為這可以釋放Python用于科學計算的生態系統的全部功能。
要使用numpy數組定義矩陣乘法的公式,您可以定義以下函數:
~~~
import xlwings as xw
import numpy as np
@xw.func
@xw.arg('x', np.array, ndim=2)
@xw.arg('y', np.array, ndim=2)
def matrix_mult(x, y):
return x @ y
~~~
>[info]注意
如果你沒有使用NumPy> = 1.10的Python> = 3.5,請使用`x.dot(y)`而不是`x @ y`。
一個很好的例子,說明如何讓Pandas工作就是創建一個基于數組的`CORREL`公式。 Excel的`CORREL`版本僅適用于2個數據集,如果您想快速獲取幾個時間序列的相關矩陣,則使用起來很麻煩。 Pandas基于數組創建了一個基于數組的`CORREL2`公式:
~~~
import xlwings as xw
import pandas as pd
@xw.func
@xw.arg('x', pd.DataFrame, index=False, header=False)
@xw.ret(index=False, header=False)
def CORREL2(x):
"""與CORREL類似,但作為2個以上數據集的數組公式"""
return x.corr()
~~~
## @xw.arg 和@xw.ret 修飾
這些修飾符對UDF的作用就像`options`方法對`Range`對象的作用一樣:它們允許您將轉換器及其選項應用于函數參數(`@xw.arg`)和返回值(`@xw.ret`)。例如,要將參數`x`轉換為pandas DataFrame并在返回時抑制索引,請執行以下操作:
~~~
@xw.func
@xw.arg('x', pd.DataFrame)
@xw.ret(index=False)
def myfunction(x):
# x is a DataFrame, do something with it
return x
~~~
有關詳細信息,請參閱[轉換器和選項](converters.md)。
## 動態數組公式
>[info]注意
如果您的Excel版本支持新的原生動態數組,那么您不必執行任何特殊操作,也不應使用`expand`裝飾器! 要檢查您的Excel版本是否支持它,請查看是否有`= UNIQUE()`公式。 原生動態數組在2018年9月底在Office 365 Insider Fast中引入。
如上圖所示,要使用Excel的數組公式,您需要先選擇結果數組,然后輸入公式,最后單擊`Ctrl-Shift-Enter`來指定其前面的維度。實際上,這通常是一個很麻煩的過程,尤其是在處理動態數組(如時間序列數據)時。自v0.10以來,XLwings提供動態UDF擴展:
這是一個演示UDF擴展的語法和效果的簡單示例:
~~~
import numpy as np
@xw.func
@xw.ret(expand='table')
def dynamic_array(r, c):
return np.random.randn(int(r), int(c))
~~~


>[info]Note
·擴展數組公式將在不提示的情況下覆蓋單元格
·Pre v0.15.0不允許將volatile函數作為參數,例如 你不能使用像`= TODAY()`這樣的函數作為參數。 從v0.15.0開始,您可以使用volatile函數作為輸入,但UDF將被調用超過1次。
·動態數組已使用v0.15.0進行了重構,以便成為正確的舊數組:要編輯xlwings大于等于v0.15.0的動態數組,需要在左上角單元格中單擊`ctrl-shift-enter`。請注意,當您第一次輸入公式時,不必這樣做。
## Docstring文檔字符串
下面的示例演示如何為函數和參數x和y包括文檔字符串docstring,然后這些參數將顯示在Excel的函數向導中:
~~~
import xlwings as xw
@xw.func
@xw.arg('x', doc='This is x.')
@xw.arg('y', doc='This is y.')
def double_sum(x, y):
"""Returns twice the sum of the two arguments"""
return 2 * (x + y)
~~~
## “vba”關鍵字
獲取調用單元格的地址通常是有幫助的。現在,最簡單的方法之一就是使用`vba`關鍵字。實際上,`vba`允許您訪問任何可用的`vba`表達式,例如`application`。但是,請注意,當前您正直接處理pywin32 com對象:
~~~
@xw.func
@xw.arg('xl_app', vba='Application')
def get_caller_address(xl_app):
return xl_app.Caller.Address
~~~
## 宏
在Windows上,作為通過[VBA: RunPython](vba.md)調用宏的替代方法,您還可以使用`@xw.sub`裝飾器:
~~~
import xlwings as xw
@xw.sub
def my_macro():
"""將工作簿的名稱寫入工作表1的(A1)Range"""
wb = xw.Book.caller()
wb.sheets[0].range('A1').value = wb.name
~~~
單擊`Import Python UDFs`后,您可以通過`Alt + F8`執行此宏或通過綁定它來使用此宏。 到一個按鈕。 對于后者,請確保在`文件>選項>自定義功能區`下選擇`開發工具`選項卡。 然后,在`開發工具`選項卡下,您可以通過`插入>表單控件`插入一個按鈕。 繪制按鈕后,系統將提示您為其指定一個宏,您可以選擇“my_macro”。
## 從VBA調用UDF
也可以從VBA使用導入的函數。 例如,對于返回2維數組的函數:
~~~
Sub MySub()
Dim arr() As Variant
Dim i As Long, j As Long
arr = my_imported_function(...)
For j = LBound(arr, 2) To UBound(arr, 2)
For i = LBound(arr, 1) To UBound(arr, 1)
Debug.Print "(" & i & "," & j & ")", arr(i, j)
Next i
Next j
End Sub
~~~
## 異步UDF
新版本v0.14.0。
xlwings提供了一種在Excel中編寫異步函數的簡便方法。 異步函數立即返回`#N / A waiting ...`。 當函數正在等待其返回值時,您可以使用Excel執行其他操作,并且只要返回值可用,就會更新單元格值。
唯一可用的模式是`async_mode ='threading'`,這意味著它對I/O綁定任務很有用,例如當您通過Web從API獲取數據時。
只需在函數裝飾器中賦予它相應的參數,就可以使函數異步。 在此示例中,使用`time.sleep`模擬耗時的I/O綁定任務:
~~~
import xlwings as xw
import time
@xw.func(async_mode='threading')
def myfunction(a):
time.sleep(5) # long running tasks
return a
~~~
您可以像使用任何其他xlwings函數一樣使用此函數,只需將`=myfunction("abcd")`放入一個單元格(在導入該函數后,將其關閉)。
請注意,xlwings不使用Excel 2010中引入的原生異步函數,因此任何版本的Excel都支持xlwings異步函數。