2012-10-10 19 views
9

Estoy trabajando en una aplicación que toma datos de un archivo YAML, los analiza en objetos y les permite hacer lo suyo. El único problema que estoy teniendo ahora, es que el analizador YAML parece ignorar el método de "inicialización" de los objetos. Estaba contando con el constructor para completar las variables de instancia que faltaban en el archivo YAML con los valores predeterminados, así como para almacenar algunas cosas en las variables de la clase. Aquí está un ejemplo:Analizador de Ruby YAML al pasar el constructor

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 

lo que habría esperado lo anterior a la salida:

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

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

En cambio me dieron:

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

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

No entiendo cómo se hace que estos objetos sin usando su método de inicialización definido. También me pregunto si hay alguna forma de forzar al analizador a utilizar el método de inicialización.

+1

Posible duplicado: http: // stackoverflow.com/questions/1823386/calling-initialize-when-loading-an-object-serialized-with-yaml –

+0

Esa publicación es útil, sin embargo, no resuelve por completo mi problema. La secuencia YAML que estoy analizando es más compleja que un solo objeto, son muchos objetos, algunos de los cuales están compuestos por otros. – clementine

+0

Lo siento, solo lo intento. Tal vez esto sea más útil: para averiguar por qué _ 'YAML :: load' does _not_ call' initialize', compruebe la fuente. : P O tal vez podemos esperar un respondedor que sepa más de los detalles. Intenté 'pone d.class == c.class' en tu script y encontré que era verdadero. Entonces +1 en tu pregunta. –

Respuesta

9

deserializar un objeto de Yaml no utiliza el método initialize porque en general no hay correspondencia entre las variables de instancia de objetos (que es lo que almacena la serialización Yaml predeterminada) y los parámetros a initialize.

Como un ejemplo, considere un objeto con una initialize que tiene este aspecto (sin otras variables de instancia):

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

Ahora, cuando se deserializa una instancia de este, el procesador Yaml tiene un valor para @a_variable , pero el método initialize requiere dos parámetros, por lo que no puede llamarlo. Incluso si el número de variables de instancia coincide con el número de parámetros a initialize, no es necesariamente el caso de que se correspondan, e incluso si lo hicieron, el procesador no sabe el orden en que deben pasar a initialize.

El proceso predeterminado para serializar y deserializar un objeto Ruby en Yaml es escribir todas las variables de instancia (con sus nombres) durante la serialización, luego al deserializar asignar una nueva instancia de la clase y simplemente establecer las mismas variables de instancia en este nueva instancia.

Por supuesto, a veces necesita más control de este proceso. Si está utilizando el procesador Psych Yaml (que es el predeterminado en Ruby 1.9.3), entonces debe implementar los métodos encode_with (para la serialización) o init_with (para la deserialización) según corresponda.

Para la serialización, Psych llamará al método encode_with de un objeto si está presente, pasando un coder object. Este objeto te permite especificar cómo se debe representar el objeto en Yaml, normalmente lo tratas como un hash.

Para la deserialización, Psych llamará al método init_with si está presente en su objeto en lugar de utilizar el procedimiento predeterminado descrito anteriormente, pasando de nuevo un objeto coder. Esta vez, el coder contendrá la información sobre la representación de los objetos en Yaml.

Tenga en cuenta que no necesita proporcionar ambos métodos, solo puede proporcionar uno si lo desea. Si proporciona ambos, el objeto coder que obtenga pasado en init_with será esencialmente el mismo que pasó a encode_with después de que se haya ejecutado ese método.

Como ejemplo, considere un objeto que tiene algunas variables de instancia que se calculan a partir de otras (tal vez como una optimización para evitar un cálculo grande), pero no se debe serializar en 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 

Al volcar una instancia de esta clase para Yaml, que se verá algo como esto, sin el valor calculated:

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

Al cargar este Yaml de nuevo en Rubí, el objeto creado se tener la variable de instancia @calculated establecida.

Si usted quería podía llamada initialize desde dentro init_with, pero yo creo que sería mejor mantener la separación clara entre la inicialización de un nueva instancia de la clase, y deserializar un ejemplo existente de Yaml . Recomendaría extraer la lógica común en métodos que se pueden llamar desde ambos en su lugar,

