同步發表於個人網站
_G
和_ENV
在Lua有兩個特殊變量--_G
和_ENV
,其分別表示全局環境和當前環境。_G
在與C交互時,另有作用。但大致上你可以將兩者視為相同。實際上,在Lua環境建立之初,兩著也確實是相同的:
print(_G == _ENV) -- => Output: true
先前說過,Lua只有表(table
)這個複合結構。而_G
和_ENV
也是table
結構。_ENV
中包含一個_G
的key
,其值指向_G
,而起出_G
就是_ENV
。像是一個咬尾蛇(Ouroboros)
print(_ENV._G == _ENV) -- => Output: true
print(_G == _ENV) -- => Output: true
print(_G._G == _ENV) -- => Output: true
print(_G._G == _G) -- => Output: true
對於一個變數,Lua會先嘗試從當前詞法環境(Lexical)尋找,在從當前環境中尋找(_ENV
)。這意味著你可以準備一個乾淨的執行環境執行函數。
s = "outer"
print(s)
do
local _ENV = {
print = print -- import print
}
s = "inner"
print(s)
end
print(s)
outer
inner
outer
在do-end
的區塊,雖然s
並未宣告為local
卻不影響到區塊外部的s
。所有不是宣告為local
的變數,都會出現在_ENV
,上面程式等價於:
_ENV.s = "outer"
print(_ENV.s)
do
local _ENV = {
print = print -- import print
}
_ENV.s = "inner"
_ENV.print(_ENV.s)
end
print(_ENV.s)
上面程式,新環境指引入了print
的能力。透過類似方式,可以用於限制函數的執行,避免執行危險的片段。像是:
function writeHello()
local f<close> = io.open("hello.txt", "w")
f:write("Hello, World")
end
writeHello() -- 可以正確執行
do
local _E = {}
------ Copy _ENV ------
for k, v in pairs(_ENV) do
_E[k] = v
end
local _ENV = _E -- cover _ENV, create new environment
------ 黑名單 ---------
_ENV._G = nil -- 禁用全局環境
_ENV.io = nil -- 禁止檔案讀寫
_ENV.os = nil -- 禁止作業系統相關操作
_ENV.load = nil -- 禁止eval
_ENV.loadfile = nil -- 禁止載入檔案
_ENV.require = nil -- 禁止 require操作
_ENV.dofile = nil -- 禁止執行檔案
_ENV.writeHello = nil -- 禁用global的writeHello
------ 執行writeHello -------
local function writeHello()
local f<close> = io.open("hello.txt", "w") -- Error: 禁用`io`
f:write("Hello, World")
end
writeHello() -- Error: 禁止檔案讀寫
end
不過你無法對於已經建立好的函式,限制其執行能力:
早期Lua 5.1確實有些辦法,包裹一個禁用部份能力的函數。不過現在得繞點路,但很好理解。
function writeHello()
local f<close> = io.open("hello.txt", "w")
f:write("Hello, World")
end
writeHello() -- 可以正確執行
do
local _E = {}
------ Copy _ENV ------
for k, v in pairs(_ENV) do
_E[k] = v
end
local _ENV = _E -- cover _ENV, create new environment
------ 黑名單 ---------
_ENV._G = nil -- 禁用全局環境
_ENV.io = nil -- 禁止檔案讀寫
_ENV.os = nil -- 禁止作業系統相關操作
_ENV.load = nil -- 禁止eval
_ENV.loadfile = nil -- 禁止載入檔案
_ENV.require = nil -- 禁止 require操作
_ENV.dofile = nil -- 禁止執行檔案
writeHello() -- 仍然可以執行成功
end
早期Lua 5.1確實有些辦法,包裹一個禁用部份能力的函數。不過現在得繞點路,但很好理解。
function writeHello()
local f<close> = io.open("hello.txt", "w")
f:write("Hello, World")
end
writeHello() -- 可以正確執行
do -- 準備一個乾淨6環境的區塊
local _ENV = {
_G = _ENV, -- 引入全局環境
pairs = pairs,
print = print,
writeHello = writeHello, -- 受限制的函數
}
-- 備份全局環境
_E = {}
for k, v in pairs(_G) do
_E[k] = _G[k]
end
------ 全局環境黑名單 ---------
_G._G = nil -- 禁用全局環境
_G.io = nil -- 禁止檔案讀寫
_G.os = nil -- 禁止作業系統相關操作
_G.load = nil -- 禁止eval
_G.loadfile = nil -- 禁止載入檔案
_G.require = nil -- 禁止 require操作
_G.dofile = nil -- 禁止執行檔案
do
local _ENV = _G --[[cover _ENV
此區塊同樣禁止使用而外function
還有一大意義是禁止透過外部的_EVN去存取_E,這個全局環境的備份。
--]]
print("---受限制的區塊: 開始--")
local successp = pcall(writeHello) -- 保護模式下執行writeHello
if successp then
successp = "執行成功"
else
successp = "執行失敗"
end
print("writeHello(): "..successp)
print("_G:", _G) -- 以禁止存取全局變數 Output: nil
print("_E", _E) -- 以禁止存取_E,以做到保護效果 Output: nil
new_global_var = "新的全局變數" -- 仍然允許其新增新的全局變數。
new_global_var = "更新的全局變數" -- 當然更新也是。
print("---受限制的區塊: 結束--")
end
print("_G全局環境下的新變數:", _G.new_global_var)
-- 還原全局環境
for k, v in pairs(_E) do
_G[k] = _E[k]
end
end
print("當前環境下的新變數:", new_global_var)
---受限制的區塊: 開始--
writeHello(): 執行失敗
_G: nil
_E nil
---受限制的區塊: 結束--
_G全局環境下的新變數: 更新的全局變數
當前環境下的新變數: 更新的全局變數
可以看到受限制區塊下,writeHello()
的執行是失敗的,其原因是因為於全局禁用了io
檔案讀寫功能。
不過仍然允許受限制的區塊直接於全局環境添加新的變量,其原因在於其使用的環境是同一張表。實際上是有辦法分別使用不同環境的,試著想想看吧!
這部份牽扯到metaprogramming的內容,之後會在提到。
你可以為環境指定一個父環境。
所有不是宣告為
local
的變數,都會出現在_ENV
透過此規則,我們可以將可以執行的函式,與新建立的變數分開來。
do
local _E = {} -- 新環境
setmetatable(_E, {__index=_ENV}) -- 指定父環境
local _ENV = _E -- 啟用新環境
v1 = "one"
print(_ENV["v1"]) -- output: one
print("---inner environment---")
for k, v in pairs(_ENV) do
print(k, v) -- only v1
end
end
print("\nin outer, v1 = ", _ENV["v1"]) -- v1 = nil
print("\n---outer environment---")
for k, v in pairs(_ENV) do
print(k, v) -- many, but no v1
end
one
---inner environment---
v1 one
in outer, v1 = nil---outer environment---
package table: 0x558db9393b20
getmetatable function: 0x558db7d7b9e0
rawset function: 0x558db7d7ae30
......
可以看到,在內部區塊環境裡,雖然沒有print
卻依然可以使用,這是因為父環境提供了print
。相對的,新的環境變數v1
在離開內部環境區塊時,就不可用了。可以透過保有內部環境,來存取內部區塊新建立的環境變數:
local _E = {} -- 新環境
setmetatable(_E, {__index=_ENV}) -- 指定父環境
do
local _ENV = _E -- 啟用新環境
v1 = "one"
print(_ENV["v1"]) -- output: one
print("---inner environment---")
for k, v in pairs(_ENV) do
print(k, v) -- only v1
end
end
print("\n---outer environment---")
print("in outer, v1 = ", _ENV["v1"]) -- v1 = nil
print("in outer, _E.v1 = ", _E.v1)
one
---inner environment---
v1 one---outer environment---
in outer, v1 = nil
in outer, _E.v1 = one
_ENV
和 setfenv()
在Lua 5.1版本並沒有_ENV
這個特殊變數,這個變數是於Lua 5.2新增加的。相較於Lua 5.1,Lua 5.2的_ENV
沒多特別。反過來,Lua 5.1使用不同層次概念的setfenv()
。
setfenv()
只有Lua 5.1可以使用,這是5.1到5.2之間比較大的鴻溝,5.2已經用_ENV
替代部份setfenv()
功能,並將之移除。
setfenv()
的意思是 設定函數環境(set function environment) ,與之對應的有 取得函數環境(get function environment/getfenv()
) 。
function createA()
-- a function will create environment viriable - a
a = "A"
print("in createA function: a is ", a)
end
EnvCreateA = setfenv(createA, {print = print}) -- update createA function environment
print("----------EnvCreateA()---------")
EnvCreateA()
print("in global: a is ", a)
_E = getfenv(createA)
print("_E.a is ", _E.a)
print("---------createA()----------")
createA()
print("in global: a is ", a)
print("EnvCreateA is createA: ", EnvCreateA == createA)
----------EnvCreateA()---------
in createA function: a is A
in global: a is nil
_E.a is A
---------createA()----------
in createA function: a is A
in global: a is nil
EnvCreateA is createA: true
setfenv()
有一個函數返回值,其實更新後的函數。雖然可以使用getfenv()
取得函數環境,不過同樣可以給予一個已經存在的環境作為參考:
_E = {print = print}
EnvCreateA = setfenv(createA, _E) -- update createA function environment
EnvCreateA()
print(_E.a) -- Output: A
_E = getfenv(createA)
print(_E.a) -- Output: A
_E = getfenv(EnvCreateA)
print(_E.a) -- Output: A
print(createA == EnvCreateA) -- Output: true
setfenv()
設定的變數許多內置函數無法使用setfenv()
更新函數環境變數,例如:load()
、loadstring()
(Lua 5.2後移除)、require()
、dofile()
、loadfile()
等等。
ok = pcall(setfenv, load)
print("set load function environment: ", ok)
ok = pcall(setfenv, loadstring)
print("set loadstring function environment: ", ok)
ok = pcall(setfenv, require)
print("set require function environment: ", ok)
ok = pcall(setfenv, dofile)
print("set dofile function environment: ", ok)
ok = pcall(setfenv, loadfile)
print("set loadfile function environment: ", ok)
set load function environment: false
set loadstring function environment: false
set require function environment: false
set dofile function environment: false
set loadfile function environment: false
這些函數仍然使用全局環境_G
。來自C的函數多是如此。
load()
更安全的使用方式在幾天前有提到過,
load()
有個更安全的寫法。今天就來看看。
Lua 5.2後的load()
函數原形如下:
load (chunk [, chunkname [, mode [, env]]])
除第一個參數外,其餘都是可選的:
"bt"
)_G
※ 注意: 使用二進位(binary),並不保證環境變數都會創建於
_ENV
_E = {}
setmetatable(_E,{__index = _ENV})
load([[
a = 1 -- create a new environment variable
print(a) -- Output: 1
]],
--[[chunkname = ]] "Example: Create a New Environment Variable",
--[[mode = ]] "bt",
--[[env = ]] _E)() -- exec
print(a) -- Output: nil
print(_E.a) -- Output: 1
這樣一來load
chunk的執行將不會影響到主要環境。
exec()
和eval()
Python3的exec()
和eval()
同樣允許設定其執行的全局環境和區域環境,來看看:
def showInfo():
print('''show Informatio
------------------
showInfo is a global function
''')
_L = {}
_G = {}
exec('''
# create a local variable L1
L1 = 1
print(L1) # Output: 1
''',
_G, # global environment
_L # local environment
)
print(_L["L1"]) # Output: 1
print("-"*10)
eval('''
print(L1) # Output: 1
''', _G, _L)
exec('''
showInfo() # Error: can't find showInfo()
''', _G, _L)
_G["showInfo"] = showInfo # import showInfo
exec('''
showInfo() # Success
global G1
G1 = 100
''', _G, _L)
print(_G["G1"]) # Output: 100
因為本系列並不是在討論Python,所以就不特別多做解釋了。但相信你或多或少看到從Lua去認識其他類似程式語言的能力。
load
與5.2的行為不同。類似的有loadstring
。但其是危險的,應小心使用。