当前位置: 首页>后端>正文

从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
    

运算符

逻辑运算符包括 andornot。与控制结构类似,所有逻辑运算符都将 false 和 nil 视为假,其余任何值都视为真,这意味着数字0也被视为真(这个在其他语言里面比较少见)

  • 运算符 and 如果第一个参数为假,则返回该参数;否则返回第二个参数。

  • 运算符 or 如果第一个参数不为假,则返回该参数;否则返回第二个参数。

  • andor 运算符都只在必要时计算第二个操作数,如下:

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.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.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的命名规范就行了。


https://www.xamrdz.com/backend/35g1869363.html

相关文章: