Memoize техники в Ruby и Ruby on Rails
декабря 24, 2010 | Published in Ruby, Ruby on Rails, Основы | 2 Comments
Заголовок не опечатка. Если вы не слышали о «memoizе», то я вам кратко расскажу. Memoiz’инг — это кэширование результата метода таким образом что когда вы вызываете метод в будущем, то он не выполняет заново все операции. Я покажу вам несколько различных техник для подобного кэширования, с комментариями за и против использования каждой техники.
Подготовка
Давайте создадим простое приложение Ruby on Rails на котором мы будем испытывать техники memoiz’инга. Нам понадобится лишь одна простая модель и таблица, которая вкоторой будут храниться имена и фамилии пользователей.
memoize$ rails g model user first_name:string last_name:string
invoke active_record
create db/migrate/20101204003605_create_users.rb
create app/models/user.rb
invoke test_unit
create test/unit/user_test.rb
create test/fixtures/users.yml
memoize$ rake db:migrate
(in /Users/bellmyer/Desktop/bellmyer/blog/memoize)
== CreateUsers: migrating ====================================================
— create_table(:users)
-> 0.0013s
== CreateUsers: migrated (0.0014s) ===========================================
Стандартный стиль использования
Вы вероятно случайно или намеренно сталкивались с простейшей формой memoize. Наша модель User имеет поля first_name и last_name. Давайте представим, что нам необходимо сделать метод который будет возвращать полное имя пользователя объединяя first_name с last_name:
class User < ActiveRecord::Base def full_name "#{first_name} #{last_name}" end end
Давайте проверим в консоли Ruby on Rails, что модель работает правильно:
memoize$ rails c
Loading development environment (Rails 3.0.1)
uby-1.9.2-p0 > user = User.new :first_name => ‘Bob’, :last_name => ‘Smith’
=> #
ruby-1.9.2-p0 > user.full_name
=> «Bob Smith»
Отлично! Теперь давайте воспользуемся простой формой memoiz’инга:
class User < ActiveRecord::Base def full_name @full_name ||= "#{first_name} #{last_name}" end end
В первый раз, когда этот метод был вызван, @full_name еще не существовало, поэтому будет выполнен код справа от ||=. В следующий раз когда вы вызовите этот метод, результат метода уже будет храниться в @full_name, поэтому методу нет необходимости заново создавать полное имя пользователя.
Способ получше
Это быстрый и грязный способ работает отлично для множества вещей, но работает он не во всех случаях. Давайте добавим к модели другой метод с memoize, и попытаемся найти в нем недостаток в логическом условии:
def has_full_name? @has_full_name ||= (!first_name.blank? && !last_name.blank?) end
Этот метод проверяет есть ли у пользователя и имя и фамилия. На первый взгляд все выглядит хорошо и нам кажется, что он будет отлично работать, но что если результат будет false? @has_full_name будет иметь значение false,а это значит что правая сторона уравнения будет запущена с метки каждый раз. Вместо проверки того, содержит @has_full_name значение true или false, нам необходимо проверить объявлена ли переменная @has_full_name следующим образом:
def has_full_name? return @has_full_name if defined?(@has_full_name) @has_full_name = (!first_name.blank? && !last_name.blank?) end
Теперь мы возвращаем значение переменной @has_full_name если она существует и в противном случае производим оценку. Теперь больше нет больше глюков с true/false. Этот метод более читабелен, но он не так краток и хорош, чтобы я его использовал.
Memoize для методов с аргументами
Что если у нас есть метод с аргументами? Мы обычно хотим чтобы при передаче одинаковых значений аргументов метод возвращал одинаковый результат. Таким образом почему бы не добавить нашей меморизации возможность кэширование для методов с аргументами?
def formal_name(salutation='Mr.', suffix=nil) @formal_name ||= {} return @formal_name[[salutation, suffix]] if @formal_name.has_key?([salutation, suffix]) @formal_name[[salutation, suffix]] = "#{salutation} #{full_name} #{suffix}" end
Эта реализация memoize немного сложнее. Так как этот метод может быть вызван с различными аргументами мы создаем хэш для хранения наших кэшируемых результатов. Мы используем метод has_key? для проверки того имеем ли мы уже значение для переданных аргументов.
Давайте испытаем наш новый метод:
memoize$ rails c user
Loading development environment (Rails 3.0.1) ruby-1.9.2-p0 >
user = User.new :first_name => ‘Bob’, :last_name => ‘Smith’
=> #
ruby-1.9.2-p0 > user.formal_name(‘Mr.’, ‘Jr.’)
=> «Mr. Bob Smith Jr.»
ruby-1.9.2-p0 > user.first_name = ‘John’
=> «John»
ruby-1.9.2-p0 > user.formal_name(‘Mr.’, ‘Jr.’)
=> «Mr. Bob Smith Jr.»
Теперь вы знаете, что memoize работает, потому, что несмотря на то, что я изменил первое имя метод formal_name продолжил возвращать нам тот же результат.
Используем модуль Memoizable из Ruby on Rails
Наш «лучший способ» замечательно работает и мы даже добавили возможность кеширования результатов для методов с аргументами. Но требуется много сил и времени чтобы добавить memoize для каждого метода, кроме того это увеличивает размер кода и уменьшает его читабельность.
Если вы используете Ruby on Rails версии 2.2 и выше, то вы можете поиметь хорошую выгоду от использования модуля Memoizable, который возьмет на себя все работу по добавлению кэширования в методы. Давайте добавим к нашей модели метод, который использует модуль Memoizable:
class User < ActiveRecord::Base extend ActiveSupport::Memoizable # здесь всякие-разные методы... def initials(middle_initial) first_name[0] + middle_initial + last_name[0] end memoize :initials end
Очень просто! Метод memoize берет заботу о добавлении кэширования результатов методов. Все, что вам необходимо — это расширить ваш класс с помощью модуля Memoizable и передать методу memoize имена методов, для которых необходимо добавить memoiz’ацию. В отличии от других способов memoize’инга обсуждаемых выше, этот использует модуль из ActiveSupport, поэтому этот способ больше подходит для проектов разрабатываемых на Ruby on Rails.
Хорошая новость состоит в том, что если вы используете Ruby on Rails в своем проекте, то вы можете использовать Memoizable везде, где вам того хочется. Давайте добавим клас который не наследуется от Active Record в папку lib.
# lib/my_number.rb class MyNumber extend ActiveSupport::Memoizable attr_accessor :x def initialize(x) @x = x end def plus(y) @x + y end memoize :plus end
А вот и доказательство того, что это работает:
memoize$ rails c
require ‘myLoading development environment (Rails 3.0.1)
ruby-1.9.2-p0 > require ‘my_number’
=> ["MyNumber"]
ruby-1.9.2-p0 > num = MyNumber.new(5)
=> #
ruby-1.9.2-p0 > num.plus 2
=> 7
ruby-1.9.2-p0 > num.x = 0
=> 0
ruby-1.9.2-p0 > num.plus 2
=> 7
Это самое лучшее решение для memoiz’инга если вы используете Ruby on Rails и я очень рекомендую его вам. Чем меньше кода вы вводите сами — тем меньше в вашем коде ошибок.
Кое-что, что вы еще должны знать
Во-первых, как упоминалось выше, модуль Memoizable доступен только в части ActiveSupport Ruby on Rails. Однако предыдущие примеры являются чистым Ruby и могут быть использованы в любом проекте на Ruby.
Во-вторых, вам не нужна mamoiz’ация абсолютно везде. Привожу ситуации где кэширование результатов недопустимо:
— Метод простой и memoiz’ация не сыграет особой роли
— Результат метода нуждается в изменении во время жизни своего объекта.
— Метод вряд ли будет вызван снова с теми же параметрами.
— Есть огромное количество комбинаций параметров которые будут вызываться и это съест слишком много памяти для хранения всех результатов.
Оригинал статьи на английском:
Ваши комментарии — лучшая благодарность автору блога.
Нашли ошибку — сообщите о ней.
января 5, 2011 at 00:29 (#)
Великолепно!
Спасибо за перевод, не знал об этой технике раньше.
Переписал часть кода своего проекта, избавившись от неуклюжего кэширования в угоду этой memoiz’ации. Работает просто идеально. :)
января 5, 2011 at 07:46 (#)
И вам спасибо за спасибо=)
Кстати, у вас замечательная преветствующая страница на сайте. Мне понравилась.