+0

Me gusta la explicación sobre por qué no usa el método de inicialización, lo cual tiene sentido para mí. La pregunta entonces es cómo cambiar el comportamiento de cómo se crea un objeto cuando se carga desde YAML. Sobre su respuesta tengo dos que sugieren el uso de Object.to_yaml y Object.allocate. Entonces, ¿cuál es? Object.init_with, Object.allocate u Object.to_yaml? Supongo que encontraste documentación sobre esto, ¿me puedes enviar el enlace? – clementine

+0

@clementine la respuesta que sugiere utilizar enlaces 'from_yaml' a la API para Rubygems, no es parte de Yaml. En la respuesta de la otra pregunta de Stackoverflow, 'from_yaml' se usa simplemente como un método simple, no se integrará con el resto de la carga y descarga yaml. 'allocate' es bastante poco nivel, recomendaría evitar interferir con él. – matt

+0

@clementine La documentación para Psych es bastante pobre, pero puedo ofrecer un enlace a la fuente donde se comprueba 'init_with' y se produce el comportamiento predeterminado si no está allí: https://github.com/tenderlove/psych /blob/v1.3.4/lib/psych/visitors/to_ruby.rb#L291-300. – matt

1

from_yaml(input)

cargador especial para los archivos YAML. Cuando se carga un objeto Specification desde un archivo YAML, pasa por alto la rutina normal de inicialización de objetos de Ruby (inicialización). Este método lo compensa y trata con gemas de diferentes edades.

entrada puede ser cualquier cosa que YAML.load() acepte: Cadena o IO.

Este es el motivo por el que el método de inicialización no se ejecutó cuando ejecutó YAML::Load.

+0

Buen descubrimiento. ¿Significa eso que puedo resolver esto anulando el método de clase from_yaml ?, como en esta respuesta: http://stackoverflow.com/questions/1823386/calling-initialize-when-loading-an-object-serialized-with-yaml < - El problema que tuve con esto es que estaba cargando un solo objeto de una secuencia YAML. El archivo yaml que voy a cargar es una jerarquía compleja de objetos, todos los cuales deben pasar por el constructor predeterminado. – clementine

+0

Puede usar [YAML :: load_documents] (http://yaml4r.sourceforge.net/doc/page/ loading_yaml_documents.htm) para lograr esto. –

+0

No se lo pierda, entiéndalo ... no se trata de múltiples flujos yaml en el mismo archivo. Es una secuencia yaml que describe una jerarquía de objetos en composición. Entonces, por ejemplo, puedo tener un árbol de objetos que contenga fruta, hojas, tal vez un nido de pájaros, todo lo cual necesita ser extraterritorializado en un objeto de rubí – clementine

3

Si solo desea este comportamiento con las clases de ruby ​​puro que usan las variables de instancia de estilo @ (no las de las extensiones compiladas y no Struct -style), lo siguiente debería funcionar. YAML parece llamar al método de clase allocate al cargar una instancia de esa clase, incluso si la instancia está anidada como miembro de otro objeto. Entonces podemos redefinir allocate. Ejemplo:

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] 

que evita intencionadamente un cheque o.nil? en los bloques tap porque nil pueden ser en realidad un valor significativo que no desea sobrescribir.

Una última advertencia: allocate puede ser utilizada por bibliotecas de terceros (o por su propio código), y es posible que no desee configurar los miembros en esos casos. Si desea restringir la asignación, simplemente cargar yaml, tendrá que hacer algo más frágil y complejo como comprobar la pila caller en el método allocate para ver si yaml lo está llamando.

Estoy en rubí 1.9.3 (con psych) y la parte superior de la pila se parece a esto (prefijo de ruta eliminado):

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

No estoy familiarizado con Object.tap, pero gracias, esto se ve exactamente estaba buscando. – clementine

+0

@clementine 'tap' es realmente conveniente. Ver [el ruby-doc] (http://ruby-doc.org/core-1.9.3/Object.html#method-i-tap). Le permite hacer cosas a un objeto sin tener que asignarlo a una variable. – Kelvin

Cuestiones relacionadas