Средства самопознания в Ruby. Что программа на Ruby может знать о самой себе?::Журнал СА 1-2.2015
www.samag.ru
     
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Журнал "Системный администратор"
Журнал «БИТ»
Наука и технологии
Подписка
Где купить
Авторам
Рекламодателям
Архив номеров
Контакты
   

  Опросы
  Статьи

Рынок труда  

Системные администраторы по-прежнему востребованы и незаменимы

Системные администраторы, практически, есть везде. Порой их не видно и не слышно,

 Читать далее...

1001 и 1 книга  
19.03.2018г.
Просмотров: 9876
Комментарии: 0
Потоковая обработка данных

 Читать далее...

19.03.2018г.
Просмотров: 8090
Комментарии: 0
Релевантный поиск с использованием Elasticsearch и Solr

 Читать далее...

19.03.2018г.
Просмотров: 8193
Комментарии: 0
Конкурентное программирование на SCALA

 Читать далее...

19.03.2018г.
Просмотров: 5189
Комментарии: 0
Машинное обучение с использованием библиотеки Н2О

 Читать далее...

12.03.2018г.
Просмотров: 5866
Комментарии: 0
Особенности киберпреступлений в России: инструменты нападения и защита информации

 Читать далее...

Друзья сайта  

 Средства самопознания в Ruby. Что программа на Ruby может знать о самой себе?

Архив номеров / 2015 / Выпуск №1-2 (146-147) / Средства самопознания в Ruby. Что программа на Ruby может знать о самой себе?

Рубрика: Разработка /  Особенности языка

Иван Шихалев ИВАН ШИХАЛЕВ, фрилансер, специализируется на веб-разработке и Ruby, shikhalev@gmail.com

Средства самопознания в Ruby
Что программа на Ruby может знать о самой себе?

Практически все современные языки программирования содержат средства, позволяющие во время выполнения программы получить какие-то данные о структуре самой этой программы

В компилируемых языках такие возможности, как правило, ограничены и отключаемы в целях оптимизации, в интерпретируемых же более обширны, поскольку эти данные все равно необходимы самому интерпретатору, соответственно содержатся в памяти. Вопрос только в том, предоставлять ли к ним доступ языковыми средствами.

В данной статье я планирую рассмотреть те средства «самопознания», которые доступны для программ на Ruby.

Возвращаясь к компилируемым языкам: в них существует четкое разделение – есть отладочная информация, которая самой программе недоступна, и есть RTTI (Run-Time Type Information) – первая включается только для отладки, вторая может использоваться в нормальной логике программы, если есть такая потребность (это полезно для написания гибко строящихся программ из «кирпичиков» – компонентов, которые могут добавляться/подгружаться и во время выполнения тоже). Такое функциональное деление удобно и для интерпретируемых языков, в которых, правда, к этим двум категориям можно добавить еще одну – состояние интерпретатора/ виртуальной машины в целом.

Отладочная информация, доступная программе

Начнем с самого простого: специальные методы __FILE__ и __LINE__ позволят определить и, скажем, вывести текущую точку исполнения программы. Например, для логирования.

def log msg, file, line

$stderr.puts "[#{file}:#{line}] #{msg}"

end

log 'Сообщение', __FILE__, __LINE__

Запустив пример, получим что-то вроде:

$ ruby intro01.rb

[intro01.rb:7] Сообщение

Почему 7, а не 5? В файле примера [1] присутствует еще две строки: первая – специальный комментарий с указанием кодировки, вторая для отступа. Внутри статей я подобные повторяемые везде вещи опускаю.

Конечно, в момент написания кода с __FILE__ и __LINE__ мы и так знаем, в каком файле и на какой строке находимся, но при дальнейшем редактировании эта строчка кода может оказаться где угодно.

Однако было бы здорово, если бы наш метод логирования как-то сам узнавал, откуда был вызван, без лишних параметров. И это вполне возможно – рассмотрим следующий пример.

def log msg

$stderr.puts "[#{caller[0]}] #{msg}"

end

 

def log2 msg

cl = caller_locations[0]

$stderr.puts "[#{cl.path}:#{cl.lineno}] #{msg}"

end

 

log 'Сообщение'

log2 'Сообщение'

Запустив его, мы получим следующее:

