iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
Software Development

30天 Lua重拾筆記系列 第 23

【30天Lua重拾筆記22】中級議題: 全局表(_G)、環境表(_ENV)

同步發表於個人網站

_G_ENV

在Lua有兩個特殊變量--_G_ENV,其分別表示全局環境和當前環境。_G在與C交互時,另有作用。但大致上你可以將兩者視為相同。實際上,在Lua環境建立之初,兩著也確實是相同的:

print(_G == _ENV) -- => Output: true

先前說過,Lua只有表(table)這個複合結構。而_G_ENV也是table結構。_ENV中包含一個_Gkey,其值指向_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

_ENVsetfenv()

在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的函數多是如此。

[Lua 5.2] load()更安全的使用方式

幾天前有提到過,load()有個更安全的寫法。今天就來看看。

Lua 5.2後的load()函數原形如下:

load (chunk [, chunkname [, mode [, env]]])

除第一個參數外,其餘都是可選的:

  1. chunk: 執行的程式片段
  2. chunkname: 給予執行區塊一個更容易識別的名字
  3. mode: 有二進位(binary)和文字模式(text)。當前並不在意使用哪種模式,所以使用預設的混合模式("bt")
  4. env: 所使用的環境。預設使用全局環境_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的執行將不會影響到主要環境。

Python3的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去認識其他類似程式語言的能力。

其他注意事項

  • Lua 5.1 的load與5.2的行為不同。類似的有loadstring。但其是危險的,應小心使用。

上一篇
【30天Lua重拾筆記21】基礎3: 再看pairs, ipairs
下一篇
【30天Lua重拾筆記23】中級議題: 閉包
系列文
30天 Lua重拾筆記36

尚未有邦友留言

立即登入留言