Libro Ruby
Short Description
Descripción: ruby...
Description
Versión 0.22
Temario: Temario: 1) Motivación Ruby 2) Setup Instalando Ruby en OSX Instalando RVM y ruby Introducción a RVM 3) Introducción a Ruby El Intérprete Entrada y salida de datos Introducción a tipos de datos Tipos de lenguaje Tipos de datos frecuentes. Preguntas 4) Operadores Operadores aritméticos Operadores de comparación Operadores de asignación Operadores lógicos Preguntas 5) Trabajando fuera del intérprete. Creando scripts Analicemos el resultado Preguntas: 6) Condiciones y loops Condicionales Ciclos
While Until For Ejercicios para resolver Break Ciclos anidados. Desafíos Preguntas 7) Métodos Definiendo un método Llamando una función Parámetros Métodos con parámetros valores por defecto El scope de las variables Variables globales vs locales Métodos recursivos Preguntas 8) Arrays Creando arrays Índices Contando elementos Los arrays como contenedores Mutabilidad en los arrays Operaciones funcionales sobre los arrays Select Inject Group_by Contando elementos con group by Ordenando arreglos Los arrays como conjuntos
Los arrays como una pila (o stack) Arrayception (arrays dentro de arrays) Preguntas 9) Hashs Creando un hash Agregando y cambiando elementos a un hash Removiendo elementos de un hash Iterando un hash Convirtiendo un hash en un arreglo. Convirtiendo un arreglo en un hash Preguntas 10) Objetos en ruby Requisitos Para que sirven los objetos Los tres conceptos más importantes Identidad, Estados y Comportamientos Métodos de instancia Getters y Setters
Preguntas 11) Objetos y arreglos Ejercicios resueltos Preguntas 12) Bloques, Procs y Lambdas Bloques Procs Lambdas 13) Archivos
Preguntas
14) Manejo de errores Introducción a manejo de errores Manejando las excepciones Manejo de errores y archivos Desafío de Puntos y líneas Preguntas 15) Expresiones regulares Patrones 16) Scrapping Introducción a Mechanize Navegando por los tags Navegando en google Lyrics Desafío Preguntas 17) Objetos avanzado Self y main Llamados implícitos vs explícitos Métodos públicos privados y protegidos. Métodos protegidos Herencia Clase, Superclase, y tipo Herencia de estados, métodos y super Herencia de los métodos privados y protegidos 18) Módulos y Mixins Módulos vs clases Módulo en archivo externo Includes Extends Herencia + includes
Clases dentro de los módulos Módulos dentro de los módulos Desafio Preguntas 19) Testing Mi primer test ¿Qué testear? TDD BDD Ejercicios prácticos
1) Motivación Este es el segundo libro de la colección de Fullstack de Desafío Latam, el libro fue creado con el propósito de ayudar a los alumnos de desafiolatam a apoyar sus clases, y entender a profundidad como funciona Ruby, para posteriormente sacarle todo el provecho a Ruby on Rails. Este libro incluye todos los conceptos claves de ruby, desde las instrucciones báscias, hasta el manejo de bloques, procs y objetos. Este libro fue escrito y pensado para aquellas personas que ya tengan bases en pensamiento analítico y resolución de problemas básicos de programación, si no es tu caso puedes empezar con el primer libro de la colección.
Ruby Ruby fue creado por Yukihiro Matsumoto, también conocido como Matz, dentro de las característica tiene: Sintaxis simple Programación orientado a objeto (classes, methods, objects) Características especiales de orientación a objetos (Mix-ins, métodos singleton methods, etc) Sobrecarga de operadores Manejo de errores Iteradores Closures Colector de basura Carga dinámica Multiplataforma (Unices, Windows, DOS, OSX, OS/2, Amiga, …)
2) Setup La instalación de ruby depende del sistema operativo, tanto en OSX como en Linux se recomienda instalar RVM (Ruby Virtual Machine), el cual permite tener instalada diferentes versiones de ruby en el sistema sin conflicto, lo que es muy útil a la hora de trabajar en diversos proyectos. Se desaconseja trabajar con Windows, si bien ruby funciona bien en windows muchas componentes (gemas) no funcionan, pero dentro del alcance de este libro no habrá problemas con ningún sistema operativo.
Instalando Ruby en OSX Previo a otras instalaciones necesitaremos el command line tools, para eso:
1
xcode-select --install
Se puede encontrar más información en la página oficial: http://railsapps.github.io/xcode-command-linetools.html
Instalando RVM y ruby La página oficial de RVM contiene las instrucciones de instalación, que son preferibles a las que siguen debido a que estas cambian en el tiempo.
1
\curl -sSL https://get.rvm.io | bash -s stable --ruby
Existen otros programas similares a RVM, los otros dos más famosos son Rbenv y CHruby. Para el propósito de este libro es posible usar cualquiera, pero para el siguiente libro de la colección, el libro de Rails se ocupará RVM para automatizar el deployment con Capistrano así que es mejor instalar RVM desde ya.
Introducción a RVM Con RVM instalado (es necesario cerrar la consola y abrirla de nuevo después de instalarlo) rvm list muestra todas las versiones de ruby que tienes en el sistema rvm install 2.3.1 instala
la versión 2.3.1 de ruby rvm use 2.3.1 marca como default la versión 2.3.1 de ruby. Finalmente podemos saber si la versión que tenemos de ruby ocupando:
1
ruby -v
La versión de ruby que utilizaremos en este libro es la 2.3.1.
3) Introducción a Ruby El Intérprete La forma más rápida de empezar a trabajar con Ruby es con el intérprete, para entrar a el debemos abrir la terminal y escribir
1
irb
Dentro del intérprete ya no podemos escribir comandos bash como cd o ls en su lugar debemos escribir métodos y operadores de ruby por ejemplo podemos escribir 2+2 y ver como output 4 Para salir del intérprete debemos escribir exit y con eso volveremos a la terminal bash .
Introducción a variables Podemos almacenar valores en variables simplemente asignándolos, ejemplo a = 2 Con los valores asignados podemos realizar las mismas operaciones que en otros lenguajes, como en el siguiente caso:
1 2 3
a = 2 b = 3 a + b
En el intérprete cuando realizamos cualquier operación obtenemos el output inmediatamente, pero esto no sucederá cuando creemos nuestros programas fuera del intérprete como normalmente se hace.
Recuperando la última línea En el intérprete podemos recuperar el resultado de la última línea ejecutada con _ , esto es exclusivamente para el intérprete.
1 2 3 4 5
a b a c #
= 2 = 3 + b = _ => 5
Entrada y salida de datos Con ruby podemos mostrar datos ocupando la instrucción puts y capturar datos ocupando la instrucción gets, de esta forma:
1 2
a = gets puts a
Antes de mostrar el valor, la consola quedará bloqueada hasta que nosotros ingresemos una secuencia de caracteres, ya sea números o letras.
El problema de gets como lo podemos ver en la imagen es que también captura el salto de línea que uno ingresa al presionar la tecla enter. Para evitar este problema podemos ocupar el método chomp:
1 2
a = gets.chomp puts a
Introducción a tipos de datos En ruby existen distintos tipos de datos, aunque todo son objetos así que lo correcto sería decir tipos de objeto, cada uno de estos tipos se comporta de forma distinta, y este es el tema que abordaremos a continuación. En ruby podemos saber de que tipo es un objeto con el método .class
1
a = gets.chomp
2 3
puts a.class # String
En este caso veremos que el resultado es string sin importar si ingresamos un número o una secuencia de caracteres. Ahora si probamos con un número, obtendríamos:
1 2
b = 2 puts a.class
Veremos que b es del tipo de dato Fixnum, o sea un número, hemos descubierto algo importante que 2 no es lo mismo que "2" y por lo mismo no podemos sumarlos.
1
2+"2"
1 2 3 4
TypeError: String can't be coerced into Fixnum from (irb):5:in `+' from (irb):5 from /Users/gonzalosanchez/.rvm/rubies/ruby-2.2.3/bin/irb:15:in `'
Antes de ahondar sobre los distintos tipos de datos es necesario que hagamos un pequeño de repaso de los tipos de lenguaje de programación para que podemos entender mejor como funciona ruby.
Tipos de lenguaje Languages de tipo estático vs dinámicos En ruby los tipos de datos se revisan en tiempo de ejecución, o sea dinámico, esto quiere decir que si hay una operación que sume datos que no son sumables, como por ejemplo "2" + 2 no lo sabremos hasta que se lea exactamente esa línea, pero además tiene otra ventaja, en algunos lenguajes como C (de tipado estático) uno tiene que definir el tipo de variable, por ejemplo:
1
int c = 5;
Luego cambiar el valor que contiene c a algo que no sea un número entero causaría un error, incluso tampoco podríamos decir específicamente que ahora c va a contener una variable de otro tipo, en cambio en los lenguajes dinámicos como ruby se puede hacer:
1 2
c = 5 c = "hola"
Independiente de que sea mucho más fácil para un programador trabajar de esta forma, también hace el código más propenso a errores, pero para evitar varios de estos errores hay otra característica muy potente que tiene ruby, llamada duck typing.
Strong Typing vs Weak Typing Otra forma de catalogar los lenguajes además de estático o dinámico es según el tipo de conversión que hacen sobres sus datos, los lenguajes de programación pueden ser de tipeo fuerte, débil o tipo pato. En tipado fuerte no se permite conversión automática de los tipos de datos, se asume que si hay se debe a un error del programador. En lenguajes de tipado débil como Javascript las conversiones son automáticas lo que muchas veces genera errores, como por ejemplo al sumar "1" + 1, javascript lo convierte a "11" Ruby es un lenguaje de un tercer tipo, que recibe el peculiar nombre de tipo pato, ¿cuál es la idea de esto?, bueno si algo se comporta como pato, o sea nada como pato, y hace cuac como pato entonces es un pato, esto implica que no importa el tipo de dato, lo que importa es el comportamiento que tiene definido, y nosotros podemos definir y redefinir comportamientos, de esta forma podemos crear operaciones a nuestra necesidad sobre cualquier tipo de objetos, esta es una de las grandes bellezas del lenguaje de Ruby.
Tipos de datos frecuentes. En ruby todo es un objeto por lo mismo no existen los tipos de datos nativos como en otros lenguajes, sin embargo si existen tipos de datos incluidos con todo lo necesario para trabajar. Los más frecuentes son: String Fixnum y Bignum Float NilClass
TrueClass, FalseClass Array Symbol Hash Time
String Los strings son cadenas de texto como "hola mundo". Ruby diferencia los identificadores (o sea los nombres de las variables) de los strings porque estos últimos están rodeados de comillas dobles o comillas simples.
1 2 3 4
b a c d
= = = =
5 b "b" 'b'
¿Cuánto vale a y cuanto vale c?, si no te quedó clara la idea pruébalo en el intérprete y obtendrás los resultados.
Interpolación La interpolación consiste en mostrar el valor de una variable dentro de un string como en el siguiente caso:
1 2 3
edad = 30 puts "tienes #{edad} años" # tienes 30 años
El mensaje mostraría que tienes 30 años, la interpolación sólo funciona sobre comillas dobles y no funciona sobre comillas simples.
1 2 3
edad = 30 puts 'tienes #{edad} años' # tienes #{edad} años
Fixunum and Bignum Los detalles que aprenderemos a continuación no son imporantes para programar en ruby pero nos enseñarán algunas características interesantes del lenguaje ruby. Fixnum son aquellos números enteros que son capaces de almacenar un Integer, el tamaño final depende de la arquitectura del computador, pero en una máquina de 64 bits el número más alto que se puede almacenar es (2 ∗ ∗62) − 1, sin embargo en caso de exceder ese valor el tipo de dato se convertirá automáticamente en un Bignum, en otras palabras no tenemos que preocuparnos de excedernos overflow como en otros lenguajes. Un ejercicio interesante es probar esto en el intérprete, para eso escribiremos:
1 2
(2**62 - 1).class (2**62).class
En el primer caso obtendremos Fixnum en el segundo Bignum, si en ambos obtienes bignum es muy posible que la arquitectura de tu computador sea de 32 bits. Como detalle cabe destacar que los paréntesis son necesarios porque el .class tiene precedencia por sobre la operación, entonces sin los paréntesis en el primer caso haríamos .class de 1 que es un Fixnum y luego no lo habríamos podido restar de 2**62, en el segundo sin los paréntesis habríamos sacado el .class de 62 y tratado de elevar 2 a Fixnum operación que claramente no tiene sentido.
Float Float son los valores con decimales, o sea por ejemplo: 2.0 3.14 1.5 Los números flotantes son representaciones inexactas de los números reales y por lo mismo se debe tener precaución al compararlos a escala muy pequeñas. En ruby la división no exacta de dos números enteros no da como resultado un número decimal en lugar de eso da como resultado otro entero. Ejemplo: al dividr 4 / 5 esperaríamos como resultado 8, pero en su lugar obtenemos cero. Hay dos formas de resolver el problema:
La primera es agregando un .0, por ejemplo 4.0 / 5 o 4 / 5.0 nos darán como resultado 0.8 La segunda es transformando el dato con el método .to_f, ejemplo 4.to_f / 5 , esta segunda forma es la que hay que utilizar cuando nuestros valores están dentro de variables. La división de un float con un entero o de un entero con un float si da un float como resultado.
La clase nil o (nilclass) A diferencia de muchos otros lenguajes populares existe un objeto para los valores nulos, este es nil, lo bueno de que los valores nulos sean un objeto es que podemos hacer cosas como esta:
1 2
a = nil a.nil?
En cualquier otro lenguaje habríamos obtenido un error del tipo que a no es un objeto y por lo tanto no tiene el método .nil?, ruby lo soporta. Como resultado del experimento anterior obtendremos el valor booleano true, pero se preguntamos por un valor no nulo, como por ejemplo 2.nil? obtendremos como resultado false. no confundir con undefined, una variable será undefined mientras no se haya definido.
TrueClass y FalseClass Los objetos true y false sirven para representar valores booleanos, son útiles para representar estados, veamos un ejemplo básico:
1 2 3
if true puts "hola" end
En muchos lenguajes existe una conversión implícita entre el 0 y el falso y el 1 y verdadero, pero en ruby son cosas distintas.
1 2
1 == true # Veremos que es falso 0 == false # Veremos que también es falso
En ruby todo es un objeto, true y false son instancias de las clases TrueClass y FalseClass respectivamente. Esto significa que si preguntamos por la clase de true obtendremos TrueClass y si lo hacemos por la clase de false obtendremos FalseClass.
1
true.class # TrueClass
Array [] Los arrays de ruby son contenedores que permiten agregar múltiples datos y de diversos tipos, los datos dentro pueden accedidos secuencialmente o por su índice. ejemplo:
1 2
a = [1,2,3,4,5, "hola"] puts a[0]
Los índices van desde cero hasta n - 1. Acceder a un indice más allá del rango tendrá como efecto la devolución de nil
1
puts a[10] # => nil
Para acceder a todos los datos secuencialmente ocuparemos el .each
1 2 3
a.each do |i| puts a[i] end
Los arrays puedes ser recorridos de otras formas, y son un capítulo completo de este libro que abordaremos más adelante.
Símbolos :simbolo Los símbolos son tipos de datos similares a los strings pero están optimizados para ocupar menos memoria. los símbolos empiezan con :
1 2
a = :hola puts a
¿Para qué sirven? En muchas ocasiones no nos interesa ocupar el string para operar sobre el, en algunas ocasiones lo ocupamos para denotar el estado de una variable, por ejemplo supongamos que tenemos un semáforo que puede tener tres estados, y estos pueden ser "rojo", "amarillo" o "verde"`, en ese caso conviene utilizar símbolos y simplemente guardar el estado como símbolo, ejemplo semáforo = :amarillo Si el array almacena posibles estados entonces también podemos aplicar la misma lógica
1 2
estados_semaforo = [:verde, :amarillo, :rojo] semaforo1 = estados_semaforo[0]
Hash {} Los hash son otro tipo de contenedor que lugar de indexarse por un número se indexan por una clave. Ejemplo:
1 2
a = {"clave1" => "valor1", "clave2" => "valor2"} # => {"clave1"=>"valor1", "clave2"=>"valor2"}
Para acceder a los valores de un hash debemos hacerlo por la clave, como en el siguiente ejmplo:
1 2
a["clave1"] # => "valor1"
A los hash se les suele llamar diccionarios porque funcionan de la misma forma, para poder obtener el significado de una palabra la búscas por el índice. La sintaxis de los hash es clave => valor , donde la clave puede ser un string o símbolo y el valor puede ser cualquier objeto, o sea cualquier tipo de dato ya sea una array o un hash.
Hash y Símbolos
Mencionamos que los hashs pueden utilizar símbolos como claves, para lograrlo podemos utilizar una de las dos siguientes sintaxis.
1 2
a = {:símbolo1 => "valor1", :símbolo2 => "valor2"} #=> {:símbolo1=>"valor1", :símbolo2=>"valor2"}
O podemos utilizar la siguiente:
1 2
a = {"clave1": "valor1", "clave2": "valor2"} # => {:clave1=>"valor1", :clave2=>"valor2"}
Time La clase Time nos permite generar instancias de tiempo, Time.now nos devuelve una instancia con la hora local del sistema, luego podemos movernos en el tiempo sumando y restando segundos.
1 2 3
hora = Time.now puts hora + 60 puts hora + 3600
Preguntas ¿Cuál es la diferencia entre un símbolo y un array? ¿Cuál es la diferencia entre un array y un hash? ¿En que consiste el duck typing? ¿Para que sirve la interpolación? ¿Es lo mismo un string con doble comilla y que uno de comillas simples? ¿Cómo ruby diferencia un string de una variable? ¿Es lo mismo a = 2, que a = "2"? ¿Es lo mismo a = "b", que a = b? ¿Cuál es la diferencia entre Bignum y Fixnum? ¿Qué es una IDE?
¿Cómo se puede correr un script directamente desde sublime?
4) Operadores En ruby los operadores son parte del comportamiento de los objetos, están definidos dentro de las clases como veremos más adelante y son necesarios para sobre los distintos tipos de datos existen. Los operadores básicos se dividen en estos cuatro tipos: 1. Operadores aritméticos 2. Operadores de comparación 3. Operadores de asignación 4. Operadores lógicos
Operadores aritméticos Los operadores aritméticos son aquellas instrucciones que permiten operar sobre los distintos tipos de datos, principalmente aplican a números, pero algunos objetos tienen sus propias definiciones de estos comportamientos Operator
Nombre
Ejemplo
Resultado
+
Suma
2+3
5
-
Resta
2-3
-1
*
Multiplicación
3*4
12
/
División
3/4
0
%
Módulo (resto)
3%4
3
**
Potencia
2 ** 4
16
Operadores de comparación Los operadores de comparación son aquellos que comparan dos valores y obtienen como resultado un valor booleano. Operator ==
Nombre Igual a
Ejemplo
Resultado
2 == 2
true
==
Igual a
2 == 2
true
!=
Distinto a
2 != 2
false
>
Mayor a
3>4
false
>=
Mayor o igual a
3 >= 3
true
<
Menor a
4 10 puts "tienes más de 10 años" elsif a >= 20 puts "tienes más de 20 años" else puts "eres menor de 10 años" end
Se debe tener mucho cuidado que utilizar dos ifs no es lo mismo que utilizar if y elseif, analicemos el siguiente caso:
1 2 3 4 5 6 7 8 9 10
puts "ingresa un número" a = gets.chomp.to_i if a > 10 puts "tienes más de 10 años" end if a >= 20 puts "tienes más de 20 años" else puts "eres menor de 10 años" end
En este caso si la persona tiene 11 años, mostrará que tiene más de 10 y luego mostrará que es menor de 10 años y si la persona tiene 20 o más entonces mostrará que tiene más de 10 años y más de 20 años.
Case Cuando son muchos los posibles casos, se recomienda ocupar la instrucción case.
1 2 3 4 5 6 7
a = 10 case when (a > 1 and a < 10) puts "Estoy entre 1 y 10" when a >= 10 puts "a es mayor que 10" end
La instrucción Case puede ser utilizada de dos formas, con parámetro o sin, ya revisamos el primer caso, ahora veamos el segundo.
1 2 3 4 5 6 7 8 9 10
case a when 1..10 puts "Estoy entre 1 y 10" when 11 puts "Soy 11, o sea más que 10" when String puts "No soy un número soy un string" else puts "Ups, no se que hacer con #{a}" end
Gracias al duck typing de ruby la instrucción case se convierte en una herramienta bastante poderosa para manejar diversas situaciones.
Operador ternario. El operador ternario es una variante del if que permite asignar un valor u otro dada una condición. la lógica es la siguiente: si_es_cierto ? resultado_cierto : resultado falso. llevado a ruby sería:
1
a == 5 ? b = 1 : b = 2
Lo anterior se le, b tendrá el valor 1 si a vale 5, en caso contrario tendrá el valor 2.
Ciclos Los ciclos son instrucciones que permiten repetir una o un conjunto de instrucciones, aunque siempre debemos tener cuidado de que no sea un número infinito de veces.
Times La forma más fácil de repetir una instrucción o grupo de instrucciones es con la función times
1 2 3
5.times do puts "repitiendo" end
Además si queremos mostrar el número de la repetición podemos hacerlo utilizando una variable.
1 2 3
5.times do |i| puts "repitiendo: #{i}" end
Tanto times como muchas de las instrucciones que veremos a continuación reciben bloques, estos son tan importantes en ruby que tienen su propio capítulo en este libro, pero por ahora es importante entender que todos los bloques tienen dos posibles sintaxis. La primera es con do y end como vimos previamente, y la segunda es entre llaves
1
5.times { |i| puts "repitiendo #{i}" }
Desafio
Utiliza la instrucción times para contar en reversa de 100 a 1 Utiliza la instrucción times para contar todos los números pares hasta 100
While while permite repetir una instrucción mientras se cumpla una condición, la sintaxis del while es:
1 2 3
while (condicion) do end
Mientras la condición sea verdadera todo lo que esté dentro del bloque se repetirá. while tiene una sintaxis alternativas al igual que el if
1
codigo while condition
Y funciona de la misma forma, al igual que en el caso anterior el código se ejecutará mientras la condición sea verdadera. La instrucción while no puede recibir un bloque :(
Until Until es la versión negativa del while, la idea al igual que unless es evitar ocupar el operador not que hace más difícil leer ciertas expresiones (ademas como operador lógico sigue reglas que en ciertas ocasiones puede ser difícil de entender, como por ejemplo !a y !b = !(a o b) La sintaxis es:
1 2 3
until (condicion) do end
Al igual que while también puede ser usada en una sola línea
1
pedir_password until password == "secreto"
For for es una instrucción que nos permite iterar sobre rangos y arreglos, por ejemplo:
1 2 3
for i in 0..10 puts i end
Para un arreglo tenemos varias opciones parar iterarlo, principalmente se dividen en sobre los índices, los arreglos, y ambos simultáneamente.
Por índice 1 2 3 4
a = [9,3,5,7,1,2,3] for i in 0..a.length puts a[i] end
Por elemento 1 2 3 4
a = [9,3,5,7,1,2,3] for element in a puts element end
Recordar que element en este caso es simplemente una variable que sirve para iterar por sobre los valores de a, no es una palabra reservada.
Utilizando each
1 2 3 4
a = [9,3,5,7,1,2,3] a.each do |element| puts element end
Iterando sobre índice y elemento con each 1 2 3 4
a = [9,3,5,7,1,2,3] a.each_with_index do |element, indice| puts "el elemento #{element} en la posición #{indice}" end
Ejercicios para resolver Ejercicios básicos Repetir Repetir 10 veces "voy a aprender ruby" Mostrar cien números al azar. Hint:
1 2
prng = Random.new prng.rand(100)
Mostrar todos los divisores del número 840 con while con for con times
Cambiando de do-while a while Dado el código
1 2 3 4
begin puts "escriba si para continuar" input = gets.chomp end while(input != "si")
Transformarlo utilizando únicamente while
El problema de 3n + 1 La conjetura de collatz dice que para cualquier número positivo entero: Si el número es par se divide en dos Si el número es impar se multiplica por 3 y se le suma 1. Si ahora tomamos el número resultante y aplicamos el mismo criterio sucesivamente esta secuencia tarde o temprano el número resultante será 1. Ejemplo 10 => 5 => 16 => 8 => 4 => 2 => 1
Lo que debes hacer es crear un método que permita mostrar toda la secuencia a partir de un número ingresado por el usuario.
Búsqueda binaria Utilizando exclusivamente ifs y while es posible construir un pequeño juego donde el ordenador adivina un número que tu piensas entre 1 y 1000 en un máximo de diez intentos. Escoges un número, La idea ahora es partir buscando en la mitad del arreglo, por ejemplo en la posición 500, si el jugador dice que es mayor, buscaremos en la 750, luego si dice que es menor buscaremos en 675, y así sucesivamente hasta encontrar el número
Break
La instrucción break es capaz de romper un ciclo, la podemos ocupar directamente o dentro de una condición, ej:
1 2 3 4 5 6
10.times do |i| puts i break end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
i = 0 while (i < 100) puts i if i > 5 break end i = i + 1 end
0
# # # # # # # #
0 1 2 3 4 5 6 => nil
Ciclos anidados. Es perfectamente posible anidar ciclos, o sea escribir un ciclo de otro, esto tiene varias utilidades, desde las más obvias como repetir un ciclo n veces hasta mostrar todos los elementos que existen dentro de una matriz. Supongamos que por algún motivo queremos generar la secuencia:
1 2 3 4
0 1 1 1
0 1 2 3
5 6 7 8 9 10 11 12
1 2 3 4 5
1 1 1 2 2 2 . 5
4 5 6 1 2 3 . 5
6.times do |i| 6.times do |j| puts "#{i} #{j}" end end
Desafíos Similar al ejercicio anterior genera todas las combinaciones de 4 números posibles del número cero al diez. Genera todas las secuencias de números posibles entre tres números tales que el número de la izquierda siempre sea menor o igual que el de la derecha ej: 111 112 11. 1 1 10 1 2 2 # el término 1 2 1 no podría aparecer Antes de avanzar al siguiente capítulo deberías ser capaz de contestar estas preguntas, si no te recomiendo que vuelvas a leerlo y lo contestes
Preguntas ¿Por qué es bueno escribir siempre las condiciones en sentido positivo? ¿Cuál es la diferencia entre if y unless? ¿De un ejemplo de un if inline? ¿Cuál es la diferencia entre while y until?
7) Métodos En muchos lenguajes existe la posibilidad de agrupar código y darle un nombre para reutilizarlo a voluntad, a esto se le llaman funciones. En lenguajes como en ruby no existen las funciones, pero existe algo muy similar llamado métodos y sirven para exactamente lo mismo construir
Definiendo un método El método más básica que podemos definir:
1 2
def nombre_metodo() end
Dentro de la definición de un método existe: - def (para indicar que se va a definir una función) - el nombre - los parámetros (que van dentro de los paréntesis) - ej: metodo(x, y) - el retorno Ejemplo de un método más complejo:
1 2 3
def perimetro(r) return 2 * Math.pi * r end
Llamando una función Luego el llamado consiste en utilizar el nombre y pasar los parámetros indicados.
1
perimetro(10)
De esta forma uno define una función y luego la ocupa varias veces.
Retorno implícito
En ruby el retorno de la función es siempre la última línea, por lo tanto podríamos escribir el código anterior de la siguiente forma y sería exactamente lo mismo.
1 2 3
def perimetro(r) 2 * Math.pi * r end
En el caso de no estar especificado el retorno devolverá nulo. Debemos ser cuidadoso por que por error podríamos hacer que un método devolviese nulo u otro valor no esperado, miremos el siguiente ejemplo:
1 2 3
def foo(x) true if x > 18 end
En este caso el método foo devolverá true si x es mayor de 18, pero en lugar de devolver falso si es menor, devolverá nulo, luego no es lo mismo nulo que falso y tendremos problemas con nuestro código, una mejor solución sería:
1 2 3 4 5 6 7
def foo(x) if x > 18 true else false end end
Otro motivo por el cual tenemos que tener mucho cuidado con el retorno implícito es que agregando un puts en la última línea para saber el valor de una función romperíamos el retorno, imaginemos este caso:
1 2 3 4 5 6 7 8
def foo(x) if x > 18 true else false end puts x end
El método puts muestra en pantalla el valor de x, pero luego devuelve nil, así es como funciona puts y otros métodos para mostrar en pantalla, eso no tiene nada de malo, el problema es que los métodos devuelven el valor de la última línea, o sea en este caso se devolverá nil y nuestro método dejará de funcionar. Antes de pasar a otro tema veamos una forma más corta y más elegante de implementar el mismo método.
1 2 3
def foo(x) x > 18 end
Parámetros Un método puede recibir diversos parámetros, los parámetros son variables que puede recibir un método. Ejemplo:
1 2 3 4 5 6
def mostrar_valores(a,b) puts a puts b end mostrar_valores(5,8)
Dentro del método mostrar_valores la variable a tendría el valor 5 y b el valor 8 Los parámetros que se le pasan a un método no tienen porque llamarse igual que los que recibe.
1 2 3 4 5 6 7 8
def mostrar_valores(c,d) puts c puts d end a = 5 b = 8 mostrar_valores(a, b)
En el momento del llamado se asignan los valores, o sea, cuando se hace el llamado mostrar_valores(a,b) ruby lo
que hace es remplazar eso por mostrar_valores(5,8), luego dentro del método lo que hace es hacer c = 5, d = 8, el orden siempre se conserva.
Métodos con parámetros valores por defecto Los métodos pueden tener valores por defecto, esto les permite asumir el valor si el argumento no es pasado.
1 2 3
def foo(a, b = 5) puts b end
Si llamamos a foo como foo(8) entonces ruby lee que a es el valor 8, como b no fue pasado lo toma como 5 y eso es lo que muestra en pantalla.
El scope de las variables Cuando se entra a un método, se define un nuevo ambiente. Este ambiente es nuevo, dentro de el se pueden definir variables que no afecten a las de afuera, pero no son la misma variable, son una variable distinta definida en este ambiente, sucede lo mismo cuando pasamos parámetros a la función, al pasarle a y b estamos generando una copia de esos valores en este ambiente nuevo, si los modificamos acá dentro ese cambio no sucederá afuera
Veamos un ejemplo de esto:
1 2 3 4 5 6 7
def foo() a = 7 end a = 5 foo() puts a
¿Cuánto vale a en el momento que se muestra en pantalla? Vale 5 y la razón es porque a toma el valor 5, luego se llama al método foo, donde a a se le asigna el valor 7, pero esta a es nueva variable, está asignada en otro espació y al terminar el método y volver al espacio original su valor vuelve al original. Veamos otro ejemplo, ahora con parámetros
1 2 3 4 5 6
def foo(a, b) puts b puts a return foo(2,5)
¿Qué muestra foo? ¿2 y 5 o 5 y 2? La respuesta correcta es 5 y 2 debido a que cuando llamamos a foo, a toma el valor 2 y b toma el valor 5, luego mostramos b primero. Un ejemplo más, ahora lidiando con el tema del ambiente y la sobreescritura de variables
1 2 3 4 5 6 7 8
def foo(a, b) puts b puts a return a = 2 b = 5 foo(b,a)
¿Qué se muestra en pantalla? El llamado a foo en la línea 8 se hace con los valores 5,2, o sea foo(5,2), luego dentro de foo a tendría el valor 2 y b el valor 5, y como el orden se muestra al revés se muestra primero 5 y luego 2.
Variables globales vs locales Las variables globales son variables que existen en todos los ámbitos, pero son una pésima práctica de programación, porque hacen muy fácil romper un programa por error, pero de todas formas necesitamos saber identificarlas en ruby (aunque nunca las vayamos a utilizar) En ruby las variables globales llevan un $, por ejemplo $z= 5 luego
1 2
defined? $x # => "global-variable"
Métodos recursivos Los métodos recursivos son métodos que se llaman así mismo, son muy útiles para resolver problemas de inteligencia artificial, de investigación de operaciones, de búsqueda o problemas matemáticos, en el contexto de una aplicación en Rails muy rara vez se utilizan. En matemáticas suelen existir expresiones del tipo: f (n) = f (n − 1) + 10
lease que el siguiente termino de la función se calcula como el termino anterior + 10 o sea: f (2) = f (1) + 10 f(1) = f(0) + 10
Este tipo de expresiones son muy fáciles de modelar con funciones recursivas.
1 2 3
def fun(x) fun(x - 1) + 10 end
Lo anterior termina en un ciclo infinito, de hecho el error obtenido será SystemStackError: stack level too deep y esto se debe a que no termina nunca, pero además debemos recordar algo del principio del capítulo, por cada función ruby deja espacio para guardar las variables en un ámbito especial, este espacio se llama stack. Para evitar el error basta con poner un punto de término, por ejemplo digamos que la función cuando llegue a f(1) termina.
1 2 3 4 5 6 7
def fun(x) puts "con x = #{x}" if x > 1 resultado = fun(x - 1) + 10 end resultado || 0 end
Entonces si llega al final del código y no ha terminado devuelve el resultado y cuando llega al último número devuelve cero. Veamos otra operación, una famosa, fibbonacci, esta serie parte: 1 1 2 3 5 8 13 21 35 En la serie de fibbonacci todos cada número de la serie viene dado por la suma de sus dos números anteriores, o sea f(n) = f(n-1) + f(n-2), y el primer término f(0) es 1 y el segundo término o sea f(1) también es 1.
1 2 3 4 5 6 7 8
def fibo(x) if x > 2 resultado = fibo(x - 1) + fibo(x - 2) else return 1 end resultado end
Luego podemos saber cuál es el sexto término de la serie simplemente escribiendo.
1
puts fibo(6)
Preguntas ¿Cuál es la diferencia entre una variable global y una local? ¿Qué es un método? ¿Cuál es la diferencia entre un método y una función? ¿Cómo se hace para que un método tenga valores para los parámetros por defecto? ¿Importa el orden de los parámetros en un método? ¿Existen las funciones en ruby? ¿Qué se entiende por scope? ¿Todas los métodos devuelven valores? ¿Cuáles no? ¿Qué es un método recursivo? ¿Qué es el stack? ¿Cuál es la diferencia entre break y return? ¿En qué consiste el error SystemStackError: stack level too deep ?
8) Arrays Creando arrays Ya tuvimos un primer acercamiento a los array, pero en este capítulo aprenderemos a sacarle provecho. Hay dos formas de crear un array, la primera es con los [], la segunda es ocupando la clase Array Ejemplos:
1 2
a = [1,2,3,4] # => [1, 2, 3, 4]
El constructor del array es contraintuitivo, puede recibir uno o dos parámetros, si sólo hay un parámetro se crea un array vacío con la cantidad de elementos indicadas por el parámetro:
1 2
a = Array.new(4) # => [nil, nil, nil, nil]
Y cuando hay dos parámetros el segundo es un elemento que se repite n veces, donde n es el primer parámetro
1 2
a = Array.new(3,0) # => [0,0,0]
Índices Los arrays tienen dos partes, una son los elementos, o sea el contenido dentro del array, la otra es el índice, y permite acceder al elemento que está dentro, los índices van de cero hasta n - 1, donde n es la cantidad de elementos del arreglo. O sea en un arreglo que contiene 5 elementos, el primer elemento está en la posición cero, y el último en la 4.
1 2
a = Array.new(3,0) # => [0,0,0]
3 4 5 6
a[2] # => 0 a[3] # => nil
Los índices también se pueden utilizar con números negativos y de esta forma referirse a los elementos desde el último al primero.
1 2
a = [1,2,3,4,5] a[-1] # => 5
Contando elementos Podemos contar la cantidad de elementos dentro de un array con el método .count o con el método .length
1 2 3 4
a.count # => 5 a.length # => 5
Los arrays como contenedores Ya hemos ocupado los arrays como contenedores, un array puede contener un número indeterminado de cualquier cantidad de objetos dentro, aunque si existe un límite que viene dado por la cantidad de memoria disponible del computador. Cuando uno asigna un array a una variable la variable no contiene el contenedor, contiene una dirección al contenedor, y esta diferencia es importante especialmente cuando intentemos copiarlo.
1 2 3 4
a = ["hola", "yo", "soy", "un", "arreglo"] b = a b[0] = "chao" puts a[0]
¿Cuánto vale a[0]?
Tiene el mismo valor que b[0] porque no cambiamos ni a ni b, cambiamos uno de los elementos adonde ambas variables apuntaban.
Mutabilidad en los arrays Esto se conoce como mutabilidad, un objeto es mutable si consecuencia de sus operaciones su estado puede cambiar, un objeto es inmutable si no cambia, ahora este enfoque tan purista no existe en ruby, uno puede programar los objetos de forma de que cambien o que devuelvan uno nuevo, por eso se habla exclusivamente de métodos mutables o de métodos inmutables, y un objeto es mutable si contiene al menos un método mutable. Cómo los arrays tienen métodos que pueden cambiarlo entonces son mutables, y lo más peligroso es la copia por los mencionado previamente ¿Cómo podemos copiar entonces un arreglo, sin que sea una referencia? Con el método .dup
1 2 3 4
b = a.dup b[0] = "probando el cambio" a.inspect b.inspect
Lo importante de los arrays como contenedores es saber manipularlos, hay dos formas de acceder a los datos dentro de un array, secuencialmente y aleatoriamente Lo forma secuencial consiste en ver los datos en orden, ya vimos previamente dos de esas formas.
Con for 1 2 3 4
a = [1,2,6,1,7,2,3] for i in a: puts a[i] end
Con each
1 2 3 4
a = [1,2,6,1,7,2,3] a.each do |i| puts i end
.each es un método muy interesante que realiza dos cosas, al primera es aplicar todas las operaciones definidas dentro del bloque, la segunda es devolver el arreglo original.
1 2 3
b = a.each do |i| puts i**2 end
Ahora b contiene los mismos valores de a y por explicado anteriormente si cambiamos algún valor de b cambiará el valor de a.
Operaciones funcionales sobre los arrays Además de la iteración con each hay cuatro formas muy interesante de operar sobre los datos de un array. map o collect select o reject inject group_by
Map La primera es map, o collect, ambas son exactamente iguales. Map sirve para aplicar una operación a cada elemento del array, y devuelve un array con las operaciones aplicadas, ejemplo:
1 2 3 4 5
a = [1,2,3,4,5,6,7] b = a.map do |e| e * 2 end # => [2, 4, 6, 8, 10, 12, 14]
O en su forma express:
1 2
a = [1,2,3,4,5,6,7] b = a.map {|e| e * 2}
map y collect son exactamente lo mismo, no hay ninguna diferencia entre ocupar uno u otro.
1 2
a = [1,2,3,4,5,6,7] b = a.collect {|e| e * 2}
Select La segunda operación funcional muy útil sobre array es select, select permite sacar de un arreglo todos los elementos que no cumplan un criterio, select al igual que map devuelve un array nuevo sin modificar el original
1 2 3 4
a = [1,2,3,4,5,6,7] b = a.select{ |x| x % 2 == 0} # seleccionamos todos los pares # => [2,4,6]
Hay función que hace lo contrario a select, se llama reject y lo que hace es eliminar a todos los elementos del arreglo que no cumplan con el criterio.
1 2
b = a.reject{ |x| x % 2 == 0} # => [1, 3, 5, 7]
.select y .reject no están solo limitados a números, por ejemplo supongamos que tenemos un arreglo que tiene números y palabras y queremos seleccionar solo las palabras.
1
a = [1,"hola", 2, "aprendiendo", 3, "ruby"].select{|x| x.class == String
Inject
Inject permite operar sobre todos los resultados pero en lugar de devolver un arreglo, devuelve un único valor con el resultado de las operaciones, por lo mismo en el bloque hay que pasar dos variables, la que itera y la que guarda el resultado.
1
b = a.inject(0){|x, sum| sum += x}
Más adelante en este libro estudiaremos los procs los procs son perfectos sustitutos para los bloques y funcionan muy bien en conjunto con las operaciones como map, filter e inject. Gracias a los procs y al método .to_proc de ruby es posible utilizar lo siguiente:
1 2
b = a.inject(&:+) #sumatoria de todos los elementos c = a.inject(&:*) #productoria de todos los elementos
Group_by .group_by permite agrupar los elementos de un array acorde al criterio especificado en el bloque, a diferencia de los otros métodos group_by devuelve un hash donde la llave es el criterio de aceptación en el grupo y el valor es un array con los elementos que cumplen con la condición. Para ejemplificar agrupemos todos los números dentro de un array acorde si son pares o no.
1 2
a.group_by {|x| x % 2 == 0} # => {false=>[1, 3, 5, 7], true=>[2, 4, 6]}
Un array puede contener elementos de muchos tipos, así que podríamos agruparlo acorde al tipo con:
1
a.group_by {|ele| ele.class}
Contando elementos con group by Utilizando la misma idea podríamos también contar los elementos de cada tipo dentro de un arreglo. si tenemos un arreglo como a = [1,1,3,2,6,8,9,2] podríamos agruparlo en función de cada elemento con:
1 2
a.group_by{|x| x} # => {1=>[1, 1], 3=>[3], 2=>[2, 2], 6=>[6], 8=>[8], 9=>[9]}
Nos devuelve un hash donde el key es el elemento y en el value aparece un elemento por cada repetición, ahora el siguiente paso sería contar cada uno de esos, para eso ocuparemos un map, para devolver un arreglo nuevo que tenga el elemento y la cantidad de repeticiones.
1 2
a.group_by{|x| x}.map{|k,v| [k,v.count]}.to_h # => {1=>2, 3=>1, 2=>2, 6=>1, 8=>1, 9=>1}
Ordenando arreglos Es posible ordenar todos los elementos dentro de un arreglo utilizando .sort
1 2 3
a = [2,82,1,5,6] a.sort # => [1, 2, 5, 6, 82]
Pero si dentro de nuestro arreglo hubiese un tipo de datos que no fuera compatible con la comparación, como por ejemplo un string, entonces obtendríamos un error.
1 2 3
a = [2,82,1,5,6, "90"] a.sort ArgumentError: comparison of Fixnum with String failed
Esto lo podemos corregir ocupando sort_by, en lugar de sort, el método sort_by recibe un bloque o proc que nos permite explicar como comparar.
Desordenando arreglos 1 2
a.shuffle [5, 1, 6, 82, 2]
Los arrays como conjuntos Es posible hacer operaciones de conjunto sobre los array Para esta sección tendremos dos arreglos, a y b
1 2
a = [1,2,3] b = [3,4,5]
Suma de elementos Al conjunto a se le suman los elementos del conjunto b
1 2
a + b # [1,2,3,3,4,5]
Diferencia de elementos Al conjunto a se le quitan los elementos del conjunto b
1 2
a - b # [1, 2]
Unión Los elementos del conjunto a o los elementos del conjunto b
1 2
a | b # [1,2,3,4,5]
Interesección Los elementos del conjunto a y los elementos del conjunto b
1 2
a & b # [3]
Los arrays como una pila (o stack) Las pilas son una abstracción bien potente para la resolución de algunos problemas, consiste en olvidarse que el array puede ser accedido aleatoriamente (a través del índice) y que estos solo se pueden acceder por el final. Para eso ocuparemos sólo 2 métodos:
Push a un array podemos agregarle un dato al final ocupando el método .push
1 2 3
a = [0,1,3,4] a.push(5) # => [0,1,3,4,5]
Es posible hacer lo mismo ocupando el operador 5 a.inspect # => 0,1,3,4
Arrayception (arrays dentro de arrays) Los arrays pueden contener arrays dentro, iterarlo es el mismo proceso que itarar uno unidimensional.
1 2 3 4 5 6
super_array = [[1,2,3],[4,5,6],[7,8,9]] super_array.each do |array| array.each do |ele| puts l end end
Es posible aplanar un array multidimensional para convertirlo en un array normal ocupando el método .flatten
1 2
super_array.flatten # => [1, 2, 3, 4, 5, 6, 7, 8, 9]
El último punto que tenemos que cubrir es particularmente delicado, en ruby no existe un deep dup (en rails si existe), por lo que debemos ser extra cuidadoso al copiar arrays multidimensionales.
1 2 3 4 5 6 7 8
super_array = [[1,2,3],[4,5,6],[7,8,9]] new_array = super_array.dup new_array[0] = [11,12,13] super_array.inspect [[1,2,3],[4,5,6],[7,8,9]] new_array.inspect [[11,12,13],[4,5,6],[7,8,9]]
hasta ahí vemos que se comporta según lo esperado, pero que pasa si en lugar de haber cambiado un arreglo dentro hubiésemos cambiado un elemento dentro dentro del arreglo.
1 2
super_array = [[1,2,3],[4,5,6],[7,8,9]] new_array = super_array.dup
3 4 5 6
new_array[0][0] = "esto se cambiará en ambos arrays" super_array.inspect new_array.inspect
¿Cómo podemos copiarlo sin que pase eso?
1
new_array = super_array.map { |array| array.dup }
o de forma más corta
1
new_array = super_array.map(&:dup)
Preguntas ¿Cuál es el peligro al copiar una variable que contiene un arreglo? ¿Cuál es el problema que pueda ocurrir al copiar un arreglo multidimensional? ¿Qué hace el método dup? ¿Cómo se puede saber el largo de un array? ¿Qué devuelve el método group_by? ¿Cuál es la principal diferencia entre .each y .map? ¿Cuál es la diferencia entre map y collect? ¿Que recibe como parámetro el método .inject? ¿Qué devuelve el método inject? ¿Cuál es la diferencia entre select y reject? Si a = [1,2,3,4,5] y b = ["hola", 1,2,5 ] ¿Qué devuelve a + b? ¿Qué devuelve a - b? ¿Es lo mismo a - b que b - a? ¿Qué devuelve a & b? ¿Qué devuelve a | b?
¿Qué devuelve el método pop? ¿Qué hace el método flatten?
9) Hashs Los hash son contenedores en el cual sus elementos en lugar de ser accedidos por un índice se acceden por una clave.
Creando un hash Hay dos formas de crear un hash
con Hash.new 1
a = {}
También podemos crear un hash que ya venga con algunos valores.
1
hash_con_valores = {a:5, b:"hola"}
la segunda es con :
1
a = Hash.new
La primera es la comúnmente utilizada. La clave en los hashs es única, al igual que el índice en los array, pues si hubiesen dos iguales no podríamos rescatar el valor
1 2 3
a = {clave1:5, clave1:7} (irb):94: warning: duplicated key at line 94 ignored: :clave1 => {:clave1=>7}
Se suele utilizar símbolos como claves ya que estos no pueden ser cambiados y no tendría ningún sentido cambiar una clave, ya que en ese caso sería otra clave.
Podemos acceder a los valores utilizando la clave, en este caso a[:clave]
Agregando y cambiando elementos a un hash Supongamos que tenemos un hash definido como:
1
a = {llave1: 5}
Para agregar un elemento a un hash basta con utilizar una clave nueva.
1 2 3
a[:llave2] = 7 a["llave2"] = 9 puts a
El output mostrado por el puts sería:
1
{:llave1=>5, :llave2=>7, "llave2"=>9}
Hay que tener muchos cuidado con los claves en los hash, pues los símbolos y los strings son cosas distintas.
Por ejemplo en el caso anterior en lugar de remplazar el valor de la :llave2 agregamos una nueva llave llamada "llave2"
Removiendo elementos de un hash Podemos eleminar una llave con su valor del hash ocupando el método .delete
1 2 3
a = {k1: 5, k2:7} a.delete(:k1) puts a
Cuando borremos la clave debemos ser concientes de si es un símbolo o un string.
Iterando un hash los hash tiene clave y valor por lo tanto ocuparemos dos variables, una contendrá la clave y la otra el valor.
1 2 3 4
a = {k1: 5, k2:7, k3: 9, k4: 11, k5:1} a.each do |k, v| puts "la clave es #{k}, el valor es #{v}" end
Convirtiendo un hash en un arreglo. Es muy simple convertir un hash en un arreglo, simplemente tenemos que llamar al método to_a. Miremos el siguiente ejemplo:
1 2 3
require 'pp' a = {k1: 5, k2:7} pp a.to_a
Ocupamos la libreria ‘pp’ pretty print para mostrar el arreglo, pero la transformación del hash en un arreglo se logra únicamente con el método .to_a
Convirtiendo un arreglo en un hash Si tenemos un arreglo del tipo [[1,2],[3,4]] lo podemos convertir a hash con el método .to_h ahí el elemento de cada subarreglo se convierte en la clave y el resto en el valor.
1 2
[[1,2],[3,4]].to_h # => {1=>2, 3=>4}
Si tenemos más elementos podemos agruparlos, la parte de izquierda es la clave así que va sola, la parte de al derecha.
1 2
[[1,[2,3]],[2,[3,4]]].to_h # => {1=>[2, 3], 2=>[3, 4]}
Preguntas ¿Puede un hash tener dos claves iguales? ¿Puede un hash tener dos claves iguales pero una es un símbolo y la otra un string? ¿Cuántas variables se necesitan para iterar un hash? ¿Qué características tiene que tener un arreglo para convertirlo en hash? ¿Cómo se itera un hash?
10) Objetos en ruby Requisitos Entender: Funciones Paso de parámetros Return Variables locales / globales Para muchos programadores es complicado aprender a programar con objetos debido a la gran cantidad de conceptos que se introducen simultáneamente, pero a mi parecer es porque simplemente la teoría está explicada bajo la metáfora de la receta, o del esqueleto y realmente no aplica a la situación. En este capítulo aprenderemos una mejor forma de entender los objetos junto a todos los conceptos básicos.
Conceptos básicos Algunos conceptos que vamos a aprender en este capítulo: Clase Objeto Instancia Herencia Método de clase Método de instancias Atributos Getters y Setters Self
Para que sirven los objetos
En ruby todo es un objeto, los números son objetos del tipo Fixnum, y existen objetos de diversos tipos, como por ejemplo: los Strings, los Array, y tanto True como False son objetos. Los objetos nos permiten crear nuestros propios tipos de datos y nuestras propias operaciones para resolver los problemas que necesitemos, además nos permiten reutilizar el código que hagamos para resolver problemas similares.
Los tres conceptos más importantes El primer paso es entender los tres conceptos claves, todo el resto derivará de ahí: Clase -> Instanciar -> Objeto Un objeto es un conjunto de propiedades y comportamientos, por ejemplo una mesa tiene 4 patas (eso es una propiedad) y la capacidad de golpear dedos del pie (eso es un comportamiento), se dice que está encapsulado porque si destruyes una mesa, eso no afecta a las otras mesas del mundo. Se suele decir que las clases (la forma de definir objetos) son recetas, o son un esqueleto, el secreto para comprender bien la programación orientada a objetos es entender a las clases como un molde o una fábrica que sirve para construir esos objetos, por ejemplo un molde de una pieza de lego y a los objetos como productos de este molde, o sea los legos.
Entonces tenemos la clase que es un molde de legos, que es capaz de imprimir objetos lego, los cuales tienen un identificador (por ejemplo un número de serie), propiedades como el color y tamaño; y comportamientos como acoplarse y causar dolor infinito si uno los pisa. En ruby para definir una clase:
1 2
class MoldeLego end
Para crear un objeto a partir de ese molde hay que instanciarlo, para instanciar basta con utilizar el nombre de la clase con el método new, o sea:
1
lego1 = MoldeLego.new
Si lo anterior lo hacemos en el intérprete, obtendremos como Output => #
Resumen hasta ahora: Las clases son un molde o fabrica que uno programa, y a través del método new creas instancias que utilizas para resolver diversos problemas y mantener ordenado tu código.
Identidad, Estados y Comportamientos El identificador En ruby todos los objetos tienen un identificador único, el lego que creamos previamente tiene uno, este se puede capturar utilizando el método object_id con:
1
lego1.object_id
Recordar que en ruby el uso de paréntesis en los métodos es opcional, por lo que lego1.object_id es lo mismo que lego1.object_id()
Como resultado obtendremos 70351300158900 , u otro número de similares características. Nuestro objeto lego ya tiene un identificador, pero todavía no le hemos agregado ni comportamiento ni propiedades, aunque si tiene algunas que vienen por defecto.
Comportamientos Ahora el secreto para entender bien esta parte es que tanto el molde (la clase) como el objeto pueden tener comportamientos. Cuando los comportamientos son del molde se llaman métodos de clase, cuando lo son del objeto se llaman métodos de instancia. En este capítulo abordaremos principalmente los métodos de instancia. Para agregar un método de instancia, (o sea un comportamiento de un objeto) lo hacemos dentro de la definición de la clase.
1
class Vehiculo
2 3 4 5
def encender() puts "rrmrmmmrmrmrm" end end
Los estados Para guardar estados de un objeto ocuparemos variables de instancia, esta se distinguen porque empiezan con @
1 2 3 4 5 6 7 8 9 10 11
class Vehiculo def encender() @encendido = on end def apagar() @encendido = off end def estado() @encendido end end
Ahora con esta clase de vehículo podemos crear todos los que queramos y cada uno tendrá un estado encendido independiente de los otros. Ejemplo
1 2 3 4 5
a1 = Vehiculo.new a2 = Vehiculo.new a1.apagar a1.estado a2.estado
Podemos ver que mientras uno de los estados es prendido, el otro es apagado.
Métodos de instancia 1 2
class MoldeLego def initialize()
3 4 5
@size = 1 # las variables de instancia se definen con una @ end end
Cada vez que creamos un lego este tendrá tamaño 1,
1 2
lego1 = MoldeLego.new # Este Lego ahora tiene tamaño 1
Podemos ver que size es una variable de instancia utilizando:
1
lego1.instance_variables
Pero no podemos acceder a esta porque los estados internos de los objetos están protegidos, o sea si hacemos lego1.size obtendremos un error.
Resumen hasta ahora: Los objetos son instancias de una clase, cuando se utilizar la expresión clase.new entonces es cuando se instancia el objeto y automáticamente se llama al constructor, todo lo definido dentro del constructor se ejecuta. Los constructores sirven principalmente para darle valores iniciales a las variables de instancias. Un objeto además puede tener múltiples variables y comportamientos cuando le pertenecer a la instancia los llamamos métodos o variables de instancia y cuando le pertenecen a la clase les llamamos métodos o variables de clase.
Getters y Setters Volviendo a nuestro ejemplo, si queremos rescatar el tamaño tenemos un problema, no se puede acceder directamente a los estados internos de un objeto debido al principio de encapsulamiento, para acceder a los estados tenemos que crear getters y setters, o sea métodos para obtener los estados (getters) y métodos para cambiarlos (setters), así que nuestro MoldeLego quedaría así:
1 2 3
class MoldeLego def initialize() @size = 1 # las variables de instancia se definen con una @
4 5 6 7 8 9 10 11
end def getSize() return @size end def setSize(new_size) @size = new_size end end
Para usarlo, lo haríamos de la siguiente forma:
1 2 3
lego1 = MoldeLego.new # Este Lego ahora tiene tamaño 1 lego1.setSize(5) lego1.getSize()
Exista una mejor forma de definir los getters y setters, que es hacerlo de tal manera que pareciera que estamos trabajando directamente sobre el atributo. o sea:
1 2 3 4 5 6 7 8 9 10 11
class MoldeLego def initialize() @size = 1 # las variables de instancia se definen con una @ end def size() return @size end def size=(new_size) @size = new_size end end
De esta forma podemos ocupar el objeto así:
1 2 3 4
lego1 = MoldeLego.new # Este Lego ahora tiene tamaño 1 lego1.size # devuelve 1, pues es el tamaño original lego1.size = 5 lego1.size # devuelve 5, ya que lo cambiamos
Ahora en ruby es posible definir de forma simultánea la variable, los getters y setters a través del attr_accessor:
1 2 3 4 5 6
class MoldeLego attr_accessor :size def initialize() @size = 1 # las variables de instancia se definen con una @ end end
Y para ocuparlo sería exactamente que la forma anterior, ahora volvamos a la idea de acoplar:
1 2 3 4 5 6 7 8 9 10 11 12
class MoldeLego attr_accessor:size # Pasamos un valor al constructor # para poder crear legos de otros tamaños # en caso de que se omita es de tamaño 1 def iniatilize(size_orig = 1) @size = size_orig end def acoplar(otro_lego) return MoldeLego.new(self.size + otro_lego.size) # devolvemos un lego de la s end end
¿Cómo se usa?
1 2 3
lego1 = MoldeLego.new lego2 = MoldeLego.new lego3 = lego1.acoplar(otro_lego)
Todavía podemos hacer una mejor más, en ruby al igual que otros lenguajes se puede redefinir una operación, en este caso vamos a redefinir la suma (sólo para los legos):
1 2 3 4 5
class MoldeLego attr_accessor :size # Pasamos un valor al constructor # para poder crear legos de otros tamaños # en caso de que se omita es de tamaño 1
6 7 8 9 10 11 12 13 14
def initialize(size_orig = 1) @size = size_orig end def +(otro_lego) # devolvemos un lego de la suma del tamaño de los otros legos MoldeLego.new(@size + otro_lego.size) end end
Gracias a eso ahora podemos simplemente escribir:
1 2 3
lego1 = MoldeLego.new lego2 = MoldeLego.new lego3 = lego1 + lego2
Antes de avanzar vamos ver un método de clase más, este lo tienen todos los objetos y se llama to_s y permite mostrar el objeto como si fuera un string, por ejemplo si mostramos un lego, ocupando:
1
puts lego3
Veríamos en pantalla algo como: # El objeto automáticamente se convierte en string para poder ser mostrado, pero nosotros podemos sobreescribir el método to s.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class MoldeLego attr_accessor :size # Pasamos un valor al constructor # para poder crear legos de otros tamaños # en caso de que se omita es de tamaño 1 def initialize(size_orig = 1) @size = size_orig end def +(otro_lego) # devolvemos un lego de la suma del tamaño de los otros legos MoldeLego.new(@size + otro_lego.size) end
15 16 17 18 19
def to_s "soy un lego de tamaño #{@size}" end end
De esta forma al realizar las mismas operaciones anteriores
1 2 3 4
lego1 = MoldeLego.new lego2 = MoldeLego.new lego3 = lego1 + lego2 puts lego3
Veríamos que lego3 es mostrado como: soy un lego de tamaño 2
Métodos y atributos de clases ¿Qué pasaría si quisiéramos contar cuantos legos se han creado? Eso no lo podemos hacer con una variable de instancia, necesitamos que sea el molde cuente, (algo así como imprimir el número de serie en el producto), o sea es una propiedad de la clase, estas se llaman formalmente variables de clase, en ruby se crean con 2 @@:
1 2 3 4
class MoldeLego attr_accessor :size @@cuenta_legos = 0 end
A diferencia de la variables de instancias estas no requieren definirse en el constructor, pues existen para la clase, independiente de si existen legos o no. Ahora si quisiéramos contar cada vez que se crea un lego nuevo, podríamos hacerlo así:
1 2 3 4
class MoldeLego attr_accessor:size @@cuenta_legos = 0 def iniatilize(size_orig = 1)
5 6 7 8
@size = size_orig @@cuenta_legos += 1 end end
Si quisiéramos obtener las variables de clase toparíamos con el mismo problema de encapuslamiento, tenemos que definir getters y setters para estas, ahora lamentablemente la sintaxis en ruby no es tan bonita, se hace así:
1 2 3 4 5 6 7 8 9
class MoldeLego class e puts "can't exit yet" end rescue NameError => e puts "process must end because: #{e}" ensure puts "closing opened stream stuff" end
15) Expresiones regulares Las expresiones regulares son una técnica de búsqueda de strings, gracias a ellas con un patrón que uno define puede encontrar uno o más resultados dentro de un texto. Principalmente sirve para operaciones de buscar y remplazar. Para utilizarlas uno define un patrón (esta es la parte difícil) luego lo compila (no siempre es necesario) finalmente lo utilizas para buscar y remplazar. Entonces patron.match "texto" En ruby sería:
1
/hol/.match('hola')
Si hay match obtendremos => # pero si no lo hubiese obtendríamos nil.
Patrones ¿Tiene vocales? RegEx no solo nos permite buscar palabras exacta, nos permite buscar patrones
1 2 3 4 5 6
/[aeiou]/.match "zrk" nil /[aeiou]/.match "zerg" # /[aeiou]/.match "murcielago" #
Los patrones que están entre [] son uno o el otro, o sea se el patron es que tenga la letra a, o la e, o la i, o la o, o
la u.
¿Tiene números? Es posible saber si un texto tiene números ocupando:
1 2 3 4
/[0-9]/.match "yo no tengo" #nil /[0-9]/.match "yo tengo 1" #
el signo guión permite realizar rangos siempre y cuando se esté dentro de los corchetes []
¿Tiene dos dígitos consecutivos? Todo lo que esté dentro de los corchetes es un o, y nosotros necesitamos un y, para eso lo que tenemos que hacer es repetir el patrón
1 2 3 4
/[0-9][0-9]/.match "yo no cumplo por que tengo 1 digito" # nil /[0-9][0-9]/.match "yo cumplo por que tengo 20 digitos consecutivos" #
¿Una patente de vehículos? Estás cambian dependiendo del país, pero en muchos son letras - número o viceversa. Supongamos que PAC-201 fuera una patente válida.
1 2
patron_patente = /[A-Z][A-Z][A-Z]-[0-9][0-9][0-9]/ patron_patente.match "QTZ-410"
En este ejercicio aprendimos dos cosas más, una es que las expresiones regulares se pueden guardar en variables,
la segunda es que el - fuera de los [] funciona literalmente, no implicar un rango.
10, 100, 1000 o 10000 Con regex podemos hacer match de un número o de una palabra a pasar de no saber cuantos caracteres (o dígitos) tiene. Para eso ocuparemos dos caracteres especiales nuevos, el asterisco (*) y el signo de interrogación (?) el + implica que el patron aplicado debe aparecer 1 o más veces el * implica que el patron aplicado puede aparecer 0 o más veces el ? implica que el patron aplicado puede aparecer 0 o 1 vez
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/[0-9][0-9]+/.match "10" # => # /[0-9][0-9][0-9]+/.match "10" # => nil /[0-9][0-9]+/.match "100" # => # /[0-9][0-9]+/.match "1000" # => # /[0-9][0-9]*/.match "10" # => # /[0-9][0-9][0-9]*/.match "10" # => #
Finalmente el caracter que todo lo puede ser, el punto.
1
/.+/.match "hola" # cualquier caracter 1 o más veces.
Una buena aplicación para probar expresiones regulares es rubular. http://rubular.com/
16) Scrapping Scrapping es una técnica de extracción de datos de la web, y es muy similar a leer archivos, sólo que en lugar de abrir un archivos abriremos una página web. Primero necesitamos incorporar la librería open-uri, eso lo hacemos con:
1
require 'open-uri'
Luego:
1 2 3
open(url) do |f| page_string = f.read end
Entonces si quisieramos leer el twitter de desafiolatam lo podríamos hacer asi:
1 2 3 4 5 6 7 8
require 'open-uri' url = "https://twitter.com/desafiolatam" open(url) do |f| page_string = f.read end puts page_string
¿Pero ahora como leemos este string gigante que consiste en todo el HTML y del cual cuesta mucho sacar información?. Hay dos opciones buenas, una es Nokogiri, la otra Mechanize esta última ocupa a Nokogiri.
Introducción a Mechanize Como primera prueba vamos a hacer un mini scrap de la wikipedia. Paso 1) instalar la gema Mechanize desde el terminal
1
gem install mechanize
Paso 2) crear el parser
1 2 3 4 5 6 7 8 9 10 11
require "mechanize" def main() agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' agent.get("http://es.wikipedia.org/wiki/2014") do |page| puts page.parser.css('#content').text end end main()
¿Cómo funciona? Primero crea una agente, este agente es una instancia de mechanize, se suele usar como nombre agent, o mechanize,este agente será el que se conecte con la página y la descargue. En línea 5 se especifica un user_agent así el servidor que te entrega la página creerá que el navegador que la está viendo es Safari, es posible también ocupar user_agents de Firefox, Chrome, o incluso Internet Explorer. Finalmente el agente se conecta con la página de la wikipedia y muestra el contenido que esté debajo dentro de la etiqueta con id="content", o sea que este script no muestra la información que está en las barras de navegación. Para saber donde tienes que apuntar puedes dentro de una página web puedes entrar a ella y ocupar el inspector de elementos.
Navegando por los tags En el caso anterior obtuvimos todos el texto removiendo el HTML, pero en ciertos casos queremos obtener el HTML, por ejemplo los links
1 2 3 4
require "mechanize" def parse(url, tag) agent = Mechanize.new
5 6 7 8 9 10 11
agent.user_agent_alias = 'Mac Safari' agent.get(url) do |page| page.search(tag).each {|t| puts t.text; puts t} end end parse("http://blog.desafiolatam.com", "a")
Navegando en google 1 2 3 4 5 6 7 8 9 10 11 12 13
require 'mechanize' agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' page = agent.get('https://www.google.com/') form = page.forms.first form['q'] = 'desafiolatam' page = form.submit page.search('a').each do |link| puts link.text puts link puts "----" end
Más documentación sobre mechanize http://docs.seattlerb.org/mechanize/Mechanize.html
Lyrics Para ejercitar con un caso más difícil vamos a crear un buscador de lyrics de canciones haciendo scrapping de la página http://www.azlyrics.com/ Para eso tenemos que entrar al sitio, llenar el formulario, leer entre los resultados posibles, entrar al link del resultado y finalmente mostrar los lyrics. Una buena técnica para la resolución de problemas es dividirlos en componentes pequeñas que podamos resolver, para eso primero leeremos los lyrics de una página en específico. Para eso vamos a buscar Hello de Lionel Ritchie en la página, encontrar la url, y hacer un get de esa página con mechanize.
1 2 3 4 5 6 7 8
require 'mechanize' agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' page = agent.get('http://www.azlyrics.com/lyrics/lionelrichie/hello.html' page.search('div').each do |div| puts div.text end
XPATH El problema del método anterior es que vamos a obtener mucho ruido de la página que no nos interesa, lo bueno es que el inspector de elementos de chrome (y el de otros navegadores) nos puede ayudar a seleccionar un elemento específico a través del XPATH y como Mechanize está construido sobre Nokogiri lo soporta.
Así que sólo debemos copiarlo y utilizarlo en lugar de buscar ‘div’.
1 2 3
page.search('/html/body/div[3]/div/div[2]/div[6]').each do |div| puts div.text end
Ahora si veremos únicamente los lyrics como output. Una pregunta importante es si en otro lyric esto funcionaría, para eso lo probaremos manualmente cambiando la url.
1 2 3 4 5 6 7 8 9
require 'mechanize' agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' page = agent.get('http://www.azlyrics.com/lyrics/scorpions/stilllovingyou.html' ) page.search('/html/body/div[3]/div/div[2]/div[6]').each do |div| puts div.text end
Al probarlo veremos los lyrics, eso quiere decir que llegando a la página podemos leerlo, ahora tenemos que lograr los pasos previos, o sea utilizar el formulario para buscar, y seleccionar la canción respectiva.
El formulario de búsqueda La parte del formulario es similar a la que hicimos en google, en la página principal tenemos que tomar el formulario y pasarle un parámetro, con el inspector podemos ver que el nombre del input al igual que en google es q
. Luego de hacer la búsqueda llegaremos a otra página distinta que también tendremos que analizar.
1 2 3 4 5 6 7 8 9
require 'mechanize' agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' page = agent.get('http://www.azlyrics.com/') # Cargamos la pagina inicia form = page.forms.first form['q'] = 'hello lionel richie' page = form.submit # Enviamos el formulario y recibimos la página resultante puts results_page.body
Logramos llevar bien hasta la página de resultados de la búsqueda, el último paso es llevarlo a la página de los lyrics siguiendo el link, para este caso vamos a escoger siempre el primer resultado.
1 2 3 4 5 6 7 8 9
require 'mechanize' agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' page = agent.get('http://www.azlyrics.com/') # Cargamos la pagina inicia form = page.forms.first form['q'] = 'hello lionel richie' page = form.submit # Enviamos el formulario y recibimos la página resultante puts results_page.body
Analizando la página de resultados
Vamos a ver que existen muchos links, algunos apuntan a resultados, pero otros son como la navegación, paginación, publicidad, etc … apuntan fuera y no nos sirven, ahora por suerte existe un patrón, todos los resultados dentro de la página apuntan dentro de http://www.azlyrics.com/lyrics/
1 2 3
results_page.links_with(:href => /http:\/\/www.azlyrics.com\/lyrics/ puts links end
Juntando las partes Ahora nosotros estamos construyendo una solución sencilla, así que tomaremos sólo el primer resultado, entraremos a esa página y mostraremos los lyrics.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
require 'mechanize' agent = Mechanize.new puts "¿Qué lyrics deseas buscar?" query = gets.chomp() agent.user_agent_alias = 'Mac Safari' page = agent.get('http://www.azlyrics.com/') # Cargamos la pagina inicia form = page.forms.first form['q'] = query
results_page = form.submit # Enviamos el formulario y recibimos la página result
lyrics_link = results_page.links_with(:href => /http:\/\/www.azlyrics.com\/lyric lyrics_page = lyrics_link.click() lyrics_page.search('/html/body/div[3]/div/div[2]/div[6]').each do |div puts div.text end
Una última mejora, en caso de no encontrar resultados lo notificaremos, y guardaremos la lógica en una función para futura reutilización.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
def scrap_lyrics(query) require 'mechanize' agent = Mechanize.new agent.user_agent_alias = 'Mac Safari' page = agent.get('http://www.azlyrics.com/') # Cargamos la pagina inicia form = page.forms.first form['q'] = query
results_page = form.submit # Enviamos el formulario y recibimos la página resu lyrics_link = results_page.links_with(:href => /http:\/\/www.azlyrics.com\/lyr return "No hay resultados" if lyrics_link.nil? lyrics_page = lyrics_link.click() lyrics_page.search('/html/body/div[3]/div/div[2]/div[6]').each do puts div.text end end
Y ahora lo podemos utilizar simplemente con:
1
scrap_lyrics("lionel richie hello")
Un gran problema del que debemos estar conscientes con el scrapping es que si en algún momento la página cambia nosotros tendremos que cambiar nuestro algoritmo.
Desafío Generar un archivo que contenga el listado de las 100 mejores películas de Netflix que aparecen en este post. http://www.pastemagazine.com/articles/2015/10/the-100-best-movies-streaming-on-netflix-october-2.html
Preguntas ¿Para qué sirve el user agent? ¿Que devuelve form.submit?
¿Cómo podemos pasarle parámetros (inputs) al formulario? ¿Qué es el XPATH?
17) Objetos avanzado Self y main self es un objeto que apunta al objeto actual, o sea devuelve el objeto dentro del que estemos, si lo hacemos directamente entraremos al nivel superior llamado main.
1 2
self # main
Main y top-level Es en main donde es donde comúnmente definen las variables y métodos que no le pertenecen a ningún objeto, aquí al igual que en cualquier otro objeto podemos guardar variables locales, de instancia y de clase.
1 2 3 4 5
@x = 5 instance_variables # => [:@prompt, :@x] self.instance_variables # => [:@prompt, :@x]
Hay que recordar que el intérprete funciona distinto a un script, si usamos el mismo código dentro de un archivo .rb necesitas ocupar puts o inspect
1 2 3 4
@x = @y = puts # =>
5 7 instance_variables.inspect() [:@x, :@y]
y además no veremos el prompt. Cuando definimos una función esta no es más que un método del top-level main, ahora por definición self apunta al objeto actual, así que si devolvemos self dentro de una función obtendremos como resultado main
1 2 3 4 5 6
def prueba self end puts prueba() # => main
Pero si devolvemos self dentro de un objeto que sucedería?
1 2 3 4 5 6 7 8 9 10 11 12
class PruebaObjeto def initialize end def returnself self end end o = PruebaObjeto.new() # => # o.returnself
De este código podemos aprender dos cosas, uno es que podemos devolver una instancia del objeto desde cualquier método y otra es que initialize siempre devuelve self, para dejarlo completamente claro vamos a devolver otra cosa y ver que pasa.
1 2 3 4 5 6 7 8 9
class PruebaInitialize def initialize return 2 end end a = PruebaInitialize.new() puts a #
Si initialize devolviera el valor pedido, en este caso 2, a debería tener el valor 2, sin embargo observamos que devuelve self.
Llamados implícitos vs explícitos Para entender esta sección tenemos que entender un concepto que viene del lenguaje smalltalk que dice que los objetos se comunican por mensajes, ahora la parte importante y a veces contraintuitiva es quien es el receptor (en inglés el término es receiver) del mensaje. Cuando nosotros hacemos objeto1.método el receptor del mensaje es objeto1, ahora dentro del objeto nosotros nos podemos referir a el implícitamente o explícitamente. Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Foo def initialize() bar # forma implícita self.bar # forma explícita end def bar() puts "yeea" end end Foo.new() # yeaa # yeaa # => #
O sea podemos llamar a los métodos tanto de forma implícita o explícita y obtendremos los mismos resultados, pero veremos ahora que esta diferencia tiene implicancias.
Métodos públicos privados y protegidos. En ruby todas las variables son privadas y los métodos son públicos, sin embargo los métodos pueden ser privados o protegidos en caso de que se necesite. Advertencia: los protected y los private no son como otros lenguajes.
Los métodos privados
1 2 3 4 5 6 7 8 9 10 11 12 13
class Persona attr_reader :x, :y def initialize(x, y) @x = x @y = y no_tocar end private def no_tocar puts "heeey !!!" end end
Si intentamos ocupar este método desde dentro del constructor (o cualquier otro método de instancia) veremos que la clase existe, pero si lo intentamos ocupar directamente desde dentro de un instancia veremos que no existe.
1 2 3 4 5
a = Persona.new(2,3) # heeey !!! a.no_tocar # NoMethodError: private method `no_tocar' called for
Existe otra restricción, un método privado tampoco puede ser llamado de forma explícita dentro de un objeto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Persona attr_reader :x, :y def initialize(x, y) @x = x @y = y no_tocar # llamado implícito funciona self.no_tocar # pero llamado explícito no funciona end private def no_tocar puts "heeey !!!" end end
16 17
heeey !!! NoMethodError: private method `no_tocar' called for # Zombie
La herencia nos permite tratar al zombie como si fuera una persona, y de esta forma entregarle todo el comportamiento que tiene la clase padre, aunque en este caso el comportamiento corresponde exclusivamente a la clase Persona.
1 2
Zombie.new(2,3).class # => Persona
Clase, Superclase, y tipo Ya vimos en capítulos anteriores que Zombie.class es class, o sea la clase de la clase es clase, no hay duda de eso, pero ahora veremos que existe también la superclass y esta nos permite saber de donde está heredando una clase.
1 2 3 4
Zombie.class # => class Zombie.superclass # => Persona
Es posible conocer a todos los ancestros ocupando el método de clase .ancestors
1 2
Zombie.ancestors # => [Zombie, Persona, Object, Kernel, BasicObject]
Ruby ocupa mucho la herencia internamente, de hecho todos los objetos heredan del objeto objeto
1 2
Persona.new.superclass # => object
También es posible preguntar si un objeto pertenece a alguna clase, o hereda de algunas clase, por ejemplo:
1 2 3 4 5 6 7
z = Zombie.new(1,1) z.is_a?(Zombie) # => true z.is_a?(Persona) # => true z.is_a?(Fixnum) # => false
Herencia de estados, métodos y super Es posible para una clase hija tener estados que la clase padre no posea y también es posible reescribir completamente (o parcialmente) un método de la clase padre.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Persona attr_reader :x, :y def initialize(x, y) @x = x @y = y end end class Zombie < Persona attr_reader :days_since_zombie def initialize(x,y) super(x,y) @days_since_zombie = 0 end end
Por ejemplo en este caso un Zombie tiene su propio método initialize, por lo que cuando se cree Zombie.new se llamará a este, pero dentro se llama al método initialize del padre, el método super llama al método padre que se llama igual. y luego asigna la variable @days_since_zombies = 0
La idea de la herencia es evitar reescribir código, o sea haber hecho esto sería erróneo:
1 2 3 4 5 6
class Zombie < Persona attr_reader :days_since_zombie def initialize(x,y) super(x,y) end end
Ya que Zombie ya incluye el método initialize de Persona, reescribirlo para llamarlo sería inútil.
Herencia de los métodos privados y protegidos Los métodos privados y protegidos también son heredados, pero las restricciones siguen en pie, o sea debe ser ocupado desde dentro de la clase, no puede ser ocupado desde fuera, en el caso de los privados deben ser ocupados de forma implícita, y los protegidos pueden ser ocupados de forma implícita o explícita.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
class Persona attr_reader :x, :y def initialize(x, y) @x = x @y = y end private def no_tocar puts "heeey !!!" end end class Zombie < Persona attr_reader :days_since_zombie def initialize(x,y) super(x,y) no_tocar # se llama al método e imprime heeey @days_since_zombie = 0 end end z = Zombie.new(2,4)
Preguntas Qué keywords generan la diferencia entre un receptor implícito y uno explícito ¿Cuál es la diferencia entre private y protected? ¿Qué devuelve self fuera de cualquier objeto? ¿Qué devuelve self a nivel de una clase? ¿Qué devuelve self dentro de un método de instancia de una clase? ¿Qué devuelve self dentro de un método de clase? ¿Qué devuelve siempre el constructor independiente del return? ¿Para qué sirven los métodos protegidos? ¿Qué sucede si una clase hereda de otra pero redefine un método, cual manda? ¿Qué hace la instrucción super?
18) Módulos y Mixins Los módulos son muy parecidos a las clases en ruby, en el sentido de que se declaran, tienen métodos, y los puedes ocupar para definir constantes, pero a diferencia de las clases los módulos no pueden ser instanciados. Se les llama módulos porque sirven para modularizar el código (doooh !!!), o sea dividir el código en partes pequeñas reutilizables, además sirven para guardar cosas bajo un espacio de nombre, y esto es útil porque hay que recordar que las variables globales son nuestros enemigos.
Módulos vs clases Por ejemplo supongamos que estamos construyendo un software matemático donde hay diversas fórmulas, si bien podríamos guardar las fórmulas en una clase e implementar los métodos como métodos de clases, también lo podríamos hacer dentro de un módulo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Formula @@PI = 3.1415 def self.PI @@PI end def self.diameter(r) 2*r end def self.perimeter(distance) diameter(distance) * PI end end
Podemos hacer lo mismo de forma más sencilla con módulos
1 2 3 4 5 6
module Formula PI = 3.1415 def diameter(r) 2*r end
7 8 9 10 11
def perimeter(distance) diameter(distance) * PI end end
Pero ahora para utilizar la constante PI fuera del módulo deberemos hacer:
1
Formula::PI
Mientras que si estuviésemos ocupando una clase sería:
1
Formula.PI
En la utilización son técnicamente iguales, pero la definición es más limpia en el caso de los módulos, además tienen distintos usos, y eso es lo que veremos a continuación.
Módulo en archivo externo Los módulos pueden estar definidos en el mismo archivo donde estamos trabajando o en un o externo. Si nuestro módulo está definido en otro archivo externo podemos cargarlo con require o require_relative.
1 2
require_relative 'formula' Formula::PI
Debemos tener cuidado de especificar la ruta correcta al archivo, la extensión .rb no es necesaria, pero no afecta si se pone.
Includes Hasta ahora los módulos como los hemos estudiado no presentan mayor utilidad, pero es porque todavía nos faltan estudiar los mixins. Tenemos un módulo Formula, ahora vamos a construir la clase Círculo en el archivo circulo.rb en la misma carpeta.
1 2 3 4 5 6 7
require_relative 'formula.rb' class Circulo include Formula end puts Circulo::PI
Y aquí podemos ver que los módulos funcionan similar a la herencia pero con la ventaja de que es posible incluir diversos simultáneamente, todo los métodos del módulo serán incluidos como métodos de instancia de la clase que los incluya. veamos el siguiente ejemplo:
1 2
c = Circulo.new puts c.diameter(5)
o sea nuestro objeto ahora tiene un método de instancia que fue importado del módulo Formula a través de includes. Cabe destacar que Ruby no tiene multiherencia pero a través de los includes podemos implementar muchas funcionalidades de otras clases, un ejemplo clásico de esto son los arrays de ruby que implementan el módulo enum.
Extends Además de los includes existen los extends, son muy similares pero incluyen los métodos del módulo como métodos de clase.
1 2 3 4 5
require_relative 'formula.rb' class Circulo extend Formula end
en este caso el uso sería sobre la clase.
1 2 3 4 5 6 7
require_relative 'formula.rb' class Circulo extend Formula end Circulo.diameter(5)
Herencia + includes A modo de repaso vamos a crear un pequeño ejemplo donde ocupemos simultáneamente herencia e includes, para eso vamos a ocupar el módulo de formulas, crearemos una clase punto y una clase círculo que hereda de punto, este punto sería el punto central del círculo.
1 2 3 4 5 6 7
module Formula PI = 3.1415
1 2 3 4 5 6 7
class Punto attr_accessor :x, :y def initialize(x,y) @x = x @y = y end end
1 2 3 4 5 6 7 8
class Circulo < Punto include Formula def initialize(x,y, r) super(x,y) @r = r end
def diameter(r) return 2*r end end
9 10 11 12 13
1 2
def diameter() super(@r) end end
c = Circulo.new(2,3,4) puts c.diameter
Lo interesante que hicimos fue reescribir el método diameter, y llamamos a super utilizando nuestro radio, en este caso ese método no corresponde al punto, si no que corresponde a la fórmula.
Clases dentro de los módulos También es común que hayan clases dentro de módulos.
1 2 3 4 5
module Foo class Bar @@a = 10 end end
y podemos utilizar estas clases de la misma forma:
1 2 3 4 5 6 7 8 9 10 11
module Foo class Bar @@a = 10 def self.get_a return @@a end end end puts Foo::Bar.get_a
Módulos dentro de los módulos Al igual como se pueden insertar clases dentro de los módulos, también es perfectamente posible ingresar módulos dentro de los módulos, como por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13
module Formula module Matematicas PI = 3.1415 def diameter(r) return 2*r end def perimeter(distance) return diameter(distance) * PI end end end
Usarlo sería tan simple como
1
puts Formula::Matematicas::PI
Desafio Crear un modulo que tenga métodos que permitan leer un archivo csv, y guardar el archivo csv, dado un arreglo de objetos. Si el tamaño del arreglo no cumple con el layout de nuestro archivo, lanzar una excepción.
Preguntas ¿Cuáles son las diferencia entre un módulo y una clase? ¿Cuál las diferencias entre herencia e incluir un módulo? ¿Cuál es la diferencia entre require_relative e include? ¿Cuál es la diferencia entre include y extends? ¿Cuál es la diferencia entre Formula::PI y Formula.PI?
Si tenemos la clase Persona, y la constante NUMERO_DE_PERSONAS_EN_LA_FIESTA_X, ese constante sería mejor definirla dentro de una clase, o dentro del módulo?
19) Testing Testing es una técnica que consiste en crear pruebas para verificar que nuestro código funciona o mejor dicho que no falla La clave para entender testing es que no se trata de validación, se trata de invalidación, o sea en hacer pequeños scripts que intenten probar que nuestro código va a fallar. Un buen test detecta fallas, uno malo no aporta información. Ruby al igual que muchos otros lenguajes trae herramientas incorporadas para crear tests automatizados que corran bajo una sola línea de comando, y de esta forma podemos revisar cualquier set de cambios corriendo esta línea. Realizar los test es sencillo, lo difícil es saber que es lo que hay que testear, esto lo vamos a ir aprendiendo con la práctica
Mi primer test Supongamos que queremos calcular la hipotenusa de un triángulo, esta es una fórmula muy sencilla, la hipotenusa al cuadrado es igual a la suma de los catetos al cuadrado:
1
h^2 = c1^2 + c2^2
Entonces si queremos un método que calcula la hipotenusa sería algo como:
1 2 3
def hipotenusa(c1, c2) h = Math.sqrt(c1*c1 + c2*c2) end
Este código lo guardaremos en un archivo llamado hipotenusa.rb Ahora nosotros perfectamente podemos abrir una consola en la misma carpeta con irb y cargar el archivo
1 2 3
require hipotenusa hipotenusa(3,4) end
y obtendremos como resultado 5 Para hacer testing automatizado con ruby vamos a ocupar una gema, aquí no tenemos un gemfile como tal como en los proyectos de ruby, pero eso no quiere decir que no tengamos un gemset Corremos en el bash
1
gem install test-unit
Estructura de un test
1 2 3 4 5 6 7 8 9 10
require 'test/unit' require_relative '../hipotenusa' class TestHipotenusa < Test::Unit::TestCase #Hereda de la clase test def test_resultado_pitagorico # Cada método es un test # cada método debe incluir un assert assert_equal 5, hipotenusa(3,4), "Suma Positivos Funciona" end end
Un test case contiene varios tests, cada uno de estos vienen con un assert, que es la prueba, pero cada test tiene un sólo assert, (por ahora) es por eso que a este tipo de testing se le llama Unit testing estamos probando el código a través de unidades mínimas funcionales. Hay diversos tipos de assert para distintos Test, por ejemplo nos podría interesar sólo que el resultado fuera mayor que, menor que, distinto que, o si un arreglo contiene un elemento. Normalmente todos los tests se guardan dentro de una carpeta llamada tests, es por eso mismo que el require_relative hace un '../hipotenusa' , es porque está cargando el archivo hipotenusa de la carpeta padre. Ahora si el archivo de test está dentro de la carpeta de la test, y nosotros estamos ubicado en la carpeta del proyecto entonces podemos correr los tests con:
1
ruby test/test_hipotenusa.rb
y podemos ver los resultados:
1 2 3 4 5
Finished in 0.000526 seconds. -------------------------------------------------------------------1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions 100% passed --------------------------------------------------------------------
Hagamos ahora que nuestro test falle, para eso vamos a comentar la línea de código que devuelve la hipotenusa
1 2 3
def hipotenusa(c1, c2) # h = Math.sqrt(c1*c1 + c2*c2) end
Y ahora al correr los test veremos:
1 2 3 4 5 6 7 8 9 10
Failure: test_result(TestHipotenusa) test/test_hipotenusa.rb:6:in `test_result' 3: 4: class TestHipotenusa < Test::Unit::TestCase 5: def test_result => 6: assert_equal 5, hipotenusa(3,4) 7: end 8: end expected but was
Los test pueden tener 4 resultados posibles success failure error skipped Los primeros dos ya los revisamos, success es si nuestra prueba es pasada, failure es si falla, error es parecido pero resulta cuando hay un error sintáctico en el test y finalmente skipped es si el test tiene nombre pero no está implementado.
¿Qué testear? La práctica hace al maestro, pero lo que no hay que perder de vista es que hay que tratar de que nuestro código falle en lugar de comprobar que funcione, hay algunos tips para eso: 1. Revisar las condiciones, o sea si hay un if o while probar ambas condiciones 2. Revisar los bordes, por ejemplo si hay un NoMethodError: private method `no_tocar' called for #
View more...
Comments