9

私は、YAML ファイルから入力を取得し、それらをオブジェクトに解析して、それらを実行させるアプリケーションに取り組んでいます。私が今抱えている唯一の問題は、YAML パーサーがオブジェクトの「初期化」メソッドを無視しているように見えることです。私はコンストラクターが、YAML ファイルに欠けているインスタンス変数をデフォルトで埋めるだけでなく、いくつかのものをクラス変数に格納することを期待していました。次に例を示します。

class Test

    @@counter = 0

    def initialize(a,b)
        @a = a
        @b = b

        @a = 29 if @b == 3

        @@counter += 1
    end

    def self.how_many
        p @@counter
    end

    attr_accessor :a,:b

end

require 'YAML'

a = Test.new(2,3)
s = a.to_yaml
puts s
b = YAML::load(s)
puts b.a
puts b.b
Test.how_many

puts ""

c = Test.new(4,4)
c.b = 3
t = c.to_yaml
puts t
d = YAML::load(t)
puts d.a
puts d.b
Test.how_many

上記が出力されることを期待していました:

--- !ruby/object:Test
a: 29
b: 3
29
3
2

--- !ruby/object:Test
a: 4
b: 3
29
3
4

代わりに私は得ました:

--- !ruby/object:Test
a: 29
b: 3
29
3
1

--- !ruby/object:Test
a: 4
b: 3
4
3
2

定義された初期化メソッドを使用せずにこれらのオブジェクトを作成する方法がわかりません。また、パーサーに初期化メソッドの使用を強制する方法があるかどうかも疑問に思っています。

4

3 に答える 3

10

initialize一般に、オブジェクトのインスタンス変数 (デフォルトの Yaml シリアライゼーションが格納するもの) と へのパラメーターの間に対応関係がないため、Yaml からのオブジェクトのデシリアライズでは、メソッドは使用されませんinitialize

initialize例として、次のようなを持つオブジェクトを考えてみましょう(他のインスタンス変数はありません)。

def initialize(param_one, param_two)
  @a_variable = some_calculation(param_one, param_two)
end

this のインスタンスが逆シリアル化されると、Yaml プロセッサには の値があります@a_variableが、initializeメソッドには2 つのパラメータが必要なため、呼び出すことができません。インスタンス変数の数がパラメーターの数と一致してinitializeいても、それらが対応しているとは限りません。また、一致したとしても、プロセッサーはそれらが渡されるべき順序を知りませんinitialize

Ruby オブジェクトを Yaml にシリアライズおよびデシリアライズするデフォルトのプロセスは、シリアライズ中にすべてのインスタンス変数を (名前とともに) 書き出し、デシリアライズ時にクラスの新しいインスタンスを割り当て、この新しいインスタンスに同じインスタンス変数を設定するだけです。

もちろん、このプロセスをより細かく制御する必要がある場合もあります。Psych Yaml プロセッサ (Ruby 1.9.3 のデフォルト) を使用している場合は、必要に応じて (シリアル化のencode_with場合) またはinit_with(逆シリアル化の場合) メソッドを実装する必要があります。

シリアル化の場合、Psych はencode_with、オブジェクトが存在する場合はそのメソッドを呼び出し、 object を渡しcoderます。このオブジェクトを使用すると、オブジェクトを Yaml で表現する方法を指定できます。通常は、オブジェクトをハッシュのように扱います。

デシリアライゼーションの場合、Psych はinit_with、オブジェクトにメソッドが存在する場合、上記の既定の手順を使用する代わりに、再度coderオブジェクトを渡してメソッドを呼び出します。今回はcoder、Yaml でのオブジェクト表現に関する情報が含まれます。

両方のメソッドを提供する必要はありません。必要に応じて、どちらか一方のみを提供できます。両方を指定すると、coder渡されるオブジェクトは、メソッドの実行後にinit_with渡されるオブジェクトと基本的に同じになります。encode_with

例として、(大規模な計算を回避するための最適化として) 他のインスタンス変数から計算されるいくつかのインスタンス変数を持つが、Yaml にシリアル化されるべきではないオブジェクトを考えてみましょう。

class Foo

  def initialize(first, second)
    @first = first
    @second = second
    @calculated = expensive_calculation(@first, @second)
  end

  def encode_with(coder)
    # @calculated shouldn’t be serialized, so we just add the other two.
    # We could provide different names to use in the Yaml here if we
    # wanted (as long as the same names are used in init_with).
    coder['first'] = @first
    coder['second'] = @second
  end

  def init_with(coder)
    # The Yaml only contains values for @first and @second, we need to
    # recalculate @calculated so the object is valid.
    @first = coder['first']
    @second = coder['second']
    @calculated = expensive_calculation(@first, @second)
  end

  # The expensive calculation
  def expensive_calculation(a, b)
    ...
  end
end

このクラスのインスタンスを Yaml にダンプすると、calculated値なしで次のようになります。

--- !ruby/object:Foo
first: 1
second: 2

この Yaml を Ruby にロードし直すと、作成されたオブジェクトに@calculatedインスタンス変数が設定されます。

必要に応じて 内から呼び出すこともできますが、クラスの新しいインスタンスを初期化することと、既存のインスタンスを Yaml から逆シリアル化することを明確に区別した方がよいと思います。代わりに、両方から呼び出すことができるメソッドに共通のロジックを抽出することをお勧めします。initializeinit_with

于 2012-10-16T19:54:43.263 に答える
3

-style インスタンス変数を使用する純粋な ruby​​ クラス@(コンパイルされた拡張機能や -style ではないものStruct) でのみこの動作が必要な場合は、次のように動作するはずです。YAML はallocate、インスタンスが別のオブジェクトのメンバーとしてネストされている場合でも、そのクラスのインスタンスをロードするときにクラス メソッドを呼び出すようです。したがって、 を再定義できallocateます。例:

class Foo
  attr_accessor :yaml_flag
  def self.allocate
    super.tap {|o| o.instance_variables.include?(:@yaml_flag) or o.yaml_flag = true }
  end
end
class Bar
  attr_accessor :foo, :yaml_flag
  def self.allocate
    super.tap {|o| o.instance_variables.include?(:@yaml_flag) or o.yaml_flag = true }
  end
end

>> bar = Bar.new
=> #<Bar:0x007fa40ccda9f8>
>> bar.foo = Foo.new
=> #<Foo:0x007fa40ccdf9f8>
>> [bar.yaml_flag, bar.foo.yaml_flag]
=> [nil, nil]
>> bar_reloaded = YAML.load YAML.dump bar
=> #<Bar:0x007fa40cc7dd48 @foo=#<Foo:0x007fa40cc7db90 @yaml_flag=true>, @yaml_flag=true>
>> [bar_reloaded.yaml_flag, bar_reloaded.foo.yaml_flag]
=> [true, true]

# won't overwrite false
>> bar.foo.yaml_flag = false
=> false
>> bar_reloaded = YAML.load YAML.dump bar
=> #<Bar:0x007fa40ccf3098 @foo=#<Foo:0x007fa40ccf2f08 @yaml_flag=false>, @yaml_flag=true>
>> [bar_reloaded.yaml_flag, bar_reloaded.foo.yaml_flag]
=> [true, false]

# won't overwrite nil
>> bar.foo.yaml_flag = nil
=> nil
>> bar_reloaded = YAML.load YAML.dump bar
=> #<Bar:0x007fa40cd73518 @foo=#<Foo:0x007fa40cd73360 @yaml_flag=nil>, @yaml_flag=true>
>> [bar_reloaded.yaml_flag, bar_reloaded.foo.yaml_flag]
=> [true, nil]

実際には上書きしたくない意味のある値である可能性があるため、意図的にブロックのo.nil?チェックを避けました。tapnil

最後の注意点:allocateサード パーティのライブラリ (または独自のコード) で使用される可能性があり、そのような場合はメンバーを設定したくない場合があります。割り当てを yaml の読み込みだけに制限したい場合はcaller、allocate メソッドのスタックをチェックして yaml が呼び出しているかどうかを確認するなど、より脆弱で複雑なことを行う必要があります。

私は ruby​​ 1.9.3 (psych を使用) を使用しており、スタックの上部は次のようになっています (パスプレフィックスは削除されています)。

psych/visitors/to_ruby.rb:274:in `revive'",
psych/visitors/to_ruby.rb:219:in `visit_Psych_Nodes_Mapping'",
psych/visitors/visitor.rb:15:in `visit'",
psych/visitors/visitor.rb:5:in `accept'",
psych/visitors/to_ruby.rb:20:in `accept'",
psych/visitors/to_ruby.rb:231:in `visit_Psych_Nodes_Document'",
psych/visitors/visitor.rb:15:in `visit'",
psych/visitors/visitor.rb:5:in `accept'",
psych/visitors/to_ruby.rb:20:in `accept'",
psych/nodes/node.rb:35:in `to_ruby'",
psych.rb:128:in `load'",
于 2012-10-16T17:39:26.527 に答える
1

from_yaml(入力)

YAML ファイル用の特別なローダー。Specificationオブジェクトが YAML ファイルから読み込まれると、通常の Ruby オブジェクトの初期化ルーチン (initialize) がバイパスされます。この方法はそれを補い、年代の異なる宝石を扱います。

入力は、YAML.load() が受け入れるものなら何でもかまいません: 文字列または IO。

これが、実行時に初期化メソッドが実行されていなかった理由ですYAML::Load

于 2012-10-15T20:31:27.347 に答える