Introducción informal a Matlab y Octave - Temas avanzados (I)
16 - Temas avanzados (I)
Tutorial creado por Guillem Borrell i Nogueras. Extraido de: http://torroja.dmt.upm.es/%7Eguillem/matlab/
05 de Noviembre de 2006
< anterior
| 1
... 14
15
16 17
18
19
20
... 27
| siguiente >
Este capítulo nace de la necesidad de recojer todos los argumentos no necesariamente ligados al uso de Matlab. La mayoría de ellos están relacionados con la programación general o en cálculo numérico, sin embargo son de gran utilidad para escribir buenos programas. La teoría que contiene este capítulo es de un nivel mucho más elevado al resto, estais avisados; esto no significa que todo esté explicado del modo más sencillo posible.
Se dice que una operación es escalar cuando se hace elemento a elemento. Una suma escalar de dos vectores es tomar los elementos de cada uno de ellos, sumarlos y asignar el resultado a un tercer vector. Una operación es vectorial cuando se hace por bloques mayores en la memoria. Una suma vectorial de dos vectores sería tomar partes del los vectores o los vectores enteros y sumarlos de golpe.
Los compiladores modernos son capaces de vectorizar automáticamente. Advierten que dos bucles pueden combinarse perfectamente y realizan la operación por bloques ahorrando memoria y tiempo de cálculo. Como Matlab es un programa secuencial carece de esta capacidad de optimización. Si nosotros le pedimos un bucle con operaciones escalares lo va a realizar sin ningún tipo de optimización. Si en cambio asignamos operamos las matrices mediante la notación matricial y las submatrices Matlab sí va a ser capaz de vectorizar la operación.
En la sección 7.1.1.1 explicaremos la importancia que todas estas consideraciones tienen sobre la velocidad de ejecución.
La lentitud de los bucles llega hasta límites insospechados. Supongamos que queremos multiplicar todas las filas de una matriz por un escalar distinto. En un alarde decidimos convertir la serie de números en un vector y utilizar un bucle contador para operar la matriz por filas del siguiente modo:
A partir de ahora nos lo pensaremos dos veces antes de escribir la palabra for. Si nos acostumbramos pensar con submatrices nos ahorraremos tiempo de cálculo y la engorrosa tarea de migrar código a Fortran inútilmente.
Imaginemos que queremos sumar dos vectores y asignar el resultado a un tercero y que para ello utilicemos un bucle. Primero tomaremos el los primeros índices de cada vector y los situaremos en una posición de memoria nueva. Esto sucederá a cada paso con lo que cada iteración implicará una operación de reserva de memoria al final de un vector.
Cada vez que ampliamos un vector llenando una posición vacía Matlab debe comprobar que el elemento no existe, ampliar la memoria reservada al vector para poder situar el nuevo elemento donde debe y rellenar el resto con ceros y finalmente almacenar los datos del nuevo vector.
Cuando sumamos dos vectores escalarmente el ciclo de verificación-reserva -asignación-cierre se realiza una sola vez. Podemos concluir entonces que la operación de ampliación de una matriz en Matlab es especialmente lenta. Aunque no estemos obligados a declarar las variables antes de inicializarlas es siempre una buena práctica comprobar que cada matriz se defina entera o mediante bloques lo suficientemente grandes.
Este comportamiento está ligado al funcionamiento de los arrays en C; un buen texto para comprenderlo mejor es [3] donde encontraremos un capítulo inicial sobre qué es verdaderamente un array y qué relación tiene con un puntero.
Como curiosidad diremos que mientras las operaciones de reserva y liberación de memoria son bastante lentas, las operaciones de manipulación de forma como la función reshape son especialmente rápidas. No debemos tener miedo a cambiar la forma de las matrices según nuestras necesidados pensando que estamos sacrificando tiempo de ejecución.
Algunos lenguajes disponen de un recolector automático de basura pero no es el caso de Matlab. Si creamos una función que utiliza variables internas Matlab no liberará la memoria que hayan utilizado cuando termine su ejecución. Si esta función reserva una gran cantidad de memoria es muy importante que nos acordemos de utilizar la función clear para liberar la memoria utilizada. Incluso Fortran, sin recolector de basura y que pasa los argumentos por referencia, no tolera estos errores puesto que manda la memoria reservada en las subrutinas a un stack o un heap; en el caso de desbordarlo da un error en tiempo de ejecución. Las variables verdaderamente grandes requieren una reserva de memoria estática.
En la mayoría de los programas esto no será necesario, hay que trabajar con matrices verdaderamente grandes para ocupar una parte significativa de la memoria de un ordenador moderno. Pero es siempre una práctica muy higiénica liberar la memoria al final de cada función. Al igual que en Fortran, la responsabilidad de que un programa no tenga pérdidas de memoria recae en el programador, no en el lenguaje o sus herramientas.
¿Qué tienen los enteros que no tengan los reales? Esta pregunta suele formularse al revés; ahora nos interesa para qué puede servir un entero y no es capaz de hacer un real. Los enteros pueden servir para almacenar cantidades de bits de un modo ordenado. Esto abre la puerta a el uso de funciones que no operan según la aritmética decimal usual sino que manipulan bits de enteros de distinto tipo. Ya no debemos pensar en un entero como la expresión de un número decimal sino que podemos utilizarlo para la descripción de máscaras lógicas, algoritmos genéticos, probabilidad...
Las funciones de manipulación de enteros por bits son las siguientes:
En programación suelen evitarse este tipo de estructuras, son lentas, difíciles de programar, difíciles de entender y propensas a generar errores en tiempo de ejecución que cuesta bastante resolver. En otros lenguajes de programación los defectos de forma suelen ser los más importantes pero ya hemos aprendido que en Matlab es una buena práctica programar con la velocidad en mente.
Una solución especialmente eficiente es ver que los grupos de números binarios son un conjunto no intersectado en los que las operaciones de adición, sustracción y combinación lógicas son triviales. Esto sirve para constuir encima de cada matriz una ``máscara lógica'' que permite asignar una etiqueta a cada elemento de la matriz.
El debugging se basa en los breakpoints que no son más que puntos en los que podemos detener la ejecución del programa para analizar su estado actual. La posición de los breakpoints es más una labor de experiencia que una ley tanto en los lenguajes compilados como interactivos. Solemos poner uno antes de llamar una función y unos cuantos antes de que aparezca el error.
El editor de Matlab es además el interfaz para el debugger. Podremos poner y quitar los breakpoints con el ratón y recuperar el control del proceso con la consola. Pero cuando uno se siente cómodo con el debugging prefiere realizar todo el proceso manualmente mediante las funciones propias. Estas funciones son casi las mismas en Matlab y Octave.
Para eliminar alguno de los breakpoints:
7.1 Aumentar la calidad del código escrito en Matlab
Que el código funcione no suele ser suficiente. Debemos intentar en cualquier caso escribir código de calidad, debemos convertir en hábitos ciertas prácticas de programación orientadas a hacer más fácil el uso de las funciones y los scripts. No todo termina en escribir una pequeña ayuda en cada función. Hay estrategias muy útiles para aumentar significativamente la potencia del código escrito sin necesidad de aumentar el esfuerzo. Debemos entender que si Matlab es una plataforma de desarrollo rápido de aplicaciones dispondrá de funciones para escribir código de un modo más eficiente.7.1.1 Vectorizar, la clave para aumentar la velocidad
Hay muchas maneras de asignar un argumento a una variable. Cuando se crearon los ordenadores y empezaron a surgir los lenguajes de programación casi todos los procesos eran escalares. Todo estaba gobernado por operaciones lógicas que operaban unidades muy pequeñas de memoria. A medida que los ordenadores iban creciendo en potencia y versatilidad se empezó a pensar en una manera más eficiente de calcular. Uno de los conceptos era la vectorización.1Se dice que una operación es escalar cuando se hace elemento a elemento. Una suma escalar de dos vectores es tomar los elementos de cada uno de ellos, sumarlos y asignar el resultado a un tercer vector. Una operación es vectorial cuando se hace por bloques mayores en la memoria. Una suma vectorial de dos vectores sería tomar partes del los vectores o los vectores enteros y sumarlos de golpe.
Los compiladores modernos son capaces de vectorizar automáticamente. Advierten que dos bucles pueden combinarse perfectamente y realizan la operación por bloques ahorrando memoria y tiempo de cálculo. Como Matlab es un programa secuencial carece de esta capacidad de optimización. Si nosotros le pedimos un bucle con operaciones escalares lo va a realizar sin ningún tipo de optimización. Si en cambio asignamos operamos las matrices mediante la notación matricial y las submatrices Matlab sí va a ser capaz de vectorizar la operación.
En la sección 7.1.1.1 explicaremos la importancia que todas estas consideraciones tienen sobre la velocidad de ejecución.
7.1.1.1 El truco más importante de la programación en Matlab
El truco más importante para que nuestros scripts tengan una velocidad aceptable es evitar los bucles con contador. Es la estructura más lenta que existe en el lenguaje. El siguiente ejemplo nos ayudará a entenderlo perfectamente. Crearemos dos matrices de números aleatorios y las sumaremos creando una tercera matriz. Primero lo haremos mediante un bucle que sume con dos índices y luego utilizando el operador suma elemento a elemento. Utilizaremos la función rand para crear las matrices y la pareja tic y toc para calcular el tiempo de cálculo.a=rand(66) #matriz de 66 x 66 b=rand(66) >> tic;for i=1:66;for j=1:66;c(i,j)=a(i,j)+b(i,j);end;end;toc ans = 0.70488 >> tic;c=a.+b;toc ans = 0.0022700Donde el número que obtenemos como resultado es el tiempo transcurrido entre la llamada de tic y la de toc. La diferencia entre los dos métodos es de2:
>> 0.70488/0.00227 ans = 310.52Utilizar los operadores matriciales y las submatrices generará código del orden de 100 veces más rápido. Para una EDP esto es la diferencia entre un rato de espera y una semana de cálculos, sólo un contador mal puesto puede acabar con un código globalmente bien escrito.
La lentitud de los bucles llega hasta límites insospechados. Supongamos que queremos multiplicar todas las filas de una matriz por un escalar distinto. En un alarde decidimos convertir la serie de números en un vector y utilizar un bucle contador para operar la matriz por filas del siguiente modo:
>> a=1:66; >> b=rand(66); >> tic;for i=1:66;c(i,:)=a(i)*b(i,:);end;toc ans = 0.029491Para eliminar este bucle tenemos que convertir la secuencia de números en una matriz de 666 y luego multiplicarla por una matriz. Qué sorpresa nos llevamos cuando observamos que el tiempo de proceso es menor:
>> tic;c=a'*ones(1,66).*b;toc ans = 0.0023100Eliminando un bucle que parecía completamente justificado acabamos de reducir el tiempo de proceso a la décima parte.
A partir de ahora nos lo pensaremos dos veces antes de escribir la palabra for. Si nos acostumbramos pensar con submatrices nos ahorraremos tiempo de cálculo y la engorrosa tarea de migrar código a Fortran inútilmente.
7.1.1.2 ¿Por qué son tan lentos los bucles?
Lo que hace que los bucles sean tan lentos no es únicamente la ausencia de vectorización en el cálculo. Los bucles escalares son muy rápidos sea cual sea la arquitectura y el lenguaje de programación. Si analizamos con un poco más de precisión el código de los ejemplos anteriores observamos que no sólo se están multiplicando dos matrices o dos escalares, además se está reservando la memoria correspondiente al resultado.Imaginemos que queremos sumar dos vectores y asignar el resultado a un tercero y que para ello utilicemos un bucle. Primero tomaremos el los primeros índices de cada vector y los situaremos en una posición de memoria nueva. Esto sucederá a cada paso con lo que cada iteración implicará una operación de reserva de memoria al final de un vector.
Cada vez que ampliamos un vector llenando una posición vacía Matlab debe comprobar que el elemento no existe, ampliar la memoria reservada al vector para poder situar el nuevo elemento donde debe y rellenar el resto con ceros y finalmente almacenar los datos del nuevo vector.
Cuando sumamos dos vectores escalarmente el ciclo de verificación-reserva -asignación-cierre se realiza una sola vez. Podemos concluir entonces que la operación de ampliación de una matriz en Matlab es especialmente lenta. Aunque no estemos obligados a declarar las variables antes de inicializarlas es siempre una buena práctica comprobar que cada matriz se defina entera o mediante bloques lo suficientemente grandes.
Este comportamiento está ligado al funcionamiento de los arrays en C; un buen texto para comprenderlo mejor es [3] donde encontraremos un capítulo inicial sobre qué es verdaderamente un array y qué relación tiene con un puntero.
Como curiosidad diremos que mientras las operaciones de reserva y liberación de memoria son bastante lentas, las operaciones de manipulación de forma como la función reshape son especialmente rápidas. No debemos tener miedo a cambiar la forma de las matrices según nuestras necesidados pensando que estamos sacrificando tiempo de ejecución.
7.1.2 Control de las variables de entrada y salida en funciones.(+)
La necesidad de pasar una cantidad fija de argumentos a una función en forma de variables no es una limitación para Matlab. Uno de los puntos débiles de la definición de las cabeceras de las funciones es que no pueden definirse, tal como lo hacen otros lenguajes de programación, valores por defecto para las variables de entrada. Matlab cuenta con la siguiente serie de funciones dedicadas a manipular las variables de entrada y salida de las funciones:- nargin
- Da el número de argumentos con el que se ha llamado una función
- nargoun
- Retorna el número de argumentos de salida de una función
- varargin
- Permite que las funciones admitan cualquier combinación de argumentos de entrada.
- varargout
- Permite que las funciones adimitan cualquier combinación de argumentos de salida.
- inputname
- Retorna el nombre de la variable que se ha pasado como argumento de entrada en una función.
7.1.3 Comunicación entre el entorno de ejecución global y el entorno de la función
En el léxico utilizado por Matlab se habla de dos entornos de ejecución o workspaces. Existen sólo dos workspaces en los que habitan varibles inicialmente independientes. El modo usual de comunicar los dos entornos es mediante variables globales, una vez definimos una variable como global en todos los workspace la hacemos visible para todas las unidades de programa. Matlab define dos workspace, base y caller. Base es el nombre del entorno de ejecución principal; sería el intérprete en una sesión interactiva. Caller es la función que se esté activa en algún momento de la ejecución. Los dos métodos siguientes son interfaces entre las variables en base y las variables en caller.- evalin
- Evalua una variable o una expresión en cualquier entorno.
function out=testfunc()
out=evalin('base','dummy');
Ahora en una sesión del intérprete definiremos la variable var y veremos cómo queda capturada por la sentencia evalin sin que aparezca en la cabecera: >> testfunc() error: `dummy' undefined near line 23 column 1 error: evaluating assignment expression near line 2, column 4 error: called from `testfunc'Nos ha dado un error porque aún no hemos definido la variable dummy en el entorno base. Si ahora definimos la variable y llamamos la función:
>> dummy='hola' >> testfunc() ans = holaAcabamos de comuncar de un modo bastante elegante los dos entornos de ejecución. Los programadores experimentados están acostumbrados a lidiar con los punteros. nos podemos imaginar esta función como una manera razonable de emular el comportamiento de un puntero3 y así añadir algo de potencia a nuestros algoritmos. No será literalmente un puntero porque en vez de apuntar una posición de memoria apuntará a una variable pero como es la manera normal de definir los punteros podemos hacer que se comporte del mismo modo. Por ejemplo, en el caso anterior hemos definido una función que extrae el valor out que ``apunta'' al valor contenido en la variable dummy. ¿Qué sucede si cambiamos la variable dummy? Pues que en tiempo de ejecución la variable out cambiará inmediatamente de valor:
>> dummy='adios' dummy = adios >> testfunc() ans = adiosVemos que esto no es exactamente una asignación de una misma posición de memoria pero la ejecución emula el mismo comportamiento, es como hacer un out==dummy implícito.
- assignin
- Asigna un valor dado a una variable de cualquier entorno de ejecución.
7.1.4 La función clear
Más adelante en este capítulo, en la sección 7.4.3.2 y por si aún es ajeno a nosotros este concepto, hablaremos de la llamada por valor y la llamada por referencia. Matlab, como C, llama por valor. Esta característica unida a que podemos iniciar variables sin declararlas previamente hace que estemos acumulando memoria en uso. Se llama pérdida de memoria, memory leak al defecto que se presenta cuando una variable inútil no es destruida y la memoria que reserva liberada.Algunos lenguajes disponen de un recolector automático de basura pero no es el caso de Matlab. Si creamos una función que utiliza variables internas Matlab no liberará la memoria que hayan utilizado cuando termine su ejecución. Si esta función reserva una gran cantidad de memoria es muy importante que nos acordemos de utilizar la función clear para liberar la memoria utilizada. Incluso Fortran, sin recolector de basura y que pasa los argumentos por referencia, no tolera estos errores puesto que manda la memoria reservada en las subrutinas a un stack o un heap; en el caso de desbordarlo da un error en tiempo de ejecución. Las variables verdaderamente grandes requieren una reserva de memoria estática.
En la mayoría de los programas esto no será necesario, hay que trabajar con matrices verdaderamente grandes para ocupar una parte significativa de la memoria de un ordenador moderno. Pero es siempre una práctica muy higiénica liberar la memoria al final de cada función. Al igual que en Fortran, la responsabilidad de que un programa no tenga pérdidas de memoria recae en el programador, no en el lenguaje o sus herramientas.
7.2 Manipulación de bits y Array Masking
En la sección 2.5.1.1 hemos visto que Matlab puede definir argumentos cuyos tipos son números enteros de distinta precisión. Es significativo que la manera de nombrarlos sea precisamente la cantidad de bits que ocupan en memoria; el tipo uint8 es un entero sin signo que ocupa exactamente ocho bits en memoria.¿Qué tienen los enteros que no tengan los reales? Esta pregunta suele formularse al revés; ahora nos interesa para qué puede servir un entero y no es capaz de hacer un real. Los enteros pueden servir para almacenar cantidades de bits de un modo ordenado. Esto abre la puerta a el uso de funciones que no operan según la aritmética decimal usual sino que manipulan bits de enteros de distinto tipo. Ya no debemos pensar en un entero como la expresión de un número decimal sino que podemos utilizarlo para la descripción de máscaras lógicas, algoritmos genéticos, probabilidad...
Las funciones de manipulación de enteros por bits son las siguientes:
- bitand
- Adición lógica de dos cadenas de números binarios en su expresión como números enteros
- bitor
- Sustracción lógica de dos números enteros
- bitxor
- Operación de ``or'' exclusivo
7.2.1 Array masking(+)
Cuando necesitamos controlar el flujo de ejecución de un programa y este flujo necesita ciertas condiciones lógicas solemos utilizar una estructura condicional (if). Cuando dichas condiciones lógicas adquieren un alto grado de complejidad, con más de seis o siete opciones que pueden ser complementarias entre ellas, la implementación de la estructura suele ser harto complicada.En programación suelen evitarse este tipo de estructuras, son lentas, difíciles de programar, difíciles de entender y propensas a generar errores en tiempo de ejecución que cuesta bastante resolver. En otros lenguajes de programación los defectos de forma suelen ser los más importantes pero ya hemos aprendido que en Matlab es una buena práctica programar con la velocidad en mente.
Una solución especialmente eficiente es ver que los grupos de números binarios son un conjunto no intersectado en los que las operaciones de adición, sustracción y combinación lógicas son triviales. Esto sirve para constuir encima de cada matriz una ``máscara lógica'' que permite asignar una etiqueta a cada elemento de la matriz.
7.3 Introducción al debbugging
La traducción de la palabra debugging es ``quitar los bichos''. Un bug o bicho es un error en el código, sus efectos pueden ser evidentes o sutiles y la tarea de encontrarlos es mucho más complicada que eliminarlos. Los debuggers son programas especiales para esta tarea de uso en lenguajes compilados. Lo que hacen es ejecutar los procesos paso a paso para conocer su funcionamiento de un modo más interactivo. Matlab ya es en sí mismo interactivo pero algunas herramientas del debugging clásico serán útiles en programas muy grandes.El debugging se basa en los breakpoints que no son más que puntos en los que podemos detener la ejecución del programa para analizar su estado actual. La posición de los breakpoints es más una labor de experiencia que una ley tanto en los lenguajes compilados como interactivos. Solemos poner uno antes de llamar una función y unos cuantos antes de que aparezca el error.
El editor de Matlab es además el interfaz para el debugger. Podremos poner y quitar los breakpoints con el ratón y recuperar el control del proceso con la consola. Pero cuando uno se siente cómodo con el debugging prefiere realizar todo el proceso manualmente mediante las funciones propias. Estas funciones son casi las mismas en Matlab y Octave.
- keyboard
- Esta palabra clave no es parte del debugging en sentido estricto pero puede ser muy útil para resolver errores del código. Si introducimos esta función en un programa pararemos su ejecución y pasaremos a tener el control en el punto de ejecución donde nos encontremos. Se abrirá un intérprete mediante el cual accederemos al estado actual del programa para poder acceder a las variables de modo interactivo. Una vez salgamos del intérprete continuaremos la ejecución conservando los cambios que hayamos introducido. Este es el modo más sencillo de hacer debugging en scripts porque las funciones para debugging clásicas sólo operan dentro de funciones.
- echo
- Traducido eco. Controla si los comandos en archivos aparecen o no en pantalla. Esta sentencia sólo tiene efecto dentro de un archivo o de una función. Normalmente los comandos ejecutables de funciones y scripts no aparecen en pantalla, sólo aparece su resultado si no hemos puesto un punto y coma al final de la línea. Con echo on los comandos de los scripts se escriben como si los hubieramos introducido a través del intérprete.
on Activa el eco en los scripts
off Desactiva el eco en los scripts
on all Activa el eco en scripts y funciones
off all Desactiva el eco en scripts y funciones
- type
- Saca por pantalla el texto correspondiente a cualquier función que esté en el árbol de directorios dentro de un archivo .m. Es útil cuando disponemos de una colección propia de funciones bastante extensa y preferimos no abrir el archivo con el editor.
- dbtype
- Muestra la función con los números de línea para facilitar la inclusión de breakpoints
>> dbtype polyadd
1 function poly=polyadd(poly1,poly2)
2 if (nargin != 2)
3 usage('polyadd(poly1,poly2)')
4 end
5 if (is_vector(poly1) & & is_vector(poly2))
6 if length(poly1)<length(poly2)
7 short=poly1;
8 long=poly2;
9 else
10 short=poly2;
11 long=poly1;
12 end
13 diff=length(long)-length(short);
14 if diff>0
15 poly=[zeros(1,diff),short]+long;
16 else
17 poly=long+short;
18 end
19 else
20 error('both arguments must be polynomials')
21 end
Ahora queremos colocar dos breakpoints, uno en la línea 14 y otro en la línea 16. Para ello usaremos la siguiente función:
- dbstop(func,line)
- Introduce un breakpoint en una función.
>> dbstop('polyadd','14')
ans = 14
>> dbstop('polyadd','16')
ans = 17
Fijémonos que la función no nos ha dejado poner el breakpoint en la línea 16 porque no es ejecutable. Para comprobar el estado de la función:
- dbstatus
- Devuelve un vector cuyos elementos son las líneas con breakpoints.
>> dbstatus polyadd ans = 14 17Ahora utilizamos la función del modo usual. La ejecución avanzará hasta que encuentre un breakpoint, entonces se abrirá una consola que nos dará el control de la ejecución.
>> polyadd([3,2,1,3],[3,2,0]) polyadd: line 14, column 8 diff debug>La consola debug es local, es decir, sólo contiene las variables de la ejecución de la función. Lo más lógico en este punto es utilizar la función who para saber qué variables han sido iniciadas:
debug> who *** local user variables: __nargin__ argn long poly1 short __nargout__ diff poly poly2Aprovechamos para conocer algunas de ellas:
debug> long long = 3 2 1 3 debug> poly1 poly1 = 3 2 1 3 debug> poly2 poly2 = 3 2 0 debug> __nargin__ __nargin__ = 2__nargin__ es el número de argumentos de entrada. Si salimos de la consola avanzaremos hasta el siguiente breakpoint o finalizaremos la ejecución. En este caso llegaremos hasta la línea 17.
Para eliminar alguno de los breakpoints:
- dbclear(func,line)
- Elimina el breakpoint de la línea solicitada.
>> dbclear('polyadd','17')
polyadd
symbol_name = polyadd
>> dbstatus polyadd
ans = 14
Hay más comandos para debugging pero estos son los más importantes.
< anterior
| 1
... 14
15
16 17
18
19
20
... 27
| siguiente >
11 opiniones
Esta bien bueno y claro ese tutorial.
De verdad es una ayuda para el que empieza a manejar matlab.
Tutoriales relacionados con 'Introducción informal a Matlab y Octave'
Hay muchos libros de Matlab, algunos muy buenos, pero en ninguno es tratado como un...
Más »
Autor y licencia de 'Introducción informal a Matlab y Octave'
Este contenido ha sido recopilado por el equipo de Wikilearning. Todo el contenido recopilado se ha obtenido respetando y comunicando en nuestro site la licencia de cada fuente.
Wikilearning tiene permiso expreso por escrito de los autores para publicar los contenidos que ha extraído de otras webs, incluyendo su uso comercial.
