본문 바로가기

3.구현/Lua

5. Lua 메타테이블(metatable)

5. 메타테이블

Writer: ospace114@empal.com, [http://ospace.tistory.com/

테이블의 제한점

루아에서 테이블은 중요한 자료형이다. 루아에서 대부분의 작업이 이 테이블을 통해서 이뤄진다. 그러나 이러한 테이블 사용에도 한계가 있다. 예를 들어 테이블 간의 비교과 연산은 불가능하다.

a = b + c --b와 c는 테이블

이러한 테이블의 제한점을 극복할 수 있는 것이 메타테이블이다. 이를 이용해서 테이블의 기능을 변경할 수 있다. 즉 테이블간의 덧셈연산도 가능하며 필요시 다른 작업으로 치환해서 사용할 수 있다.

메타테이블 정보 출력

루아에서 테이블들은 각각 자신만의 메타테이블을 가지고 있다. 사용자 데이터 역시 메타테이블을 가지고 있다. 루아에서 기본적으로 메타테이블 생성 없이 테이블을 생성하여 사용한다.

t = {}

위는 일반적인 테이블 생성 형식이다. 해당 테이블이 메타테이블인지 확인할 수 있는 중요한 함수인 getmetatable(object)를 사용해서 확인할 수 있다.

print(getmetatable(t))

테이블 t가 메타테이블이라면 "table:xxxxx"라고 해당 메타테이블 주소값이 출력되지만, 그렇지안다면 "nil"로 출력된다. 현재는 메타테이블이 설정되어 있지 않아서 "nil"로 출력 될 것이다.

메타테이블 설정하기

그러면 어떻게 메타테이블로 만들 수 있을까. 이를 위해서 사용하는 함수가

setmetatable(table, metatable)

이다. table이 일반 테이블이고 metable도 일반 테이블이지만, table의 메타테이블로 사용할 테이블을 넣어주면 된다.

t = {}
mt = {} -- t와 t1 모두 그냥 테이블 생성
setmetatable(t, mt)  -- t에 메타데이블 설정하기
assert(getmetatable(t) == mt) -- t의 메타 테이블의 테이블 mt과 같다

테이블 t의 메타 테이블로 mt이 설정되었다. 즉 t의 메타테이블을 가져오는 것이 mt 메타테이블을 반환한다. 그렇기에 assert에서 이상없이 통과하였다. 즉 어떤 테이블도 다른 테이블의 메타테이블이 될 수 있다.

메타테이블은 일반 테이블과 달리 특수한 기능들이 있다. 메타테이블 항목에는 일반 값을 저장하는 것 외에 다른 항목들이 있다.

그럼 간단한 사용 예제를 보도록 하자.

add 테스트

-- 테스트 위한 간단한 함수 정의
function testadd()
print("testadd...")
end

t = {}
t.__add = testadd -- 메타테이블 t의  "__add" 항목에 testadd함수 설정

r = t + 1 -- 덧셈연산

결과

testadd...

call 테스트

function testcall()
print("testcall")
end

t={}
t.__call = testcall
t.testcall = testcall

t()
t.testcal()

결과

testcall  
testcall

잘보면 덧셈 연산을 했는데 testadd 함수 실행결과가 표시된다. 그러나 실제 덧셈 연산을 위해서 값을 받아서 계산하는 것을 살펴보도록 하자.

간단한 메타테이블 예제

간단한 메타테이블 생성 예제를 보자. 이 예제는 "+"연산을 이용해서 합집합을 만드는 예제이다.

s1 = {11, ["a"] = "aa"} -- 테이블 s 생성
mt = {}                                          -- 테이블 mt생성
setmetatable(s1, mt)                       -- 테이블 s1에 mt 테이블을 메타테이블로 설정

위의 예제를 테이블용 메타테이블을 mt라고 선언하였다. 이는 관리 편의 상의 목적으로 mt는 (MetaTable)의 약자를 사용했다. 이 메타테이블은 다른 테이블의 메타 테이블로도 중복해서 사용할 수 있다.

그리고 위의 테이블에서 "1 = true"로 한겻은 인덱스 기반이 아닌 키값 기반으로 테이블을 사용하겠다는 의미이다. 물론 키값이 아닌 인덱스도 가능하지만 위의 숫자가 아닌 문자인 경우는 값을 처리하지 못하므로 키값 기반으로 처리하는게 좋다.

그리고 새로운 테이블을 하나 더 만들어보자. s1과 연산하기 위한 테이블이다.

s2 = {["a"] = "ospace", 44, ["b"] = "bb"}   -- 테이블 s2 생성

이것만로는 일반 테이블과 다를 것이 없다. 우리가 할려는 것은 다음 작업을 가능게하려고 한다.

s3 = s1 + s2

이제 메타테이블의 힘을 보여줄 순서이다. 메타테이블에서 덧셈연산자("+")와 대응되는 객체가 "__add"라고 있다. 즉 메타테이블이 설절된 테이블 간에 덧셈연산을 할 경우 "__add"객체가 호출되어서 처리한다. C++언어를 해다면 연산자 재정의와 비슷하다고 보면 된다.

먼저 덧셈 연산을 위한 함수를 하나 선언해보자. 우리는 덧셈연산을 집합 연산에서 교집합으로 처리하려고 한다. s1과 s2에 공통된 키는 s1의 키값으로 채워넣는다. 자세한 로직은 다음과 같다.

-- 합집합 연산자
function union(left, right)
  local r = {}

  for k, v in pairs(left) do r[k] = v end

  for k, v in pairs(right) do
     local l = rawget(left, k)  
     if l == nil then r[k] = v end
  end
  return r
end

left와 right에 각각 s1과 s2 테이블이 입력된다. 그리고 테이블의 키값을 보기 쉽게 출력하기 위한 문자열로 가공하기 위한 함수를 정의하겠다.

function print_table(t)
 print("[",t,"]")
 for k,v in pairs(t) do
  print("{",k..", ",v,"}")
 end
 print("[=================]")
end

그럼 앞의 예제를 조합해서 결과를 보자. 먼저 덧셈 연산을 합집합 연산으로 설정한 후에 작업을 해야한다.

mt.__add = union  --  덧셈연산을 교집합 연산으로 변경
s3 = s1 + s2         -- 덧셈 연산 수행
print_table(s3) -- 결과 출력

그러면 아래와 같은 결과가 나온다.

결과

[ table: 003CCAB0 ]
{ 1,  11 }
{ a,  aa }
{ b,  bb }
[=================]

잘보면 union()에서 인자 a와 인자 b는 덧셈연자자의 좌우에 있는 테이블 s1과 테이블 s2에 대응된다. 이는 나머지 연산에서도 동일하게 적용된다.

만약에 덧셈연산 중에 테이블이 아닌 일반 숫자가 나온다면 어떻게 될까?

s3 = s1 + 1

실행하면 에러가 발생하면서 종료가 된다. 이는 union 함수를 실행하면서 인자 b에 1값이 대입이 되고 pairs를 수행하면서 에러가 발생한다. paris가 테이블이 아니면 에러가 발생하기 때문이다.

test.lua:5: bad argument #1 to `pairs' (table expected, got number)

이에 대한 에러 처리 부분인데 참고로 보시기 바란다.

union 함수에서 이에 대한 에러처리 부분이 추가되어야 한다. 이는 입력되는 두 인자 모두가 메타테이블인자 확인하여 한개라도 메타테이블이 아니면 에러메시지를 출력하게 한다.

function union(a,b)
  if getmettable(a) ~= mt or getmetatable(b) ~= mt then
       error("attempt to 'add' a set with a non-set value", 2)
  end
   ...
end

즉, s1 테이블에 __add 항목이 설정되어 있어야 한다. s2는 상관이 없다. 다음과 같은 경우에 틀려진다.

r = s2 + s1

이 경우는 s2 테이블에 __add항목이 설정되어 있어야 한다.

다음은 사칙연산(+, -, *, /)과 승수(^) 및 기타 연산자과 메타테이블의 항목과 대응되는 관계를 표시했다.

  • + ==> __add
  • - ==> __sub
  • * ==> __mul
  • / ==> __div
  • ^ ==> __pow
  • - ==> __unm (음수 연산자)
  • < ==> __lt
  • <= ==> __le
  • .. ==> __concat (병합연산자)
  • 테이블에 인덱스 값을 얻으려고 할때 ==> __index
  • 테이블에 유효한 인덱스 값을 얻을려고 할때 ==> __gettable
  • 테이블에 새로운 인덱스 값을 만들려고 할때 ==> __newindex
  • 테이블에 인덱스에 새로운 값을 저장하려 할때 ==> __settable
  • 함수가 호출될때 ==> __call
  • 가비지 컬렉션이 호출될 때 ==> __gc

여기서 좀 흥미있는 __index에 대해서 자세히 살펴보겠다. __call은 앞에서 대충 살펴보았으니 어떤 것이지 알 것이라 생각해서 생략하겠다.

두번째 메타테이블 예제

이번 예제는 __index 항목에 대한 예제이다. 그냥 예제만 다루도록 하겠다. 그전에 lua.org에서 metatable의 "index"에 대한 코드를 보도록 하자.

function gettable_event(table, key)
   local h
   if type(table) == "table" then
      local v = rawget(table, key)
      if v~ = nil then return v end
      h = metatable(table).__index
      if h == nil then return nil end
   else
      h = metatable(table).__index
      if h == nil then
         error(...)
      end
   end
   if type(h) == "function" then
      return (h(table, key)) -- call the handler
   else return h[key]
end

위의 코드에 __index에 대한 작업이 어떻게 되는지 알 수 있다.

작업 순서는 아래와 같다.

  1. 타입이 테이블이면
    1-1. 현 테이블에서 해당 키값을 찾음.
    1-2. 키 값이 없으면 현 테이블의 메타테이블을 회득, 메타 테이블이 없다면 리턴
    1-3. 메타테이블에서 __index를 획득
  2. 타입이 테이블이 아니면
    2-1. 현 테이블에서 메타테이블 획득, 메타 테이블이 없다면 리턴
  3. 메타 테이블의 __index가 함수면 함수 호출.
  4. 함수가 아니면 바로 인덱스 호출.

이제 예제를 보도록 하자. 설명은 따로 없다.

m = {"osp"}
mt = {}

function test_index()
   return "ace"
end

mt.__index = test_index

setmetatable(m, mt)

print(m[1], m[2])

결과

osp ace

추가로 __newindex와 __index 에대한 예제이다.

ospace = {}

ospace["name"] = "js"

function ospace:print()
    print("==============================")
    for k,v in pairs(self) do
        print (k,v)
        if (type(v) == "table") then
            for a,b in pairs(v) do print(">>", a, b) end
        end
    end
end

ospace:print()

mt = {}
setmetatable(ospace, mt)

ospace.value = {}
mt.__index = function (table, key)
    print("=index>", table, key)
    return ospace.value[key]
end


mt.__newindex = function(table, key, value)
    print("=new_index>", table, key, value)
    ospace.value[key] = value
end

ospace.a = "aaa"
ospace:print()
print(ospace.a)
print(ospace[a])
print(ospace["a"])
ospace.value.log = ospace.print
ospace:log()

실행 결과이다.

==============================

name  js

print  function: 00B2C090

=new_index>  table: 00B2B658  a  aaa

==============================

value  table: 00B2B810

>>  a  aaa

print  function: 00B2C090

name  js

=index>  table: 00B2B658  a

aaa

=index>  table: 00B2B658  nil

nil

=index>  table: 00B2B658  a

aaa

=index>  table: 0059B658  log

==============================

value  table: 0059B810

>>  a  aaa

>>  log  function: 0059C090

print  function: 0059C090

다른 예제

이를 좀더 세련되게 표현된 것이 아래와 같다. 이는 lua.org를 참고했다.

Set = {}
Set.mt = {}

function Set.new(t)
  local set = {}
  setmetatable(set, Set.mt)
  for _, l in ipairs(t) do set[l] = true end
  return set
end

function Set.union (a,b)
   ...
end

function Set.tostring (set)
  local s = "{"
  local sep = ""
  for e in pairs(set) do
     s = s .. sep .. e
     sep = ", "
  end
  return s .. "}"
end

function Set.print (s)
  print(Set.tostring(s))
end

이를 앞의 예제와 같이 사용하면 아래와 같다.

s1 = Set.new{1, 2, 3, 4}
s2 = Set.new{1, 5}

Set.mt.__add = Set.union
s3 = s1 + s2
Set.print(s3)

결과

{1, 2, 3, 4, 5}

## 아직 정확한 내용이 아니므로, 혹시 틀린부분이나, 추가할 부분이 있다면 메일로 남겨주세요 ##

참고

http://www.lua.org/pil/13.html
http://www.lua.org/manual/5.1/
http://www.redwiki.net/wiki/wiki/LUA/%B8%DE%C5%B8%C5%D7%C0%CC%BA%ED%C0%CC%BE%DF%B1%E2

반응형

'3.구현 > Lua' 카테고리의 다른 글

lua 동향(2011/10)  (0) 2011.10.26
6. Lua의 C 바인딩  (0) 2009.01.28
Lua 5.x와 Lua 4.0 호환성  (0) 2008.01.21
4. Lua의 조건문 반복문  (0) 2008.01.21
3. Lua의 각종 연산자들  (0) 2007.07.23