$ ruby intro02.rb

[intro02.rb:12:in '<main>'] Сообщение

[intro02.rb:14] Сообщение

Замечательные методы caller и caller_locations предоставляют нам весь стек вызовов в виде строк и специальных объектов класса Thread::Backtrace::Location соответственно. Второй вариант дает более гибкие возможности, но надо помнить, что он стал доступен только начиная с версии Ruby 2.0. На момент написания статьи версия 1.9.3 еще считается актуальной, впрочем, ее официальная поддержка заканчивается в феврале 2015-го. Тем не менее столкнуться с ее использованием в старом коде вполне вероятно.

Еще одно традиционное использование caller – при генерации исключений: очень часто исключения генерируются сразу после входа в метод, при проверке переданных параметров. И нас в этом случае неособо-то интересует место в программе, где эта проверка производится, – гораздо удобнее сразу указать на то место, откуда был вызван метод и где соответственно были заданы неверные параметры.

def divide a, b

if b == 0 || b == 0.0

raise StandardError, 'На ноль делить нельзя', caller

end

a / b

end

 

puts divide(1, 0)

Получаем:

$ ruby intro02a.rb

intro02a.rb:10:in '<main>': На ноль делить нельзя (StandardError)

Если мы закомментируем «, caller», то получим более длинный вывод:

$ ruby intro02a.rb

intro02a.rb:5:in 'divide': На ноль делить нельзя (StandardError)

