从python到lua的学习笔记
Lua tutorial for python developer
基础
命令行
# 进入交互模式
lua -i
# 类似于python -c "print(math.sin(12))"
lua -e "print(math.sin(12))"
# arg[0]表示脚本
- 交互模式中使用
=变量名
可以打印变量的值; - 解释器在运行脚本前会为所有的命令行参数创建一个名为“arg”的table,脚本名称位于索引0上,它的第1个参数位于索引1,以此类推,脚本之前的参数位于负数索引上;
注释
-
单行:
--
-
多行:
--[[ 注释部分 --]]
变量
-
Lua 中的变量默认是全局变量,哪怕是语句块或是函数里,除非用 local 显式声明为局部变量;
-
跟python一样,lua是动态语言,变量的类型不需要预定义而是在runtime的时候决定;
-
应该尽可能的使用局部变量以避免命名冲突,同时局部变量的访问速度比全局变量快;
-
Lua将其所有的全局变量保存在一个常规的table中,这个table成为环境(environment),这个table自身保存在一个全部变量
_G
中。比如:
if not rawget(_G, "ConfigMgr") then rawset(_G, "ConfigMgr", {}) end
运算符
逻辑运算符包括 and、or 和 not。与控制结构类似,所有逻辑运算符都将 false 和 nil 视为假,其余任何值都视为真,这意味着数字0也被视为真(这个在其他语言里面比较少见)
-
运算符 and 如果第一个参数为假,则返回该参数;否则返回第二个参数。
-
运算符 or 如果第一个参数不为假,则返回该参数;否则返回第二个参数。
-
and 和 or 运算符都只在必要时计算第二个操作数,如下:
print(4 and 5) --> 5
print(nil and 13) --> nil
print(false and 13) --> false
print(4 or 5) --> 4
print(false or 5) --> 5
在python中我们比较习惯使用or
给变量赋缺省值,但是lua中存在使用and
来赋缺省值的情况:
local value = my_table and my_table["key"]
数据类型
-
nil:相当于None:
> type(X) nil > type(X)==nil false > type(X)=="nil" true >
-
boolean:
- true和false, 只有false和nil是false,数字0也是true!
-
number:
- Lua 默认只有一种 number 类型-double(双精度)类型
-
string:
-
除了用单引号和双引号以外,还可以用
[[]]
表示一段文本(文本内容不会进行转义),类似于python中的三个单引号的doc表示方法; -
字符串连接使用
..
而不是+
:>> print("a"+"b") stdin:1: unfinished string near '")' > print("a".."b") ab
-
使用
#
计算字符串的长度:> s="abcdefg" > print(#s) 7
-
-
function:函数也是一等公民,这意味着函数可以存储到变量中,也可以作为实参传递给其他函数或者作为函数的返回值,总之,lua也支持函数式编程。
-
userdata:表示任意存储在变量中的C数据结构;
-
thread:执行的独立线程,用于执行协同程序;
-
table:
-
其实是一个关联数组(associate arrays),数组的索引可以是数字、字符串或者table类型;
-
不同于其他语言的数组把 0 作为数组的初始索引,在 Lua 里表的默认初始索引一般以 1 开始;
-
索引为字符串或者table时,就变成了我们熟悉的字典,比如:
dd ={A=1,B=2} for k,v in pairs(dd) do print(k,v) end
注意:作为key时字符串不需要加引号
-
遍历使用
for k, v in pairs(tb) do
语句:local tbl = {"apple", "pear", "orange", "grape"} for key, val in pairs(tbl) do print("Key", key) end
-
当索引为字符串时,可以使用t.i这种简化写法,意义同t[i]
-
循环
while
a=10
while( a < 20 )
do
print("a 的值为:", a)
a = a+1
end
for
重点掌握遍历list的方法:
-- 数值for循环
for i, v in iparis(array) do
xxx
end
-- var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为1。
for var=exp1,exp2,exp3 do
<执行体>
end
for i=10,1,-1 do
print(i)
end
-- 泛型for循环
--打印数组a的所有值
a = {"one", "two", "three"}
-- ipairs相当于python中的enumerate
for i, v in ipairs(a) do
print(i, v)
end
repeat...until
repeat...until 循环的条件语句在当前循环结束后判断
repeat
statements
until( condition )
--[ 变量定义 --]
a = 10
--[ 执行循环 --]
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )
条件语句
if...else语句
if(布尔表达式)
then
--[ 布尔表达式为 true 时执行该语句块 --]
else
--[ 布尔表达式为 false 时执行该语句块 --]
end
if...elseif...else语句
if( 布尔表达式 1)
then
--[ 在布尔表达式 1 为 true 时执行该语句块 --]
elseif( 布尔表达式 2)
then
--[ 在布尔表达式 2 为 true 时执行该语句块 --]
elseif( 布尔表达式 3)
then
--[ 在布尔表达式 3 为 true 时执行该语句块 --]
else
--[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end
迭代器
closure闭包
一个closure就是一种可以访问其外部嵌套环境中的局部变量的函数。通过这些变量就可以记住它在一次遍历中所在的位置。
无状态迭代器
一种自身不保存任何状态的迭代器,比如ipairs:
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
泛型迭代器
for <var-list> in <exp-list> do
<body>
end
for line in io.lines() do
io.write(line, "\n")
end
函数
函数定义
Lua 编程语言函数定义格式如下:
optional_function_scope function function_name(argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
解析:
- optional_function_scope: 该参数是可选的指定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。
- function_name: 指定函数名称。
- argument1, argument2, argument3..., argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。
- function_body: 函数体,函数中需要执行的代码语句块。
- result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。
myprint = function(param)
print("这是打印函数 - ##",param,"##")
end
function add(num1,num2,functionPrint)
result = num1 + num2
-- 调用传递的函数参数
functionPrint(result)
end
myprint(10)
-- myprint 函数作为参数传递
add(2,5,myprint)
匿名函数
lua同样支持匿名函数,比如:
table.sort(network, function (a,b) return (a.name>b.name) end)
函数调用
lua为面向对象的调用也提供了一种特殊的语法,表达式o.foo(o, x)的另一种写法是o:foo(x),冒号操作符将使调用o.foo时将o隐含地作为函数的第一个参数,类似于python中对象方法的self
参数也是在调用时隐含的作为函数的第一个参数传递。
可变参数
- 使用三点
...
表示函数有可变的参数,通过select("#", ...)
来获取可变参数的数量:
function average(...)
result = 0
local arg={...}
for i,v in ipairs(arg) do
result = result + v
end
print("总共传入 " .. select("#",...) .. " 个数")
return result/select("#",...)
end
print("平均值为",average(10,5,3,4,5,6))
- select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表。
function f(...)
a = select(3,...) -->从第三个位置开始,变量 a 对应右边变量列表的第一个参数
print (a)
print (select(3,...)) -->打印所有列表参数
end
>f(0,1,2,3,4,5)
2
2 3 4 5
-
如果不习惯
select
用法,可以直接把可变参数赋值给table
,之后操作table
即可,例如上面的例子可以写成():function f(...) local arg = {...} print (arg[3]) for i = 3, #arg do print (arg[i]) -->打印所有列表参数 end end >f(0,1,2,3,4,5) 2 2 3 4 5
-
函数调用时,若实参多余形参,则舍弃多余的实参;若实参不足,则多余的形参初始化为nil;
-
unpack函数接受一个数组作为参数,并从下标1开始返回数组的所有元素,类似于python中的
*items
语法:print(unpack{1,2,3}) --> 1 2 3
常用方法
字符串方法
-
string.upper()、string.lower()、string.len()
-
string.gsub(mainString, findString, replaceString, num)
> string.gsub("aaaa","a","z",3); zzza 3
-
string.find(str, substr, [init, [plain]])
> string.find("Hello Lua user", "Lua", 1) 7 9
-
string.format()
> string.format("the value is:%d",4) the value is:4
-
string.char(arg) 和 string.byte(arg[,int])
> string.char(97,98,99,100) abcd > string.byte("ABCD",4) 68 > string.byte("ABCD") 65 >
-
string.match(str, pattern, init)
> = string.match("I have 2 questions for you.", "%d+ %a+") 2 questions > = string.format("%d, %q", string.match("I have 2 questions for you.", "(%d+) (%a+)")) 2, "questions"
-
string.sub(s, i [, j])
- s:要截取的字符串。
- i:截取开始位置。
- j:截取结束位置,默认为 -1,最后一个字符。
id="#autoqa1" =id:sub(2) autoqa1 =id:sub(#id) 1
表方法
- table.concat(table [, sep [, start [, end]]])
fruits = {"banana","orange","apple"}
-- 返回 table 连接后的字符串
print("连接后的字符串 ",table.concat(fruits))
-- 指定连接字符
print("连接后的字符串 ",table.concat(fruits,", "))
-- 指定索引来连接 table
print("连接后的字符串 ",table.concat(fruits,", ", 2,3))
- table.insert(table, [pos,] value) 在table的数组部分指定位置(pos)插入值为value的一个元素. pos参数可选, 默认为数组部分末尾.
fruits = {"banana","orange","apple"}
-- 在末尾插入
table.insert(fruits,"mango")
print("索引为 4 的元素为 ",fruits[4])
-- 在索引为 2 的键处插入
table.insert(fruits,2,"grapes")
print("索引为 2 的元素为 ",fruits[2])
-
table.remove (table [, pos])返回table数组部分位于pos位置的元素. 其后的元素会被前移. pos参数可选, 默认为table长度, 即从最后一个元素删起。
-- 创建一个包含 5 个元素的数组表 local myTable = {10, 20, 30, 40, 50} -- 移除第三个元素(值为 30) table.remove(myTable, 3) -- 移除元素后的数组 {10,20,40,50}
模块与包
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。
-- util.lua
util = {}
util.PATH = "/home/admin"
function util.foo()
print("foo")
end
local function bar()
print("bar")
end
return util
上述的bar声明为一个局部变量,即表示一个私有函数,因此是不能从外部访问模块里的私有函数的,必须通过公有函数来调用。
require 函数
Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:
require("<模块名>")
或者
require "<模块名>"
比如:
-- main.lua
require 'util'
util.foo()
print(util.PATH)
如果希望使用较短的模块名称,则可以设置一个局部名称,比如:
local m = require "mod"
m.foo()
即使知道某些用到的模块可能已经加载了,但只要用到require就是一个良好的编程习惯。可以将标准库排除在此规则之外,因为Lua总数会预先加载它们。只要一个模块已经加载过,后续的require调用都将返回同一个值。如果require为指定模块找到了一个Lua文件,它就通过loadfile
来加载该文件。而如果找到的是一个C程序库,则通过loadlib
来加载
加载机制
类似于PYTHON_PATH
,lua使用LUA_PATH
这个环境变量来搜索模块。
如果LUA_PATH
中无法找到与模块名相等的Lua文件,它就会找C程序库,搜索路径由LUA_CPATH
来定义。
在搜索一个文件时,require所使用的路径是一连串的模式(pattern),其中每项都是一种将模块名转换为文件名的方式。进一步说,require会用模块名来替换每个?
,然后根据替换的结果来检查是否存在这样一个文件。如果不存在,就会尝试下一项。
Windows上,安装时自动配置的LUA_PATH
的值是这个;;C:\Program Files (x86)\Lua\5.1\lua\?.luac
元表
类似于python中__XXX__
(比如__repr__
等)的特殊方法(也叫魔法方法):
- setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
- getmetatable(table): 返回对象的元表(metatable)。
常见元方法
lua元方法 | python方法 | 说明 |
---|---|---|
__index | __getattr__ |
|
__newindex |
__setattr__ |
|
__call |
__call__ |
|
__tostring_ |
__str_ |
错误处理
assert
首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出。
local function add(a,b)
assert(type(a) == "number", "a 不是一个数字")
assert(type(b) == "number", "b 不是一个数字")
return a+b
end
add(10)
pcall
接收一个函数和要传递给后者的参数,并执行,分别返回执行结果,执行是否报错以及错误信息,例如:
-- 无报错情况
> =pcall(function(i) print(i) end, 33)
33
true
-- 有报错情况
> =pcall(function(i) print(i) error('error..') end, 33)
33
false stdin:1: error..
Lua提供了xpcall
函数,xpcall
接收第二个参数——一个错误处理函数,当错误发生时,Lua会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用debug库来获取关于错误的额外信息了。
debug库提供了两个通用的错误处理函数:
- debug.debug:提供一个Lua提示符,让用户来检查错误的原因
- debug.traceback:根据调用桟来构建一个扩展的错误消息
>=xpcall(function(i) print(i) error('error..') end, function() print(debug.traceback()) end, 33)
33
stack traceback:
stdin:1: in function <stdin:1>
[C]: in function 'error'
stdin:1: in function <stdin:1>
[C]: in function 'xpcall'
stdin:1: in main chunk
[C]: in ?
false nil
面向对象
Lua中使用冒号 :
来访问类的成员函数,这种调用方法隐含的传递了一个self
参数。
Lua中最基本的结构是table,所以需要用table来描述对象的属性,可以通过创建一个新的表并将其元表设置为父表来实现对象的继承。假设有两个对象a和b,要让b作为a的一个原型,只需要输入如下语句:
setmetatable(a, {__index=b})
在此之后,a就会在b中查找所有它没有的操作,更具体的例子如下:
-- 定义父表
local parent = {
name = "Parent",
greeting = function(self)
print("Hello from " .. self.name)
end
}
-- 定义子表
local child = {
name = "Child"
}
-- 将子表的元表设置为父表
setmetatable(child, { __index = parent })
-- 子表现在可以访问父表中的属性和方法
child:greeting()
setmetatable
函数是Lua中的一个内置函数,用于设置一个表的元表。元表是一个包含特殊方法的表,当对一个表进行某些操作时,如果这个表没有定义对应的方法,Lua就会在它的元表中查找这个方法。因此,通过设置一个表的元表,我们可以为它提供默认的行为或者覆盖它的一些行为。其中,__index
是一个特殊的元方法,当Lua在子表中查找一个不存在的属性或方法时,它会在子表的元表中查找__index
所指定的表,并在这个表中查找对应的属性或方法。在上述代码中,我们将子表的元表设置为父表,这意味着当子表在自己的表中找不到对应的属性或方法时,它会在父表中查找,从而实现继承的效果。
再看一个例子:
Account = {balance=0}
function Account:new(obj)
obj = obj or {}
setmetable(o, self)
self.__index = self
return obj
end
function Account:deposit(v)
self.balance = self.balance + v
SpecialAccount = Account:new()
s=SpecialAccount:new({balance=100})
协程
当create一个coroutine的时候就是在新线程中注册了一个事件。
当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。
方法:
coroutine.create(f): 创建 coroutine,参数是一个函数,返回 一个coroutine对象;
coroutine.resume(co): 唤醒coroutine,参数是coroutine对象;
coroutine.yield():挂起当前的coroutine,将 coroutine 设置为挂起状态;
coroutine.status():查看当前coroutine的状态;
coroutine.wrap():创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine;本质就是如果coroutine对象不存在则创建,如果已经创建,则调用resume方法;
coroutine.running():返回当前正在运行的coroutine;
function async_sleep(seconds)
local start = os.time()
while os.time() - start < seconds do
coroutine.yield()
end
end
local co1 = coroutine.create(function ()
print("Coroutine 1 started!")
async_sleep(2)
print("Coroutine 1 finished!")
end)
local co2 = coroutine.create(function ()
print("Coroutine 2 started!")
async_sleep(1)
print("Coroutine 2 finished!")
end)
while coroutine.status(co1) ~= "dead" or coroutine.status(co2) ~="dead" do
coroutine.resume(co1)
coroutine.resume(co2)
end
文件IO
简单模式:
-- 简单模式
-- 以只读方式打开文件
file = io.open("test.lua", "r")
-- 设置默认输入文件为 test.lua
io.input(file)
-- 调用io.read()方法读取默认输入文件的第一行
print(io.read())
-- 关闭打开的文件
io.close(file)
-- 以附加的方式打开只写文件
file = io.open("test.lua", "a")
-- 设置默认输出文件为 test.lua
io.output(file)
-- 在文件最后一行添加 Lua 注释
io.write("-- test.lua 文件末尾注释")
-- 关闭打开的文件
io.close(file)
io.read()可以携带如下参数:
- “*n”: 读取一个数字并返回它。
- “*a”: 从当前位置读取真个文件。
- “*I”(默认): 读取下一行,在文件尾处(EOF)返回nil。
- number: 返回一个指定字符个数的字符串,或再EOF处返回nil
其他的io方法有:
- io.tmpfile():返回一个临时文件句柄,该文件以更新模式打开,程序结束时自动删除
- io.type(file):检测obj是否一个可用的文件句柄
- io.flush():向文件写入缓冲中的所有数据
- io.lines(optional file name):返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回 nil,但不关闭文件。
完全模式
使用 file:function_name 来代替 io.function_name 方法
file = io.open("test.lua", "r")
print(file:read())
file:close()
日期和时间
-- unix时间戳
> print(os.time())
1676530122
-- os.date会格式化的输出时间的字符串形式
> print(os.date())
02/16/23 14:50:42
> print(os.date("%Y/%m/%d %H:%M"))
2023/02/16 14:53
-- os.clock()会返回当前CPU时间的秒数,带小数,可以精确到毫秒,通常用来计算一段代码的执行时间。
> print(os.clock())
650.732
-- 同步sleep
require("socket")
function sleep(seconds)
socket.sleep(seconds)
end
-- 异步sleep
function async_sleep(seconds)
local start = os.time()
while os.time() - start < seconds do
coroutine.yield()
end
end
其他系统调用
-- 获取环境变量
> print(os.getenv("LUA_PATH"))
;;C:\Program Files (x86)\Lua\5.1\lua\?.luac
-- 相当于python中的system调用
os.execute("mkdir mydir")
-- dofile (FILENAME) 加载文件中的代码,且加载代码时遇到错误会报错。
-- 加入a.lua文件中定义了如下两个函数
function f_a(x,y)
return x + y
end
function g_a(x,y)
return x * y
end
-- 那么可以 dofile 加载这个文件并使用这两个函
dofile('a.lua')
f_a(3,4)
g_a(3,4)
-- unpack函数
t={1,2,3}
x,y,z=unpack(t)
-- x=1,y=2,z=3
a,b=unpack(t,1,2)
-- a=1,b=2
调试接口
调试库的性能不高,并且会打破语言的一些固有规则,因此用户通常不会在最终版本中打开这个库,或者使用debug=nil来删除它。
stack level概念:调用调试函数时level 1,调用这个函数的level是2,依次类推。
debug.getinfo
第一个参数可以是函数或者stack level。比如为某个foo函数调用debug.getinfo(foo) 时就会得到一个table,其中包含:
- source:函数定义的位置。如果是loadstring定义的,那么会返回完整的字符串,如果是文件中定义,那么就是这个文件名前@
- short_src:source的短版本(最多60个字符),用于错误信息
- linedefined
- lastlinedefined
- what:函数的类型,分为下面三种
- Lua
- C
- main
- name: 函数的名称
- namewhat:可能是
- global
- local
- method
- field
- 空字符串:没有找到该函数
- nups:函数的upvalue数量
第二个参数是可选的,其值为字符串,用来过滤打印必要的信息,具体如下:
- n:name和namewhat
- f:func
- S:source、short_src、what、linedefined和lastlinedefined
- l:currentline
- L:activielines
- u:nups
debug.getlocal
代码规范
参考这个:http://lua-users.org/wiki/LuaStyleGuide
关于命名,也是case_snake的风格,基本参照python的命名规范就行了。