Python Lists

lua-users home
wiki

Python

[Python] is a popular language that contains lists and dictionaries. Lua's table is a multipurpose container that is somewhere in between but in itself lacks functionality as a list. The functions table.insert, table.remove and table.getn from the Lua standard library give tables list functionality (or more specifically termed arrays in Lua). The listing below emulates Python's list type. Section 5.1 [1] of the Python Tutorial documents Python's list functionality.

Implementation

We create a class table, List, containing methods. We use Lua's index metamethod to call the List methods (for background info see ClassesAndMethods).

This version has been rewritten for Lua 5.1 (although the original has been kept) with overloads added for the concatenation and equality operators. For slices, the start and finish are now an inclusive range, which is more Lua-ish. There is also a slice_assign method for the Python construct s[i1:i2] = seq.

-- SteveDonovan

Examples

The code below allows you do things like:

lst = List:new()  -- create an empty List
lst2 = List:new{1,2,"blah",lst}  -- create a list with some items in
lst3 = List {10,20,30}  -- List acts like a 'constructor'
-- Our new methods
lst:append("hello")
lst:extend{"world", 35, 77, "?"}
lst:insert(3,"how?")
lst:remove(35)
q = lst:pop()
print( lst:index("world") )
print( lst:count("?") )
lst:sort()
lst:reverse()
print( lst[-2] )
a = lst:slice(3)
b = lst:slice(-4,-2)
lst:clear()
lst:range(77)