from intro02a.rb:10:in `<main>'

Но все полезное, что мы могли бы узнать, изучив пятую строку и ее окружение, уже известно из сообщения...

В общем, такая генерация ошибок принята, если исключение бросается непосредственно по итогам проверки параметров, и не принята в других случаях, когда внутренняя логика метода, где произошло исключение, важна для понимания его причин.

RTTI

Во-первых, для любого объекта Ruby мы можем получить его класс и список методов. Во-вторых, классы и модули также дают информацию о методах, определяемых в них, а кроме того, и о константах. И, в-третьих, зная объект (класс) и имя метода, мы можем получить более подробную информацию, включая список параметров и место, где метод был определен.

Чтобы узнать класс, мы можем воспользоваться методами obj.class или singleton_class. Я не случайно написал в первом случае obj.class через точку, поскольку, даже находясь в контексте объекта [2], безточки вызвать его мы не можем – это будет воспринято интерпретатором как ключевое слово class. singleton_class, т.е. уникальный класс данного единичного объекта, нам обычно не нужен, если только мы не определяли какие-то уникальные методы для него.

Далее мы можем узнать всю цепочку наследования, в том числе включенные посредством include или extend модули. Для этого нам понадобится вызов метода ancestors у класса.

Чтобы получить список имен методов объекта, мы можем воспользоваться следующими методами класса Object:

  • private_methods – вернет массив имен приватных методов;
  • protected_methods – «защищенных»;
  • public_methods – публичных;
  • methods – публичных и защищенных вместе.

Разница приватных и «защищенных» методов в том, что первые могут быть вызваны только в контексте того объекта, для которого они вызываются, тогда как вторые – в контексте любого объекта тогоже класса. Дополнительно отмечу singleton_methods, возвращающий список методов, определенных только для конкретного объекта.

Классы и модули предоставляют также списки методов экземпляров, т.е. тех методов, которые могут быть вызваны для всех объектов, принадлежащих классу или включающих модуль:

  • private_instance_methods,
  • protected_instance_methods,
  • public_instance_methods,
  • instance_methods.

Связь между этими методами и описанными выше можно выразить так:

obj.xxx_methods == obj.singleton_class.xxx_instance_methods

Классы и модули (в отличие от объектов) позволяют получить еще и список констант. Для этого служит метод constants.

Все эти методы принимают один необязательный параметр, указывающий, нужно ли включать унаследованные методы (по умолчанию – true).

Продемонстрирую вышесказанное на примере:

def print_module mod, ancestors = true

if ancestors

puts "#{mod.class.name.downcase} #{mod.name}" +

" #{mod.ancestors.inspect}"

else

puts " #{mod.class.name.downcase} #{mod.name}"

end

mod.constants(false).each do |c|

puts " const #{c.inspect}"

end

mod.public_instance_methods(false).each do |m|

puts " #{m.inspect}"

end

if ancestors

ancs = mod.ancestors[1..-1]

ancs.each do |anc|

print_module anc, false

end

end

end

 

print_module Class

Запустив пример, мы получим довольно длинный вывод, приведу лишь его начало:

$ ruby intro03.rb

class Class [Class, Module, Object, Kernel, BasicObject]

:allocate

:new

:superclass

class Module

:freeze

:===

Константы в классах Class и Module не содержатся, но далее в выводе они появятся в большом количестве – константы, которые принято считать глобальными, относятся к классу Object.

Список имен – это, конечно, хорошо, но мало. Ruby позволяет получить и более подробную информацию о каждом методе. Для этого нам нужно получить соответствующий объект посредством method (для любого объекта) или instance_method (для классов и модулей). В первом случае мы получим объект класса Method, а во втором – UnboudMethod. Разница между ними в том, что первый привязан кобъекту и может быть вызван непосредственно, тогда как второй существует как бы сам по себе и для вызова должен быть предварительно привязан посредством bind. Но сейчас для нас это непринципиально, нас интересует информация, которую они предоставляют, а она одинакова.

Итак, что мы можем получить?

Во-первых, source_location, т.е. расположение исходников метода. Возвращает массив из двух значений – имя файла и номер строки, или nil, если метод определен во внешней библиотеке (Ruby позволяет писать «расширения» – специальные разделяемые библиотеки на компилируемых языках, в первую очередь, конечно, на C).

Во-вторых, owner – класс или модуль, в котором данный метод определен.

И, в-третьих, самое, пожалуй, интересное – это parameters – массив, описывающий все параметры метода, как они заданы в его определении. Возвращаемое значение – массив, в котором каждый параметр представлен массивом же из двух элементов: первый описывает вид параметра – обязательный, необязательный и т.д., а второй – его имя. Выглядит это примерно так:

def test a, b = 1, *c, d:, e: 2, **f, &g

end

p method(:test).parameters

Получим:

$ ruby intro04.rb

[[:req, :a], [:opt, :b], [:rest, :c], [:keyreq, :d], [:key, :e], [:keyrest, :f], [:block, :g]]

Впрочем, если метод определен во внешней библиотеке-расширении или в ядре языка, то есть опять же в скомпилированном коде, то Ruby знает о параметрах только их вид, и массивы в списке состоят изодного элемента. Таким образом, например, method(:method).parameters вернет [[:req]].

Исходя из этого мы можем написать функцию, восстанавливающую примерный заголовок метода из соответствующего ему объекта.

ARG_TEMPLATE = {

req: '%s',

opt: '%s = <..>',

rest: '*%s',

key: '%s: <..>',

keyreq: '%s:',

keyrest: '**%s',

block: '&%s'

}

 

def header mobj

anprefix = 'arg'

ancounter = 0

params = []

mobj.parameters.each do |param|

if param.size == 2

name = param[1]

else

name = anprefix + ancounter.to_s

ancounter += 1

end

params << (ARG_TEMPLATE[param[0]] % name)

end

result = "#{mobj.name}(#{params.join(', ')})"

if mobj.source_location

result += " [#{mobj.source_location.join(':')}]"

else

result += " [<binary>]"

end

result

end

 

def test a, b = 1, *c, d:, e: 2, **f, &g

end

 

puts header(method(:test))

puts header(method(:header))

puts header(method(:puts))

Запустив этот код, получим:

$ ruby intro05.rb

test(a, b = <..>, *c, d:, e: <..>, **f, &g) [intro05.rb:35]

header(mobj) [intro05.rb:13]

puts(*arg0) [<binary>]

К сожалению, конкретные значения, заданные для опциональных параметров по умолчанию, так просто выяснить не получится. Можно, правда, зная, где данный метод определен, распарсить текст программы, но это уже выходит за рамки нашей темы.

Картина в целом

Итак, мы можем посмотреть методы и константы для любого класса, модуля да и произвольного объекта (хотя это и редко может потребоваться). Однако при этом нам надо откуда-то знать о егосуществовании вообще. Неплохо было бы иметь возможность получить список классов и модулей, существующих в программе, и такая возможность есть – метод ObjectSpace.each_object позволяет перебрать все «живые» объекты, при необходимости отобрав их по классу. Поскольку в Ruby все – объекты, и при этом класс Class является наследником Module, мы можем спокойно использовать отбор по Module.

Таким образом, мы можем получить общую картину классов и модулей, использовав вышеприведенный метод header и немного переделав print_module:

def print_module mod

title = "#{mod.class.name.downcase} #{mod}"

if Class === mod && mod.superclass != nil

title += " < #{mod.superclass}"

end

puts title

puts " ancestors: #{mod.ancestors.join(', ')}"

puts " constants:"

mod.constants(false).each do |c|

puts " #{c}"

end

puts " class methods:"

mod.public_methods(false).each do |m|

puts " #{header(mod.method(m))}"

end

puts " instance methods:"

mod.public_instance_methods(false).each do |m|

puts " #{header(mod.instance_method(m))}"

end

puts ''

end

 

ObjectSpace.each_object(Module) do |mod|

print_module mod

end

Полный вывод такой программы получится совсем гигантским [3], поэтому приведу лишь малую часть, относящуюся к специально созданному для примера классу:

class Alpha

ALPHA = 1

class << self

attr_accessor :beta

end

 

def alpha a, b = 0, *c

p [a, b, c]

end

 

end

Вот, что мы для него получим:

class Alpha < Object

ancestors: Alpha, Object, Kernel, BasicObject

constants:

ALPHA

class methods:

beta() [intro06.rb:40]

beta=(arg0) [intro06.rb:40]

allocate() [<binary>]

new(*arg0) [<binary>]

superclass() [<binary>]

instance methods:

alpha(a, b = <..>, *c) [intro06.rb:43]

И что это нам дает?

Механизм интроспекции достаточно универсальный и «неприкладной», поэтому его, с одной стороны, трудно применить к чему-нибудь практическому «в лоб», а с другой, есть масса случаев, когда он втой или иной мере полезен. Я, пожалуй, выделю только некоторые направления.

Мы можем создавать прокси-объекты, полностью (снаружи) эквивалентные некоторым заданным, при этом возможные изменения исходных объектов, которые могут разрабатываться где-то в другом месте другими людьми, нас не волнуют, поскольку все делается автоматически.

В сложных системах с подключением сторонних скриптов в качестве плагинов или компонентов интроспекция дает больший контроль за совместимостью, проверкой функциональности и так далее. Например, введя изменения в интерфейс плагинов в очередной версии, мы можем автоматически определять устаревшие плагины (то есть с устаревшим интерфейсом) и как-то корректно их обрабатывать, например, создавая прокси-обертку (см. предыдущий пункт).

Развитые инструменты интроспекции можно (и нужно) использовать для отладки, логирования, автоматического тестирования и так далее. То есть в инструментах для создания и обслуживания кода ненужно отдельно парсить исходные тексты, интерпретатор уже делает это за нас, причем таким же образом, как и при «боевом» выполнении. Так что, если кто задумывает написать IDE для Ruby, этими средствами пренебрегать никак нельзя.

Кроме того, не стоит забывать, что в Ruby с его развитыми средствами метапрограммирования, изучая код, относящийся к какому-нибудь классу, мы никогда не можем быть уверены, что это весь код, относящийся к этому классу. Иными словами, получить полную информацию о том, как выглядит некий класс в определенный момент исполнения программы, мы можем только в этот момент исполнения.

Соответственно данные инструменты могут стать очень хорошим подспорьем как при обучении Ruby, так и при изучении чужого кода, особенно, если он плохо документирован, что, к сожалению, всовременной программной индустрии скорее норма, чем исключение.

В целом информация о структуре программы во время выполнения, хоть и не уменьшает сложность, однако дает дополнительные возможности с ней как-то справляться.

  1. Полные тексты примеров размещены на GitHub – https://gist.github.com/shikhalev/12090b4e64340d9d8c2e.
  2. Шихалев И. Блоки и контекст в Ruby, или Что стоит за идентификатором в данном окружении. // «Системный администратор», № 1-2, 2014 г. – С. 111-115 (http://samag.ru/archive/article/2622).
  3. Файл list.txt в https://gist.github.com/shikhalev/12090b4e64340d9d8c2e.

Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru