2012-10-10 35 views
9

Tôi đang làm việc trên một ứng dụng lấy đầu vào từ một tệp YAML, phân tích chúng thành các đối tượng và cho phép chúng làm việc của chúng. Vấn đề duy nhất tôi đang gặp phải là trình phân tích cú pháp YAML dường như bỏ qua các phương thức "khởi tạo" đối tượng. Tôi đã đếm trên hàm tạo để điền vào bất kỳ biến mẫu nào mà tệp YAML thiếu mặc định, cũng như lưu trữ một số thứ trong các biến lớp. Dưới đây là một ví dụ:Trình phân tích cú pháp YAML của Ruby bằng cách truyền hàm tạo

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 

tôi dự kiến ​​sẽ có ở trên để đầu ra:

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

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

Thay vào đó tôi nhận:

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

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

Tôi không hiểu tại sao nó làm cho các đối tượng này không sử dụng phương thức khởi tạo đã xác định của chúng. Tôi cũng tự hỏi nếu có anyway để buộc các phân tích cú pháp để sử dụng phương pháp khởi tạo.

+1

Có thể trùng lặp: http: // stackoverflow.com/questions/1823386/calling-initialize-when-loading-an-object-serialized-with-yaml –

+0

Bài đăng đó hữu ích, tuy nhiên nó không hoàn toàn giải quyết được vấn đề của tôi. Dòng YAML mà tôi phân tích từ phức tạp hơn một đối tượng duy nhất, đó là nhiều đối tượng, một số trong đó bao gồm các đối tượng khác. – clementine

+0

Rất tiếc, chỉ cần thử. Có lẽ điều này hữu ích hơn: Để tìm hiểu _why_ 'YAML :: load' không _not_ gọi' initialize', hãy kiểm tra nguồn. : P Hoặc có lẽ chúng ta có thể chờ một người trả lời biết nhiều hơn về các chi tiết. Tôi đã thử 'đặt d.class == c.class' trong kịch bản của bạn và thấy nó đúng. Vì vậy, +1 về câu hỏi của bạn. –

Trả lời

9

deserializing một đối tượng từ YAML không sử dụng phương pháp initialize bởi vì nói chung không có sự tương ứng giữa các biến đối tượng của đối tượng (đó là những gì các cửa hàng tuần tự hóa Yaml mặc định) và các tham số cho initialize.

Như một ví dụ, hãy xem xét một đối tượng với một initialize trông như thế này (không có biến dụ khác):

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

Bây giờ khi một thể hiện của điều này được deserialized, bộ vi xử lý YAML có giá trị cho @a_variable , nhưng phương pháp initialize yêu cầu hai thông số, do đó, nó không thể gọi nó. Ngay cả khi số lượng biến mẫu khớp với số lượng tham số đến initialize thì không nhất thiết phải là trường hợp chúng tương ứng và ngay cả khi chúng đã xử lý không biết thứ tự chúng được chuyển đến initialize. Quy trình mặc định để tuần tự hóa và deserializing một đối tượng Ruby thành Yaml là viết ra tất cả các biến cá thể (với tên của chúng) trong quá trình tuần tự, sau đó khi deserializing cấp phát một thể hiện mới của lớp và chỉ cần đặt cùng một biến mẫu trên ví dụ mới.

Tất nhiên đôi khi bạn cần kiểm soát nhiều hơn quá trình này. Nếu bạn đang sử dụng bộ xử lý Psych Yaml (đó là mặc định trong Ruby 1.9.3) thì bạn nên thực hiện các phương thức encode_with (cho tuần tự hóa) hoặc hoặc init_with (để giải tuần tự) khi thích hợp.

Để tuần tự hóa, Psych sẽ gọi phương thức encode_with của đối tượng nếu có, đi qua coder object. Đối tượng này cho phép bạn chỉ định cách đối tượng sẽ được biểu diễn trong Yaml - thông thường bạn chỉ xem nó như một băm.

Để hủy tuần tự hóa, Psych sẽ gọi phương thức init_with nếu phương thức này xuất hiện trên đối tượng của bạn thay vì sử dụng quy trình mặc định được mô tả ở trên, một lần nữa chuyển đối tượng coder. Lần này, coder sẽ chứa thông tin về các đối tượng đối tượng trong Yaml.

Lưu ý rằng bạn không cần phải cung cấp cả hai phương pháp, bạn chỉ có thể cung cấp cả hai phương pháp nếu bạn muốn. Nếu bạn cung cấp cả hai, đối tượng coder mà bạn nhận được qua số init_with về cơ bản sẽ giống như đối tượng được chuyển đến encode_with sau khi phương thức đó đã chạy. Ví dụ, hãy xem xét một đối tượng có một số biến mẫu được tính toán từ những người khác (có lẽ là tối ưu hóa để tránh tính toán lớn), nhưng không nên được tuần tự hóa thành 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 

Khi bạn đổ một thể hiện của lớp này để YAML, nó sẽ giống như thế này, mà không có giá trị calculated:

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

Khi bạn tải YAML này trở lại vào Ruby, các đối tượng được tạo ra sẽ có bộ biến mẫu @calculated.