-- We can mix them with Lua's library calls
table.insert(lst,"boo")
print(#lst)

This interactive session demonstrates some of the operations. Note that these list objects can be called as if they were functions, which is a shortcut for the slice method.

C:\lang\lua\projects\libs>lua
Lua 5.1.2  Copyright (C) 1994-2007 Lua.org, PUC-Rio
> require 'pylist'
> a = List {10,20,30}
> b = List {1,2,3}
> = a..b
{10,20,30,1,2,3}
> = a:slice(2,3)
{20,30}
> = a[-1]
30
> = a:index(30)
3
> a:append(40)
> = a
{10,20,30,40}
> = List {1,2} == List{1,2}
true
> = List {1,2} == List{1,4}
false
> = a(2,-1)
{20}
> = a(-2)
{20,30}


-- Emulation of Python lists
-- Nick Trout
-- See http://www.python.org/doc/current/tut/tut.html, section 5.1
-- Note:The comments before some of the functions are from the Python docs
-- and contain Python code.
-- Written for Lua version 4.0
-- Redone for Lua 5.1, Steve Donovan.

local tinsert = table.insert
local tremove = table.remove
local tsort = table.sort
local write = io.write

-- metatable for our list objects
List = {}
List.__index = List
-- we give the metatable its own metatable so that we can call it like a function!
_ListMT = {}
setmetatable(List,_ListMT)
function _ListMT.__call(tbl,arg)
  return List:new(arg)
end

-- Handle the index event
-- note: this is called if something is _not_ present in the table. These are either 
-- negative indices, method names, or bad indices.
function List:__index(i)
  local typ = type(i)
  if typ=="string" then
    local fn = List[i]
    if not fn then error("Bad List function: "..i) end
    return fn
  elseif typ=="number" then
    if i<0 then
      local sz = #self
      if i<-sz then error("List index out of range: "..i) end
      return self[sz+i+1]
    else
      error("Bad list index: "..i)
    end
  end
end

-- The newindex event handles list[i] = val, if i is not present in 
-- the list - used for negative indices!
function List:__newindex(i,val)
  if type(i)=="number" then
    if i<0 then
      local sz = #self
      if i<-sz then error("List index out of range: "..i) end
      self[sz+i+1] = val
    else
      error("Bad list index: "..i)
    end
  end
end

local function simple_table(t)
  return type(t) == 'table' and getmetatable(t) ~= List
end

-- Create a new list. Can optionally pass a list eg. x=List:new{1,2,3}
-- passing another instance of List will cause a _copy_ to be created
-- we pass anything which isn't a simple table to iter() to work out
-- an appropriate iterator
function List:new(t)
  if not t then t={} 
  elseif  not simple_table(t) then
    local tbl = t
    t = {}
    for i,v in iter(tbl) do
      tinsert(t,v)
    end
  end
  setmetatable(t,List)
  return t
end

--Add an item to the end of the list; equivalent to a[len(a):] = [x].
function List:append(i)
  tinsert(self,i)
end

-- Extend the list by appending all the items in the given list;
-- equivalent to a[len(a):] = L.
function List:extend(L)
  assert(type(L)=="table","List:extend expecting a table")
  for i,v in ipairs(L) do tinsert(self,v) end
end

-- Insert an item at a given position. The first argument is the index of the
-- element before which to insert, so a.insert(0, x) inserts at the front of
-- the list, and a.insert(len(a), x) is equivalent to a.append(x).
function List:insert(i, x)
  tinsert(self,i,x)
end

-- equivalent of Python's del s[i]
List.delete = tremove

-- Remove the first item from the list whose value is x.
-- It is an error if there is no such item.
function List:remove(x)
  for i=1,#self do
    if self[i]==x then tremove(self,i) return end
  end
  error("List:remove failed, item not found")
end

-- Remove the item at the given position in the list, and return it.
-- If no index is specified, a.pop() returns the last item in the list.
-- The item is also removed from the list.
function List:pop(i)
  if not i then i = #self end
  return tremove(self,i)
end

-- Return the index in the list of the first item whose value is x.
-- It is an error if there is no such item.
function List:index(x)
  for i=1,#self do
    if self[i]==x then return i end
  end
  error("List:index failed, item not found")
end

-- Return the number of times x appears in the list.
function List:count(x)
  local cnt=0
  for i=1,#self do
    if self[i]==x then cnt=cnt+1 end
  end
  return cnt
end

-- Sort the items of the list, in place.
function List:sort()
  tsort(self)
end

-- Reverse the elements of the list, in place.
function List:reverse()
  local t,n={},#self
  for i=1,n do t[i]=self[n-i+1] end -- reverse
  for i=1,n do self[i]=t[i] end -- copy back
end

local function normalize_slice(self,first,last)
  local sz = #self
  if not first then first=1 end
  if first<0 then first=sz+first+1 end
  -- make the range _inclusive_!
  if not last then last=sz end 
  if last < 0 then last=sz+last end
  return first,last
end

-- Emulate the list slice eg. python_list[first:last]
-- If first or last are negative then they are relative to the end of the list
-- eg. slice(-2) gives last 2 entries in a list,
-- eg. slice(-4,-2) gives from -4th to -2nd
function List:slice(first,last)
  first,last = normalize_slice(self,first,last)
  local t=self:new()
  for i=first,last do tinsert(t,self[i]) end
  return t
end

-- empty the list
function List:clear()
  for i=1,#self do tremove(self,i) end
end

-- Emulate Python's range(x) function which gives you a sequence of 0..x-1
-- Include it in List table for tidyness
function List:range(x)
  local t=self
  if type(t) == 'number' then -- we did not get a self argument - it was a number!
    x = t
    t=List:new()    
  end
  for i=0,x-1 do tinsert(t,i) end
  return t
end

-- Python len(list) is the same as #list
function List:len()
  return #self
end

-- Extended operations --

-- equivalent to del s[i1:i2]
function List:chop(i1,i2)
    i1,i2 = normalize_slice(self,i1,i2)
    for i = i1,i2 do
        tremove(self,i1)
    end
end

-- equivalent to s[idx:idx] = seq
function List:splice(idx,seq)
    idx = idx - 1
    for i,v in ipairs(seq) do
        tinsert(self,i+idx,v)
    end
end

-- general slice assignment s[i1:i2] = seq
function List:slice_assign(i1,i2,seq)
    i1,i2 = normalize_slice(self,i1,i2)
    if i2 >= i1 then self:chop(i1,i2) end
    self:splice(i1,seq)
end

-- concatenation operator ..
function List:__concat(L)
  local ls = self:slice(1,-1)  -- making a copy!
  ls:extend(L)
  return ls
end

-- equality operator ==
function List:__eq(L)
--~   print(#self,#L)
  if #self ~= #L then return false end
  for i = 1,#self do
--~     print(self[i],L[i])
    if self[i] ~= L[i] then return false end
  end
  return true
end

-- how our list should be rendered as a string
-- note: really can't handle list items which are not strings or numbers
function List:__tostring()
  return '{'..table.concat(self,',')..'}'
end

-- can use the call notation to extract slices!
function List:__call(first,last)
  return self:slice(first,last)
end

function List:foreach(fun)
  for i,v in ipairs(self) do
    fun(v)
  end
end

-- capturing the Python concept of 'sequence'. 
-- In particular, this makes strings and file objects to be iterable sequences.
function iter(seq)
    if type(seq) == 'string' then
        local idx = 0
        local n = #seq
        local sub = string.sub
        return function ()
            idx = idx + 1
            if idx > n then return nil
            else
                return idx,sub(seq,idx,idx)
            end
        end
    elseif type(seq) == 'table' then
        return ipairs(seq)
    elseif type(seq) == 'function' then
        return seq()
    elseif type(seq) == 'userdata' and io.type(seq) == 'file' then
        local lines = seq:lines()
        local k = 0
        return function ()
            local line = lines()
            if not line then
              return nil 
            else
                k = k + 1
                return k,line
            end
        end			
    end
end

-- test using: lua pylist.lua
if arg and arg[0]=="pylist.lua" then
  local pr = function(l)
    for i=1,#l do io.write(l[i],' ') end
    print()
  end
  local lst = List:new()
  lst:append(10)
  lst:extend{20,30,40,50}
  assert (lst == List{10,20,30,40,50})
  lst:insert(3,11)  
  lst:remove(40)
  assert (lst == List{10,20,11,30,50})
  local q=lst:pop()
  assert( lst:index(30)==4 )
  assert( lst:count(10)==1 )
  lst:sort()
  lst:reverse()
  assert (lst == List{30,20,11,10})
  assert (lst[-1] == 10)
  assert (lst[-3] == 20)
  lst = List {10,20,30,40,50}
  assert (lst:slice(2) == List{20,30,40,50})
  assert (lst:slice(-2) == List{40,50})
  assert (lst:slice(nil,3) == List{10,20,30})
  assert (lst:slice(2,4) == List{20,30,40})
  assert (lst:slice(-4,-2) == List{20,30})
  lst = List.range(10)
  seq = List{0,1,2,3,4,5,6,7,8,9}
  assert(lst == seq)
  assert (List('abcd') == List{'a','b','c','d'})
  ls = List{10,20,30,40}
  ls:slice_assign(2,3,{21,31})
  assert (ls == List{10,21,31,40})
end

Older Example for Lua 4.0

The code below allows you do things like:

lst = List:new()  -- create an empty List
lst2 = List:new{1,2,"blah",lst}  -- create a list with some items in

-- Our new methods
lst:append("hello")
lst:extend{"world", 35, 77, "?"}
lst:insert(3,"how?")
lst:remove(35)
q = lst:pop()
print( lst:index("world") )
print( lst:count("?") )
lst:sort()
lst:reverse()
print( lst[-2] )
a = lst:slice(3)
b = lst:slice(-4,-2)
lst:clear()
lst:range(77)

-- We can mix them with Luas syntax
tinsert(lst,"boo")
print(getn(lst))


-- Emulation of Python lists
-- Nick Trout
-- See http://www.python.org/doc/current/tut/tut.html, section 5.1
-- Note:The comments before some of the functions are from the Python docs
-- and contain Python code.
-- Written for Lua version 4.0

List = settag({},newtag())

-- Handle the tagmethod "index"
function List:_index(i)
  local typ = type(i)
  if typ=="string" then
    local fn = List[i]
    if not fn then error("Bad List function: "..i) end
    return fn
  elseif typ=="number" then
    if i<0 then
      local sz = getn(self)
      if i<-sz then error("List index out of range: "..i) end
      return self[sz+i+1]
    else
      error("Bad list index: "..i)
    end
  end
end

settagmethod(tag(List), "index", List._index)

-- Create a new list. Can optionally pass a list eg. x=List:new{1,2,3}
function List:new(t)
  if not t then t={} end
  settag(t,tag(List))
  return t
end

--Add an item to the end of the list; equivalent to a[len(a):] = [x].
function List:append(i)
  tinsert(self,i)
end

-- Extend the list by appending all the items in the given list;
-- equivalent to a[len(a):] = L.
function List:extend(L)
  assert(type(L)=="table","List:extend expecting a table")
  for i=1,getn(L) do tinsert(self,L[i]) end
end

-- Insert an item at a given position. The first argument is the index of the
-- element before which to insert, so a.insert(0, x) inserts at the front of
-- the list, and a.insert(len(a), x) is equivalent to a.append(x).
function List:insert(i, x)
  tinsert(self,i,x)
end

-- Remove the first item from the list whose value is x.
-- It is an error if there is no such item.
function List:remove(x)
  for i=1,getn(self) do
    if self[i]==x then tremove(self,i) return end
  end
  error("List:remove failed, item not found")
end

-- Remove the item at the given position in the list, and return it.
-- If no index is specified, a.pop() returns the last item in the list.
-- The item is also removed from the list.
function List:pop(i)
  local item=i
  if not item then item=getn(self) end
  return tremove(self)
end

-- Return the index in the list of the first item whose value is x.
-- It is an error if there is no such item.
function List:index(x)
  for i=1,getn(self) do
    if self[i]==x then return i end
  end
  error("List:index failed, item not found")
end

-- Return the number of times x appears in the list.
function List:count(x)
  local cnt=0
  for i=1,getn(self) do if self[i]==x then cnt=cnt+1 end end
  return cnt
end

-- Sort the items of the list, in place.
function List:sort()
  sort(self)
end

-- Reverse the elements of the list, in place.
function List:reverse()
  local t,n={},getn(self)
  for i=1,n do t[i]=self[n-i+1] end -- reverse
  for i=1,n do self[i]=t[i] end -- copy back
end

-- Emulate the list slice eg. python_list[first:last]
-- If first or last are negative then they are relative to the end of the list
-- eg. slice(-2) gives last 2 entries in a list,
-- eg. slice(-4,-2) gives from -4th to -2nd
function List:slice(first,last)
  local sz = getn(self)
  if not first then first=1 end
  if first<0 then first=sz+first+1 end
  if not last then last=sz else last=last-1 end
  if last<0 then last=sz+last+1 end
  local t=self:new()
  for i=first,last do tinsert(t,self[i]) end
  return t
end

-- empty the list
function List:clear()
  for i=1,getn(self) do tremove(self,i) end
end

-- Emulate Pythons range(x) function which gives you a sequence of 0..x-1
-- Include it in List table for tidyness
function List:range(x)
  local t=self
  if not t then t=List:new() end
  for i=0,x-1 do tinsert(t,i) end
  return t
end

-- Python len(list) is the same as getn
List.len = getn

-- test using: lua -f pylist.lua -test
if arg and arg[1]=="-test" then
  local pr = function(l) for i=1,getn(l) do write(l[i]) end print() end
  local lst = List:new()
  lst:append("hello ") pr(lst)
  lst:extend{"world ","are ","you ","diddling ","?"} pr(lst)
  lst:insert(3,"how ") pr(lst)
  lst:remove("diddling ") pr(lst)
  local q=lst:pop() pr(lst)
  assert( lst:index("are ")==4 )
  assert( lst:count("are ")==1 )
  lst:sort() pr(lst)
  lst:reverse() pr(lst)
  tinsert(lst,"boo") pr(lst)
  print( getn(lst), lst:len(), List.len(lst) )
  print( lst[1],lst[-1],lst[-3] )
  --print( lst[0] )  -- test errors
  --print( lst[100] )
  --print( lst[-100] )
  write("list: ") pr(lst)
  write("[2:] : ")  pr( lst:slice(2) )
  write("[-2:] : ")  pr( lst:slice(-2) )
  write("[:3] : ")  pr( lst:slice(nil,3) )
  write("[:-2] : ") pr( lst:slice(nil,-2) )
  write("[2:4] : ") pr( lst:slice(2,4) )
  write("[-4:-2] : ") pr( lst:slice(-4,-2) )
  lst:clear() pr(lst)
  pr( List:range(10) )
  lst:range(20) pr(lst)
end

--NickTrout

Lists by comprehension in MetaLua

Another handy feature of python lists, which can also be found in Haskell[2], is the specification of lists by comprehension: generating lists through cartesian products and filtering is made easier and more readable. Metalua comes with a sketchy implementation of this features, plus a couple of others idioms such as list sampling:

-{extension "clist"}
-- integers from 2 to 50, by steps of 2:
x = { i for i = 2, 50, 2 }

-- the same, obtained by filtering over all integers <= 50:
y = { i for i = 1, 50 if i%2==0 }

-- prime numbers, implemented in an inefficient way:
local sieve, n = { i for i=2, 100 }, 1
while n < #sieve do
   sieve = { 
      i for i in values(sieve[1 ... n]);
      i for i in values(sieve[n+1 ... #sieve]) if i%sieve[n] ~= 0 }
   n += 1
end
table.print(sieve)

-- FabienFleutot

An alternative implementation of this syntax (without the filter clause) can be found in LuaMacros -- SteveDonovan

A question of Lua Style

There are several functions like List.index which throw an error, rather than returning false. This is appropriate Python style, but more awkward in Lua because the built-in exception handling mechanism is more clumsy. So should these methods return a boolean, rather? We can depend on nil not being a valid value, unlike in Python.

-- SteveDonovan


See also: ClassesAndMethods , SampleCode , PythonDictionaries
RecentChanges · preferences
edit · history
Last edited November 27, 2007 2:49 pm GMT (diff)