2012-10-10 31 views
9

我正在研究一個應用程序,它接受來自YAML文件的輸入,將它們解析爲對象,並讓它們完成它們的工作。我現在遇到的唯一問題是YAML解析器似乎忽略了對象「初始化」方法。我指望構造函數填充YAML文件缺少缺省值的任何實例變量,並在類變量中存儲一些內容。這裏有一個例子:通過構造函數傳遞Ruby 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 

我不明白它是如何使這些對象,而不使用他們定義的初始化方法。我也想知道是否有強制解析器使用初始化方法。

+1

可能的重複:http:// stackoverflow。com/questions/1823386/calling-initialize-when-loading-an-object-serialized-with-yaml –

+0

該帖子很有幫助,但它並沒有完全解決我的問題。我解析的YAML流比單個對象更復雜,它是許多對象,其中一些由其他對象組成。 – clementine

+0

對不起,只是想。也許這更有幫助:要找出_why_'YAML :: load'不會調用'initialize',請檢查源代碼。 :P或許我們可以等待一位瞭解更多細節的回答者。我確實在腳本中嘗試了'將d.class == c.class'並且發現它是真的。所以你的問題+1。 –

回答

9

反序列化從YAML對象不使用initialize方法因爲一般情況下,對象實例變量(這是默認的Yaml序列化存儲的內容)與參數initialize之間沒有對應關係。

作爲一個例子,考慮具有一個initialize看起來像這樣的物體(沒有其他實例變量):

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

現在,當這樣的一個實例被反序列化時,YAML處理器具有用於@a_variable的值,但initialize方法需要兩個參數,所以它不能調用它。即使實例變量的數量與initialize的參數數量相匹配,它們並不一定就是它們對應的情況,即使它們確實處理器不知道它們應該傳遞給initialize的順序。

將Ruby對象序列化和反序列化爲Yaml的默認過程是在序列化過程中寫出所有實例變量(及其名稱),然後在反序列化時分配一個新的類實例並簡單地在此上設置相同的實例變量新實例。

當然有時你需要更多的控制這個過程。如果您使用的是Psych Yaml處理器(這是Ruby 1.9.3中的默認處理器),那麼您應該適當地實現encode_with(用於序列化)或或init_with(用於反序列化)方法。

對於序列化,Psych會調用對象的encode_with方法,如果它存在,則傳遞coder object。這個對象允許你指定對象應該如何在Yaml中表示 - 通常你只是把它當作一個散列來對待。

對於反序列化,精極度緊張將調用init_with方法,如果它是存在的對象,而不是使用上述的默認程序,再次通過一個coder對象上。這次coder將包含有關Yaml中對象表示的信息。

請注意,您不需要同時提供兩種方法,只需提供任意一種即可。如果您確實提供了這兩種方法,那麼您在init_with中通過的coder對象基本上與該方法運行後傳遞給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回紅寶石,所創建的對象將設置了@calculated實例變量。

如果你想讓你呼叫initializeinit_with內,但我認爲這將是更好地保持初始化類的實例,從YAML反序列化現有實例之間的明確分工。我建議將通用邏輯提取爲可以從兩者調用的方法,而不是

+0

我喜歡關於它爲什麼不使用初始化方法的解釋,這對我來說很有意義。接下來的問題就是如何改變從YAML加載對象時的行爲。在你的答案之上,我有兩個建議使用Object.to_yaml和Object.allocate。那它是哪個? Object.init_with,Object.allocate或Object.to_yaml?我猜你已經找到了這方面的文檔,你能給我發送鏈接嗎? – clementine

+0

@clementine建議使用'from_yaml'鏈接到Rubygems的api的答案,它不是Yaml本身的一部分。在另一個Stackoverflow問題的答案中,'from_yaml'只是作爲一個普通的方法使用,它不會與其他的yaml加載/轉儲集成。 '分配'是相當低水平的東西,我會建議避免干擾它。 – matt

+0

@clementine Psych的文檔相當糟糕,但我可以提供一個鏈接,指向init_with'的源代碼,如果它不存在,就會發生默認行爲:https://github.com/tenderlove/psych /blob/v1.3.4/lib/psych/visitors/to_ruby.rb#L291-300。 – matt

1

from_yaml(input)

對YAML文件的特殊加載器。 當從YAML文件加載Specification對象時,它會繞過正常的Ruby對象初始化例程(初始化)。這種方法彌補了這一點,並處理不同年齡的寶石。

輸入可以是YAML.load()接受的任何內容:String或IO。

這就是執行YAML::Load時初始化方法未運行的原因。

+0

不錯的發現。這是否意味着我可以通過覆蓋from_yaml類的方法解決這個問題,就像在這個答案中一樣:http://stackoverflow.com/questions/1823386/calling-initialize-when-loading-an-object-serialized-with-yaml < - 我遇到的問題是它從YAML流中加載單個對象。我要加載的yaml文件是一個複雜的對象對象,所有這些都需要通過默認構造函數 – clementine

+0

您可以使用[YAML :: load_documents](http://yaml4r.sourceforge.net/doc/page/ loading_yaml_documents.htm)來完成此操作。 –

+0

不,你想不明白...它不是在同一個文件中的多個yaml流。它是描述組合中對象分層結構的一個yaml流。因此,例如,我可能有一個包含水果,樹葉,也許鳥巢的對象樹,所有這些都需要被反序列化成一個紅寶石對象 – clementine

3

如果您只希望使用使用@風格的實例變量(不是來自已編譯的擴展的變量而不是Struct風格的)的純ruby類的這種行爲,則以下內容應該可以工作。當加載該類的實例時,即使該實例嵌套爲另一個對象的成員,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] 

我有意避免在tapo.nil?檢查,因爲nil,實際上可能是你不希望覆蓋一個有意義的值。

最後一個警告:allocate可能會被第三方庫(或您自己的代碼)使用,並且您可能不想在這些情況下設置成員。如果你想限制分配,只需要加載yaml,你將不得不做更脆弱和複雜的事情,比如在分配方法中檢查caller堆棧以查看yaml是否正在調用它。

我在紅寶石1.9.3(病有心理)和堆棧的頂部看起來像這樣(刪除路徑前綴):

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'", 
+0

我不熟悉Object.tap,但是,謝謝,這看起來正是什麼我在尋找。 – clementine

+0

@clementine'tap'真的很方便。參見[ruby-doc](http://ruby-doc.org/core-1.9.3/Object.html#method-i-tap)。它可以讓你對一個對象做事情,而不必將它分配給一個變量。 – Kelvin

相關問題