Nếu bạn muốn bạn thể gọi initialize từ bên trong init_with, nhưng tôi nghĩ rằng nó sẽ được tốt hơn để giữ một sự tách biệt rõ ràng giữa khởi tạo một mới thể hiện của lớp, và deserializing một dụ hiện từ YAML . Tôi khuyên bạn nên trích xuất logic chung thành các phương thức có thể được gọi từ cả hai thay vào đó,

+0

Tôi thích giải thích về lý do tại sao nó không sử dụng phương thức khởi tạo, điều này có ý nghĩa với tôi. Câu hỏi sau đó sẽ trở thành cách thay đổi hành vi của một đối tượng được tạo ra khi nó được nạp từ YAML như thế nào. Trên câu trả lời của bạn, tôi có hai đề nghị sử dụng Object.to_yaml và Object.allocate. Vì vậy, đó là nó ?? Object.init_with, Object.allocate, hoặc Object.to_yaml? Tôi đoán bạn đã tìm thấy tài liệu về điều này, bạn có thể gửi cho tôi liên kết không? – clementine

+0

@clementine câu trả lời gợi ý sử dụng liên kết 'from_yaml' cho api cho Rubygems, nó không phải là một phần của chính Yaml. Trong câu trả lời từ câu hỏi Stackoverflow khác, 'from_yaml' chỉ được sử dụng làm phương thức đơn giản, nó sẽ không tích hợp với phần còn lại của tải/bán phá giá yaml. 'allocate' khá thấp, tôi khuyên bạn nên tránh can thiệp vào nó. – matt

+0

@clementine Tài liệu cho Psych khá nghèo, nhưng tôi có thể cung cấp liên kết đến nguồn nơi 'init_with' được chọn và hành vi mặc định xảy ra nếu nó không có ở đó: https://github.com/tenderlove/psych /blob/v1.3.4/lib/psych/visitors/to_ruby.rb#L291-300. – matt

1

from_yaml(input)

nạp đặc biệt cho các tập tin YAML. Khi một đối tượng Specification được tải từ một tệp YAML, nó bỏ qua thường lệ khởi tạo đối tượng Ruby (khởi tạo). Phương pháp này bù đắp cho điều đó và giao dịch với đá quý ở các độ tuổi khác nhau.

đầu vào có thể là bất kỳ thứ gì mà YAML.load() chấp nhận: Chuỗi hoặc IO.

Đây là lý do khiến phương pháp khởi tạo không được chạy khi bạn thực hiện YAML::Load.

+0

Tìm tốt. Điều đó có nghĩa là tôi có thể giải quyết điều này bằng cách ghi đè phương thức lớp from_yaml ?, như trong câu trả lời này: http://stackoverflow.com/questions/1823386/calling-initialize-when-loading-an-object-serialized-with-yaml < - Vấn đề tôi gặp phải là nó đang tải một đối tượng đơn lẻ từ luồng YAML. Tệp yaml mà tôi sẽ tải là một chế độ thừa kế phức tạp của các đối tượng, tất cả đều cần phải đi qua hàm tạo mặc định – clementine

+0

Bạn có thể sử dụng [YAML :: load_documents] (http://yaml4r.sourceforge.net/doc/page/ loading_yaml_documents.htm) để thực hiện điều này. –

+0

Bạn không bỏ lỡ hiểu ... nó không phải là nhiều luồng yaml trong cùng một tệp. Nó là một luồng yaml mô tả một hệ thống phân cấp các đối tượng trong bố cục. Vì vậy, ví dụ, tôi có thể có một cây đối tượng có chứa trái cây, lá, có thể là một tổ chim, tất cả đều cần phải được unserialized vào một đối tượng ruby ​​ – clementine

3

Nếu bạn chỉ muốn hành vi này với các lớp ruby ​​thuần túy sử dụng @ biến mẫu kiểu (không phải từ biến thể được biên dịch chứ không phải kiểu Struct), các thao tác sau sẽ hoạt động. YAML dường như gọi phương thức lớp allocate khi tải một cá thể của lớp đó, ngay cả khi cá thể được lồng vào nhau như một thành viên của một đối tượng khác. Vì vậy, chúng tôi có thể xác định lại allocate. Ví dụ:

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] 

tôi cố ý tránh một tấm séc o.nil? trong tap khối vì nil thực sự có thể là một giá trị có ý nghĩa rằng bạn không muốn ghi đè lên.

Một cảnh báo cuối cùng: allocate có thể được sử dụng bởi thư viện của bên thứ ba (hoặc bằng mã của riêng bạn) và bạn có thể không muốn đặt thành viên trong các trường hợp đó. Nếu bạn muốn hạn chế phân bổ, chỉ để tải yaml, bạn sẽ phải làm điều gì đó mỏng manh và phức tạp hơn như kiểm tra ngăn xếp caller trong phương thức phân bổ để xem liệu yaml có đang gọi nó hay không.

Tôi đang trên ruby ​​1.9.3 (với psych) và phía trên cùng của ngăn xếp trông như thế này (con đường tiền tố bị loại bỏ):

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

Tôi không quen thuộc với Object.tap, nhưng cảm ơn, điều này có vẻ chính xác những gì Tôi đang tìm kiếm. – clementine

+0

@clementine 'tap' thực sự tiện lợi. Xem [ruby-doc] (http://ruby-doc.org/core-1.9.3/Object.html#method-i-tap). Nó cho phép bạn làm việc với một đối tượng mà không phải gán nó cho một biến. – Kelvin

Các vấn đề liên quan