
На написание данной статьи меня сподвигло большое количество вопросов по метатаблицам и ООП в Lua, благо это самый сложный и проблематичный раздел у изучающих данный язык, но, так как Lua проектировалась как язык для начинающих и не-программистов и, в целом, имеет небольшой объём материала для освоения, негоже оставлять её «на потом», учитывая что с помощью метатаблиц можно творить чудеса и крайне элегантные решения заковыристых задач.
В данной публикации будет описание всех стандартных мета-методов для версий Lua 5.1-5.3, с примерами. Начнём.
Метатаблицы
Что это такое?
На самом деле, метатаблица ничем не отличается от обычной таблицы, за исключением того что она указана как управляющая.
Схематично можно представить, например, так:

В метатаблице описывается реакция основной таблицы на воздействия, например, вызов таблицы как функцию, деление таблицы на произвольное значение, или попытка вытянуть из неё ключ, которого у неё нет, благодаря специальным ключам (Lua 5.1, если не указано обратное):
Общие метаметоды
- __index — функция или таблица, с помощью которых оригинальная таблица ищет ключи, если их не существует;
- __newindex — функция, как добавлять в таблицу *новые* ключи, на уже существующие — не действует;
- __call — фунция, которая вызовется когда таблицу попробуют вызвать как как функцию;
- __tostring — функция, вызывающаяся при попытке преобразовать таблицу в строку, например, при print или tostring, сочетается с __concat;
- __concat — функция, вызывающаяся при попытке конкатенации таблицы с чем либо, сочетается с __tostring;
- __metatable — значение, которое возвращается попытке взять метатаблицу у данной таблицы, позволяет скрывать метатаблицы;
- __mode — строка, управляющая «силой» связей в таблице при сборке мусора, с её помощью можно создавать таблицы слабых ссылок или эфемероны;
- __gc — функция, которая будет вызвана при сборе userdata(5.1+) или таблицы(5.2+) мусорщиком, если очень хочется в 5.1 — есть способ применения;
- __len — функция которая будет вызываться при попытке вычисления длины таблицы, с помощью оператора # (5.2+);
- __pairs — функция, альтернатива итератора pairs для данной таблицы (5.2+);
- __ipairs — функция, альтернатива ipairs (5.2+);
Математические метаметоды и сравнение (функции)
- __add — (+) сложение;
- __sub — (-) вычитание;
- __mul — (*) умножение;
- __div — (/) деление;
- __pow — (^) возведение в степень;
- __mod — (%) деление по модулю;
- __idiv — (//) деление с изъятием целой части (5.3+);
- __eq — (==) сравнение равенства;
- __lt — (<) сравнение «меньше чем», в обратную сторону выполняется автоматически, реверсируя аргументы;
- __le — (<=) сравнение «меньше или равно»;
Битовые операции (функции, только 5.3+)
- __band — (&) «И»;
- __bor — (|) «ИЛИ»;
- __bxor — (~) исключающее «ИЛИ» (a ~ b);
- __bnot — (~) «НЕТ» (~a);
- __shl — (<<) битовый сдвиг влево;
- __shr — (>>) битовый сдвиг вправо.
Примеры
Index
Один из самых распространённых метаметодов, и вызывающий наибольшее число вопросов.
Может быть таблицей или функцией, с аргументами (self, key), где self — таблица для поиска, а key — ключ, значение которого мы хотим получить.
На основе этого метаметода строится большое количество фич, таких как ООП, прокси-таблицы, дефолтные значения таблиц, и ещё много чего.
Иногда может быть вреден, когда необходимо получить точный ключ ВОТ ЭТОЙ таблицы, в таких случаях используют функцию value = rawget(table, key), которая является функцией доступа у таблиц по умолчанию (она, в байткоде, вызывается при попытке получения значения по ключу).
--[[1]] foo = {} foo.key = 'value' --[[2]] mt = {__index = foo} -- setmetatable возвращает первый переданный в неё аргумент --[[3]] bar = setmetatable({}, mt) bar.key2 = 'value2' -- Тестирование: --[[4]] print('bar.key2', bar.key2) --> 'value2' --[[5]] print('bar.key', bar.key) --> 'value' --[[6]] bar.key = 'foobar' foo.foo = 'snafu' print('bar.key', bar.key) --> 'foobar' print('bar.foo', bar.foo) --> 'snafu' print('bar.foobarsnafu', bar.foobarsnafu) --> nil --[[7]] foo = {key = 'FooBar'} -- так тоже можно bar = setmetatable({}, {__index = foo}) snafu = setmetatable({}, {__index = bar}) print('snafu.key', snafu.key) --> 'FooBar' --[[8]] foo = {} foo.__index = foo setmetatable(foo, foo) -- print('foo.key', foo.key) --> error: loop in gettable print('foo.__index', foo.__index) --> "table: 0x12345678" print('rawget(foo, "key")', rawget(foo, "key")) --> nil --[[9]] foo = {} foo.key = 'value' setmetatable(foo, {__index = function(self, key) return key end}) print('foo.key', foo.key) --> 'value' print('foo.value', foo.value) --> 'value' print('foo.snafu', foo.snafu) --> 'snafu' --[[10]] fibs = { 1, 1 } setmetatable(fibs, { __index = function(self, v) self[v] = self[v - 1] + self[v - 2] return self[v] end })
Что тут происходит:
1. foo — таблица, в которой мы будем искать ключи, которых у нас нет.
2. mt — таблица, с ключом __index = foo. Если её прицепить к чему-то как метатаблицу, она будет указывать: «Если у нет ключей — попробуйте найти их в foo».
3. Тут — процесс цепляния метатаблицы mt к пустой таблице (которой становится bar)
4. Пример прямого доступа к ключам таблицы. В данном случае, мы берём ключ как обычно, из таблицы bar.
5. Пример доступа к ключам по __index. В данном случае, в таблице bar отсутствует ключ [«key»], и мы ищем его по __index метатаблицы — в таблице foo.
6. Уточнение: если мы внесём в таблицу bar ключ key, он найдётся в ней и при попытке забрать значение — не будет вызвана цепочка метаметодов. Но все остальные несуществующие ключи, такие как [«foo»], будут продолжать вызывать цепочку метаметодов. Ключ [«foobarsnafu»] отсутствует в обеих таблицах, и его значение — закономерный nil.
7. Index позволяет создавать цепочки поиска. В данном случае, алгоритм поиска ключа следующий:
1. Ищем ключ [«key»] в таблице snafu.
2. Не нашли. У метатаблицы snafu есть ключ __index, указывающий на таблицу bar. Ищем там.
3. Опять не нашли. Но и там есть метатаблица с ключом __index, указывающей на таблицу foo. Ищем.
4. Нашли! Вот он наш ключ, и его значение — «FooBar»!

8. В данном случае — мы создаём таблицу с ключом __index, равном ей самой, и устанавливаем её как метатаблицу для самой себя. При попытке получения значения по любому отсутствующему ключу, возникает рекурсивный цикл попыток поиска внутри себя самой, и переходов по __index метатаблицы и дальнейшего поиска. Поэтому, лучше не делать замкнутые цепочки поиска. Если использовать rawget — ни один метаметод не вызывается, и мы получаем точное значение данного ключа.
9. В качестве ключа __index у метатаблицы может быть функция с двумя аргументами — self — сама таблица, key — ключ, значение которого мы хотим получить. Возвращаемое значение функции становится значением. С помощью этого, можно создавать произвольную индексацию у таблиц, или создавать прокси.
10. Пример взят с википедии. В данном случае, __index у таблицы fibs автоматически пересчитывает значения чисел фибоначчи с мемоизацией, т.е. print(fibs[10]) выведет десятое число фибоначчи. Работает через рекурсивное вычисление отсутствующих значений таблицы. Последующие значения мемоизируются в таблицу. Нужно немного времени чтобы понять: если fibs[v — 1] отсутствует — для него выполняется тот же набор действий что и для fibs[v].
NewIndex
Не настолько распространённый метаметод, но тоже иногда удобный для создания закрытых таблиц, фильтрации или проксирования, и ещё нескольких вещей.
Всегда может быть только функцией, с аргументами (self, key, value).
Иногда может быть вреден, поэтому для принудительного не-использования данного метаметода используется функция rawset(self, key, value), который является функцией для таблиц по умолчанию.
--[[1]] foo = {} mt = {} function mt.__newindex(self, key, value) foo[key] = value end bar = setmetatable({a = 10}, mt) bar.key = 'value' print('bar.key', bar.key) --> nil print('foo.key', foo.key) --> 'value' --[[2]] bar.a = 20 print('bar.a', bar.a) --> 20 --[[3]] mt = {} function mt.__newindex(self, key, value) if type(value) == 'number' then -- чтобы не уйти в бесконечный цикл __newindex rawset(self, key, value) end end foo = setmetatable({}, mt) foo.key = 'value' foo.key2 = 100500 print('foo.key', foo.key) --> nil print('foo.key2', foo.key2) --> 100500
1. Это — простейший пример добавления ключей через прокс-таблицу с помощью метаметода __newindex. Все новые ключи-значения, которые мы добавляем в таблицу bar, добавляются в foo в соответствии с функцией. Self, в данном случае — таблица bar;
2. __newindex распространяется только на несуществующие ключи;
3. Пример функции-фильтра, которая позволяет добавлять в таблицу только числовые значения. Точно так же можно проверять «добавляем только числовые ключи», или заранее создаём несколько таблиц для чисел-строк-таблиц и т.п, и добавляем значения в соответствующие (классификация/балансировка и т.п.).
Call
Данный метаметод удобен для сокращения элементов или вызова дефолтных методов функций с таблицами и для чуть более комфортного ООП, когда мы вызываем таблицу-класс как функцию, и получаем объект.
--[[1]] mt = {} function mt.__call(self, a, b, c, d) return a..b..c..d end foo = setmetatable({}, mt) foo.key = 'value' print(foo(10, 20, 30, '!')) --> 102030! print(foo.key) --> 'value' print(foo.bar) --> nil --[[2]] mt = {} -- Многоточие - все аргументы, которые были переданы в функцию -- например: a, b, c, d = ... function mt.__call(self, ...) return self.default(...) end foo = setmetatable({}, mt) function foo.mul2(a, b) return a * b end function foo.mul3(a, b, c) return a * b * c end foo.default = foo.mul2 print('foo.mul2(2, 3)', foo.mul2(2, 3)) --> 6 print('foo.default(2, 3)', foo.default(2, 3)) --> 6 print('foo.mul3(2, 3, 4)', foo.mul3(2, 3, 4)) --> 24 -- Вызов значения по умолчанию. print('foo(2, 3)', foo(2, 3)) --> 6 foo.default = foo.mul3 print('Default was changed') print('foo(2, 3, 4)', foo(2, 3, 4)) --> 24
1. Пример использования метатаблицы, таблицу можно вызывать как функцию. В качестве self передаётся сама таблица, вызванная как функция.
2. В данном примере, мы заполняем таблицу функциями, а метатаблица указывает, что если её вызовут как функцию — выдать результат фунции под ключом default.
Tostring и Concat
Просто приведение объекта к строке и конкатенация.
mt = {} function mt.__tostring(self) return '['..table.concat(self, ', ')..']' end foo = setmetatable({}, mt) foo[1] = 10 foo[2] = 20 foo[3] = 30 print('foo', foo) --> [10, 20, 30] -- print('foo..foo', foo..foo) -- Ошибка! Невозможно конкатенировать таблицу! function mt.__concat(a, b) return tostring(a)..tostring(b) end print('foo.."!"', foo.."!") --> [10, 20, 30]! print('"!"..foo', "!"..foo) --> ![10, 20, 30] print('foo..foo', foo..foo) --> [10, 20, 30][10, 20, 30]
Metatable
Скрытие метатаблиц, иногда бывает полезно.
mt = {} mt.id = 12345 foo = setmetatable({}, mt) print(getmetatable(foo).id) --> 12345 mt.__metatable = 'No metatables here!' print(getmetatable(foo)) --> 'No metatables here!' mt.__metatable = false print(getmetatable(foo)) --> false
Mode
Строка, указывает режим связей значений таблиц. Если она содержит букву 'k', то слабыми будут объявлены ключи, если содержит букву 'v' — слабыми станут значения. Можно использовать их вместе. В примерах будет использоваться функция collectgarbage — принудительный сбор всего мусора.
Таблицы в Lua — всегда передаются по ссылке.
--[[1]] mt = {__mode = 'v'} foo = setmetatable({}, mt) --[[2]] bar = {foobar = 'fu'} foo.key = bar foo.key2 = {barfoo = 'uf'} foo[1] = 100500 --[[3]] print('foo.key.foobar', foo.key.foobar) --> 'fu' print('foo.key2.barfoo', foo.key2.barfoo) --> 'uf' print('foo[1]', foo[1]) --> 100500 collectgarbage() print('foo.key.foobar', foo.key.foobar) --> 'fu' print('foo[1]', foo[1]) --> 100500 -- print('foo.key2.barfoo', foo.key2.barfoo) --> Ошибка, key2 не существует! --[[4]] bar = nil collectgarbage() -- print('foo.key.foobar', foo.key.foobar) --> Ошибка, key не существует!
1. Пример таблицы слабых значений: если нет ссылок на значения, кроме как в этой таблице — они удалятся в процессе сборки мусора.
2. После исполнения данного участка кода, на таблицу
"{foobar = 'fu'}" существуют две ссылки (в глобальном пространстве и в таблице foo), а на таблицу
"{barfoo = 'uf'}" — одна, внутри foo.
3. Мы видим, что пока в таблице foo есть все значения, но после сборки мусора — исчезает таблица key2. Это происходит потому, что на неё не осталось больше сильных ссылок, только слабые, которые позволяют мусорщику её собрать. К foo[1] это не относится, так как 100500 — не ссылочный тип (не таблица, не функция, не userdata и т.п, а число).
4. Если мы удалим единственную сильную ссылку bar — таблица {foobar = 'fu'} тоже будет уничтожена после сборки мусора.
Аналогичным образом работает с 'k', только по отношению к ссылочным ключам таблицы (foo[{key = 'value'}] = true).
GC
Функция __gc вызовется в том случае, если таблица будет собрана сборщиком мусора. Может использоваться как финалайзер. Функционирует с таблицами и cdata/userdata.
mt = {} function mt.__gc(self) print('Table '..tostring(self)..' has been destroyed!') end -- lua 5.2+ foo = {} setmetatable(foo, mt) -- Lua 5.1 if _VERSION == 'Lua 5.1' then -- Метаметод __gc работает в 5.1 только по отношению к cdata-типам. -- Данная методика - грязный хак, но иногда полезен. -- мы будем удалять ссылку 'foo', тут локальная копия local t = foo -- newproxy возвращает cdata-указатель, недокументированная функция Lua 5.1. local proxy = newproxy(true) -- метаметод __gc данной cdata - вызов __gc-метаметода таблицы foo getmetatable(proxy).__gc = function(self) mt.__gc(t) end foo[proxy] = true end print(foo) foo = nil collectgarbage() --> 'Table 0x12345678 has been destroyed!'
Len
Функция, переопределяющая алгоритм вычисления длины таблицы (Lua 5.2+).
mt = {} function mt.__len(self) local keys = 0 for k, v in pairs(self) do keys = keys + 1 end return keys end foo = setmetatable({}, mt) foo[1] = 10 foo[2] = 20 foo.key = 'value' print('#foo', #foo) --> 3 (2 в Lua 5.1)
Pairs и Ipairs
Переопределение стандартных итераторов таблиц, для данной таблицы (Lua 5.2+).
mt = {} function mt.__pairs(self) local key, value = next(self) return function() key, value = next(self, key) -- Отфильтовываем не-числовые ключи. while key and type(key) == 'number' do key, value = next(self, key) end return key, value end end function mt.__ipairs(self) local i = 0 -- ipairs по отрицательным числам. return function() i = i - 1 return self[i] and i, self[i] end end foo = setmetatable({}, mt) foo[1] = 10 foo[2] = 20 foo[-1] = -10 foo[-2] = -20 foo.foo = 'foobar' foo.bar = 'barfoo' -- Lua 5.1 пройдёт по всем ключам, -- 5.2+ - только по строковым for k, v in pairs(foo) do print('pairs test', k, v) end --> foo foobar --> bar barfoo -- Lua 5.1 пройдёт по всем положительным числовым ключам, -- 5.2+ - только по отрицательным числовым for i, v in ipairs(foo) do print('ipairs test', i, v) end --> -1 -10 --> -2 -20
Перегрузка операторов
Перегрузка всех операторов работает по одной схеме, детальные примеры для каждого — не нужны.
]] --[[1]] vector_mt = {} function vector_mt.__add(a, b) local v = {} v.x = a.x + b.x v.y = a.y + b.y return setmetatable(v, vector_mt) end function vector_mt.__div(a, b) local v = {} v.x = a.x / b.x v.y = a.y / b.y return setmetatable(v, vector_mt) end -- Для удобства function vector_mt.__tostring(self) return '['..self.x..', '..self.y..']' end vec1 = setmetatable({}, vector_mt) vec1.x = 1 vec1.y = 2 vec2 = setmetatable({}, vector_mt) vec2.x = 3 vec2.y = 4 vec3 = vec1 + vec2 print('vec3', vec3) --> [4, 6] print('vec2 / vec1', vec2 / vec1) --> [3, 2] --[[2]] mt = {} function mt.__add(a, b) local insert_position = 1 if type(a) == 'table' and getmetatable(a) == mt then insert_position = #a + 1 else a, b = b, a end table.insert(a, insert_position, b) return a end -- Для удобства function mt.__tostring(self) return '['..table.concat(self, ', ')..']' end foo = setmetatable({}, mt) --[[3]] foo = 3 + 4 + foo + 10 + 20 + 'a' + 'b' print('foo', foo) --> [7, 10, 20, a, b] foo = '12345' + foo print('foo', foo) --> [12345, 7, 10, 20, a, b]
1. Пример перегрузки операторов на таблицах, которые ведут себя как векторы, благодаря метатаблице. Следует следить за порядком аргументов, каждая операция — возвращает новую таблицу-«вектор».
2. Таблица, в которую можно добавлять элементы оператором "+".
Порядок добавления определяет, в конец или в начало мы добавляем элемент.
3. 3 + 4 выполнится первым, поэтому первый элемент — «7».
В остальных случаях — к результату выполнения предыдущего элемента будет прибавляться следующий:
((7 + foo -> foo) + 10 -> foo)…
Что со всем этим можно сделать
ООП
Первое что напрашивается — попытка сделать ООП.
Попробуем написать простую функцию, реализующую некоторый абстрактный «класс»:
function Class() local class = {} -- Метатаблица для класса. local mClass = {__index = class} -- Функция, которая возвращает "объект" данного класса. function class.instance() return setmetatable({}, mClass) end return class end -- Пока без двоеточий. Rectangle = Class() function Rectangle.new(x, y, w, h) local self = Rectangle.instance() self.x = x or 0 self.y = y or 0 self.w = w or 10 self.h = h or 10 return self end function Rectangle.area(self) return self.w * self.h end -- Инстанцируем rect = Rectangle.new(10, 20, 30, 40) print('rect.area(rect)', rect.area(rect)) --> 1200 print('rect:area()', rect:area()) --> 1200
Вот, уже что-то похожее на ООП. Тут нет наследования и всяких крутых штук, но это уже неплохо.
При вызове rect.area, у таблицы-объекта нет ключа area, поэтому она идёт искать его через __index у таблицы-класса, находит, и подставляет туда саму себя первым аргументом.
Малое отступление от метатаблиц: пример второго вызова — первое появление в данной статье двоеточия. Двоеточие — синтаксический сахар языка Lua. Если вызвать функцию в таблице через двоеточие а не точку, первым аргументом в эту функцию подставится сама таблица, поэтому тот код эквивалентен.
Более подробно:
foo = {x = 10, y = 20} function foo.bar(self, a, b) return (self.x + a) * (self.y + b) end print('foo.bar(foo, 1, 2)', foo.bar(foo, 1, 2)) --> 242 -- Идентично, только self "автоматически" подставляется. function foo:bar(a, b) return (self.x + a) * (self.y + b) end print('foo:bar(1, 2)', foo:bar(1, 2)) --> 242
Можно попробовать слегка улучшить данный вариант.
Во-первых, добавить возможность вызывать класс как функцию с возвратом объекта, во-вторых, добавить возможность перегрузки операторов у самого класса, В третьих — наследование.
function Class(parent) local class = {} local mClass = {} -- Сам класс будет метатаблицей для объектов. -- Это позволит дописывать ему метаметоды. class.__index = class -- Поля объектов будут искаться по цепочке __index, -- и дотянутся, в том числе, до родительского класса. mClass.__index = parent -- Резервируем поле Super под родителя. class.Super = parent -- Функция, которая будет вызываться при вызове класса function mClass:__call(...) local instance = setmetatable({}, class) -- Резервируем поле класса "init" if type(class.init) == 'function' then -- Возвращаем экземпляр и всё что он вернул функцией init return instance, instance:init(...) end -- Но если её нет - тоже ничего. return instance end return setmetatable(class, mClass) end -- Основной класс. Shape = Class() function Shape:init(x, y) -- в качестве self мы сюда передаём инстанс объекта. self.x = x or 0 self.y = y or 0 return '!!!' end function Shape:posArea() return self.x * self.y end -- Так как таблица Shape является метатаблицей для рождаемых ей объектов, -- мы можем дописывать ей метаметоды. function Shape:__tostring() return '[' .. self.x .. ', ' .. self.y ..']' end local foo, value = Shape(10, 20) print('foo, value', foo, value) --> [10, 20] !!! -- Наследник Rectangle = Class(Shape) function Rectangle:init(x, y, w, h) -- В данный момент, self - это пустая таблица, -- к которой прицеплена таблица Rectangle, как мета. -- Вызов родительских методов через Super. self.Super.init(self, x, y) -- Вызов собственных методов при инициализации - тоже возможен. self.w, self.h = self:getDefault(w, h) end function Rectangle:area() return self.w * self.h end function Rectangle:getDefault(w, h) return w or 10, h or 20 end function Rectangle:__tostring() return '['..self.x..', '..self.y..', '..self.w .. ', '..self.h..']' end rect = Rectangle(10, 20, 30, 40) print('rect', rect) --> [10, 20, 30, 40] print('rect:area()' , rect:area()) --> 30 * 40 = 1200 -- Вызов родительского метода print('rect:posArea()', rect:posArea()) --> 10 * 20 = 200
Таким образом, в 15 строк полезного кода, можно имплементировать в Lua максимум из действительно необходимого ООП.
Конечно, тут есть что улучшать и чем обвешивать, и подобная работа проведена в библиотеках middleclass или hump.Class, но иногда полезно и такое.
Кстати, если не хочется заморачиваться с функциями-классами, а нужно просто написать один-два класса, можно пользоваться конструкцией отсюда.
Прокси-таблицы
Вот тут, наконец-то, пример полноценной прокси, с отслеживанием действий.
function proxy() local real_table = {} local logger = {} local metatable = {} -- Забираем значения из настоящей таблицы и логируем их function metatable:__index(key) local value = rawget(real_table, key) table.insert(logger, "Get key "..tostring(key)..' is '.. tostring(value)) return value end -- Подставляем значения в реальную таблицу, с произвольными действиями function metatable:__newindex(key, value) table.insert(logger, "Set key "..tostring(key)..' as '..tostring(value)) rawset(real_table, key, value) end return setmetatable({}, metatable), logger end prox, log = proxy() prox.a = 10 prox.a = 20 print('prox.a', prox.a) --> 20 print('log', '\n'..table.concat(log, '\n')) --> Set key a as 10 --> Set key a as 20 --> Get key a, is 20
На выходе таблица, которая логирует её использование. В данном случае, таблица-прокси всегда пуста, в ней нет никаких ключей, поэтому __newindex будет вызываться каждый раз.
Таблицы временных объектов
Время от времени, могут понадобиться временные объекты, которые существуют некоторое время, но при нехватке памяти — освобождают занимаемое пространство. Данный пример потребует наличия библиотеки Luasec (https-запросы), хотя с тем же успехом можно использовать Luasocket, только без https.
page = {} page.https = require'ssl.https' page.cache = setmetatable({}, {__mode = 'v'}) -- Метатаблица для отдельных страничек page._meta = {} function page._meta:__tostring() return 'Page: ['..self.url.. ']: '.. self.status end setmetatable(page, page) function page:__call(url) return self.cache[url] or self:request(url) end function page:request(url) local page = setmetatable({}, self._meta) page.url = url page.data, page.status, page.error, page.hate = self.https.request(url) print(page.data, page.status, page.error, page.hate) self.cache[url] = page return page end -- Запрашиваем, например, Яндекс. p = page('https://yandex.ru') print('p', p) --> Page: [https://yandex.ru]: 200 print('p.status', p.status) --> 200 -- И он болтается в кеше, -- при последующих аналогичных запросах - будет извлекаться оттуда. print('page.cache[...]', page.cache['https://yandex.ru']) --> Page: [https://yandex.ru]: 200 -- Он остаётся после сборки мусора, потому что остаётся сильная ссылка "p". collectgarbage() print('page.cache[...]', page.cache['https://yandex.ru']) --> Page: [https://yandex.ru]: 200 p = nil collectgarbage() -- Но после удаления ссылки - её больше нет, страничка больше не нужна. print('page.cache[...]', page.cache['https://yandex.ru']) --> Nil
Пока всё
Я считаю, что данного материала достаточно чтобы более-менее освоить метатаблицы, если есть интересные или забавные примеры — пишите в комментариях.
Для желающих задать кучу вопросов — оставляю ссылку на чатик в тележке.
