<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Tarea 1
Step1:  Lo que el codigo anterior hace es asociar al nombre np todas las herramientas de la libreria numpy. Ahora podremos llamar funciones de numpy como np.&lt;numpy_fun&gt;. El nombre np es opcional, pueden cambiarlo pero necesitaran ese nombre para acceder a las funciones de numpy como &lt;new_name&gt;.&lt;numpy_fun&gt;. Otra opcion es solo inlcuir import numpy, en cuya caso las funciones se llaman como numpy.&lt;numpy_fun&gt;. Para saber mas del sistema de modulos pueden revisar la liga https
Step2:  Vamos a construir una clase Array que incluye a las matrices y a los vectores. Desde el punto de vista computacional, un vector es una matriz de una columna. En clase vimos que conviene pensar a las matrices como transformacion de vectores, sin embargo, desde el punto de vista computacional, como la regla de suma y multiplicacion es similar, conviene pensarlos ambos como arrays, que es el nombre tradicional en programacion
Computacionalmente, que es un array? Tecnicamente, es una lista de listas, todas del mismo tamano, cada uno representando una fila (fila o columna es optativo, haremos filas porque asi lo hace numpy, pero yo previero columnas). Por ejemplo, la lista de listas
[[1,2,3],[4,5,6]]
Corresponde a la matriz
$$
\begin{bmatrix} 1 & 2 & 3 \ 4 & 5 & 6  \end{bmatrix}
$$
The numpy way
Step3:  Es posible sumar matrices y multiplicarlas por escalares
Step4:  Las matrices de numpy se pueden multiplicar con la funcion matmul dentro de numpy
Step5:  Los arrays the numpy pueden accesarse con indices y slices
Una entrada especifica
Step6:  Una fila entera
Step7:  Una columna entera
Step8:  Un subbloque (notar que un slice n
Step9:  En numpy podemos saber la dimension de un array con el campo shape de numpy
Step10:  Numpy es listo manejando listas simples como vectores
Step11:  Comenzando desde cero...
Step12:  El campo data de un Array almacena la lista de listas del array. Necesitamos implementar algunos metodos para que sea funcional como una clase de algebra lineal. 
Un metodo para imprimir una matriz de forma mas agradable 
Validador. Un metodo para validar que la lista de listas sea valida (columnas del mismo tamano y que las listas interiores sean numericas
Indexing Hacer sentido a expresiones A[i,j]
Iniciar matriz vacia de ceros Este metodos es muy util para preacolar espacio para guardar nuevas matrices
Transposicion B.transpose()
Suma A + B
Multiplicacion escalar y matricial 2 * A y A*B
Vectores (Opcional)
Con esto seria posible hacer algebra lineal
Metodos especiales de clase...
Para hacer esto es posible usar metodos especiales de clase __getitem, __setitem__, __add__, __mul__, __str__. Teoricamente es posible hacer todo sin estos metodos especiales, pero, por ejemplo, es mucho mas agradable escribir A[i,j] que A.get(i,j) o A.setitem(i,j,newval) que A[i,j] = newval.
1. Un metodo para imprimir mejor...
Necesitamos agregar un metodo de impresion. Noten que un array de numpy se imprime bonito comparado con el nuestro
Step13:  Por que estas diferencias? Python secretamente busca un metodo llamado __repr__ cuando un objeto es llamado sin imprimir explicitamente, y  __str__ cuando se imprime con print explicitamente. Por ejemplo
Step14:  Ejercicios
Step15:  Prueba de las clases
<ASSISTANT_TASK:>
Python Code:
import numpy as np
Explanation: Tarea 1: Creando una sistema de Álgebra Lineal
En esta tarea seran guiados paso a paso en como realizar un sistema de arrays en Python para realizar operaciones de algebra lineal. 
Pero antes... (FAQ)
Como se hace en la realidad? En la practica, se usan paqueterias funcionales ya probadas, en particular numpy, que contiene todas las herramientas necesarias para hacer computo numerico en Python.
Por que hacer esta tarea entonces? Python es un lenguage disenado para la programacion orientada a objetos. Al hacer la tarea desarrollaran experiencia en este tipo de programacion que les permitira crear objetos en el futuro cuando lo necesiten, y entender mejor como funciona numpy y en general, todas las herramientas de Python. Ademas, en esta tarea tambien aprenderan la forma de usar numpy simultaneamente.
Como comenzar con numpy? En la tarea necesitaremos importar la libreria numpy, que contiene funciones y clases que no son parte de Python basico. Recuerden que Python no es un lenguage de computo cientifico, sino de programacion de proposito general. No esta disenado para hacer algebra lineal, sin embargo, tiene librerias extensas y bien probadas que permiten lograrlo. Anaconda es una distribucion de Python que ademas de instalarlo incluye varias librerias de computo cientifico como numpy. Si instalaron Python por separado deberan tambien instalar numpy manualmente.
Antes de comenzar la tarea deberan poder correr:
End of explanation
x = [1,2,3]
y = [4,5,6]
x + y
Explanation: Lo que el codigo anterior hace es asociar al nombre np todas las herramientas de la libreria numpy. Ahora podremos llamar funciones de numpy como np.&lt;numpy_fun&gt;. El nombre np es opcional, pueden cambiarlo pero necesitaran ese nombre para acceder a las funciones de numpy como &lt;new_name&gt;.&lt;numpy_fun&gt;. Otra opcion es solo inlcuir import numpy, en cuya caso las funciones se llaman como numpy.&lt;numpy_fun&gt;. Para saber mas del sistema de modulos pueden revisar la liga https://docs.python.org/2/tutorial/modules.html
I. Creando una clase Array
Python incluye nativo el uso de listas (e.g. x = [1,2,3]). El problema es que las listas no son herramientas de computo numerico, Python ni siquiera entiende una suma de ellas. De hecho, la suma la entiende como concatenacion:
End of explanation
B = np.array([[1,2,3], [4,5,6]]) # habiendo corrido import numpy as np
Explanation: Vamos a construir una clase Array que incluye a las matrices y a los vectores. Desde el punto de vista computacional, un vector es una matriz de una columna. En clase vimos que conviene pensar a las matrices como transformacion de vectores, sin embargo, desde el punto de vista computacional, como la regla de suma y multiplicacion es similar, conviene pensarlos ambos como arrays, que es el nombre tradicional en programacion
Computacionalmente, que es un array? Tecnicamente, es una lista de listas, todas del mismo tamano, cada uno representando una fila (fila o columna es optativo, haremos filas porque asi lo hace numpy, pero yo previero columnas). Por ejemplo, la lista de listas
[[1,2,3],[4,5,6]]
Corresponde a la matriz
$$
\begin{bmatrix} 1 & 2 & 3 \ 4 & 5 & 6  \end{bmatrix}
$$
The numpy way
End of explanation
B + 2*B # Python sabe sumar y multiplicar arrays como algebra lineal
Explanation: Es posible sumar matrices y multiplicarlas por escalares
End of explanation
np.matmul(B.transpose(), B) # B^t*B
Explanation: Las matrices de numpy se pueden multiplicar con la funcion matmul dentro de numpy
End of explanation
B[1,1]
Explanation: Los arrays the numpy pueden accesarse con indices y slices
Una entrada especifica:
End of explanation
B[1,:]
Explanation: Una fila entera:
End of explanation
B[:,2]
Explanation: Una columna entera:
End of explanation
B[0:2,0:2]
Explanation: Un subbloque (notar que un slice n:m es n,n+1,...,m-1
End of explanation
B.shape
Explanation: En numpy podemos saber la dimension de un array con el campo shape de numpy
End of explanation
vec = np.array([1,2,3])
print(vec)
Explanation: Numpy es listo manejando listas simples como vectores
End of explanation
class Array:
    "Una clase minima para algebra lineal"    
    def __init__(self, list_of_rows): 
        "Constructor"
        self.data = list_of_rows
        self.shape = (len(list_of_rows), len(list_of_rows[0]))
A = Array([[1,2,3], [4,5,6]])
A.__dict__ # el campo escondido __dict__ permite acceder a las propiedades de clase de un objeto
A.data
A.shape
Explanation: Comenzando desde cero...
End of explanation
Array([[1,2,3], [4,5,6]])
print(Array([[1,2,3], [4,5,6]]))
np.array([[1,2,3], [4,5,6]])
print(np.array([[1,2,3], [4,5,6]]))
Explanation: El campo data de un Array almacena la lista de listas del array. Necesitamos implementar algunos metodos para que sea funcional como una clase de algebra lineal. 
Un metodo para imprimir una matriz de forma mas agradable 
Validador. Un metodo para validar que la lista de listas sea valida (columnas del mismo tamano y que las listas interiores sean numericas
Indexing Hacer sentido a expresiones A[i,j]
Iniciar matriz vacia de ceros Este metodos es muy util para preacolar espacio para guardar nuevas matrices
Transposicion B.transpose()
Suma A + B
Multiplicacion escalar y matricial 2 * A y A*B
Vectores (Opcional)
Con esto seria posible hacer algebra lineal
Metodos especiales de clase...
Para hacer esto es posible usar metodos especiales de clase __getitem, __setitem__, __add__, __mul__, __str__. Teoricamente es posible hacer todo sin estos metodos especiales, pero, por ejemplo, es mucho mas agradable escribir A[i,j] que A.get(i,j) o A.setitem(i,j,newval) que A[i,j] = newval.
1. Un metodo para imprimir mejor...
Necesitamos agregar un metodo de impresion. Noten que un array de numpy se imprime bonito comparado con el nuestro
End of explanation
class TestClass:
    def __init__(self):
        pass # this means do nothing in Python
    def say_hi(self):
        print("Hey, I am just a normal method saying hi!")
    def __repr__(self):
        return "I am the special class method REPRESENTING a TestClass without printing"
    def __str__(self):
        return "I am the special class method for explicitly PRINTING a TestClass object"
x = TestClass()
x.say_hi()
x
print(x)
Explanation: Por que estas diferencias? Python secretamente busca un metodo llamado __repr__ cuando un objeto es llamado sin imprimir explicitamente, y  __str__ cuando se imprime con print explicitamente. Por ejemplo:
End of explanation
class Array:
    "Una clase minima para algebra lineal"    
    def __init__(self, list_of_rows): 
        "Constructor y validador"
        # obtener dimensiones
        self.data = list_of_rows
        nrow = len(list_of_rows)
        #  ___caso vector: redimensionar correctamente
        if not isinstance(list_of_rows[0], list):
            nrow = 1
            self.data = [[x] for x in list_of_rows]
        # ahora las columnas deben estar bien aunque sea un vector
        ncol = len(self.data[0])
        self.shape = (nrow, ncol)
        # validar tamano correcto de filas
        if any([len(r) != ncol for r in self.data]):
            raise Exception("Las filas deben ser del mismo tamano")
                    
    # Ejercicio 1
    def __repr__(self):
        str2print = "Array"   
        for i in range(len(self.data)):
            if(i==0):
                str2print += str(self.data[i]) + "\n"
            if(i>0):
                str2print += "     " + str(self.data[i]) + "\n" 
        return str2print
    
    def __str__(self):
        str2print = ""   
        for i in range(len(self.data)):
            str2print += str(self.data[i]) + "\n"
        return str2print
    
    #Ejercicio2
    def __getitem__(self, idx):
        return self.data[idx[0]][idx[1]]
    
    
    def __setitem__(self, idx, valor):
        self.data[idx[0]][idx[1]] = valor
        
    # Ejercicio 3
    def zeros(x, y):
        array_de_ceros = Array([[0 for col in range(y)] for row in range(x)])
        return array_de_ceros
        
    def eye(x):
        array_eye = Array([[0 for col in range(x)] for row in range(x)])  
        for i in range(x):
            for j in range(x):
                if i == j:
                    array_eye[i,j] = 1
        return array_eye
    
    # Ejercicio 4
    def transpose(self):
        #Obtener dimensiones
        num_row = len(self.data)
        num_col = len(self.data[0])  
        #Crear matriz receptora
        mat_transpuesta = Array([[0 for col in range(num_row)] for row in range(num_col)])
        #Transponer
        for i in range(num_row):
            for j in range(num_col):
                mat_transpuesta[j,i] = self.data[i][j]         
        return mat_transpuesta
    
    def __add__(self, other):
        "Hora de sumar"
        if isinstance(other, Array):
            if self.shape != other.shape:
                raise Exception("Las dimensiones son distintas!")
            rows, cols = self.shape
            newArray = Array([[0. for c in range(cols)] for r in range(rows)])
            for r in range(rows):
                for c in range(cols):
                    newArray.data[r][c] = self.data[r][c] + other.data[r][c]
            return newArray
        elif isinstance(2, (int, float, complex)): # en caso de que el lado derecho sea solo un numero
            rows, cols = self.shape
            newArray = Array([[0. for c in range(cols)] for r in range(rows)])
            for r in range(rows):
                for c in range(cols):
                    newArray.data[r][c] = self.data[r][c] + other
            return newArray
        else:
            return NotImplemented # es un tipo de error particular usado en estos metodos
        
    #Ejercicio 5
    ##No me salió :(
    #Ejercicio 6
    def __mul__(self, other):
        if isinstance(other, Array):
            #Validar las dimensiones
            if self.shape[1] != other.shape[0]:
                raise Exception("Las matrices no son compatibles!")
            #Obtener las dimensiones
            num_rowsA = self.shape[0]
            num_rowsB = other.shape[0]
            num_colsB = other.shape[1]
            #Crear matriz receptora
            newArray = Array([[0 for col in range(num_colsB)] for row in range(num_rowsA)])
            #Multiplicar
            for i in range(num_rowsA):
                for j in range(num_colsB):
                    for k in range(num_rowsB):
                        newArray[i,j] = newArray[i,j] + self.data[i][k] * other.data[k][j]
            return newArray
        #Matriz, entero
        elif isinstance(other, (int, float, complex)):
            #Obtener las dimensiones
            rows, cols = self.shape
            #Crear matriz receptora
            newArray = Array([[0 for col in range(cols)] for row in range(rows)])
            #Multiplicar
            for row in range(rows):
                for col in range(cols):
                    newArray.data[row][col] = self.data[row][col] * other
            return newArray
        else:
            return NotImplemented
        
    def __rmul__(self, other):
        if isinstance(other, (int, float, complex)):
            rows, cols = self.shape
            newArray = Array([[0 for col in range(cols)] for row in range(rows)])
            for row in range(rows):
                for col in range(cols):
                    newArray.data[row][col] = self.data[row][col] * other
            return newArray
        else:
            return NotImplemented   
Explanation: Ejercicios
End of explanation
X = Array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]])
X
print(X)
X[0,2]
X
X[0,0] = 10
X
Array.zeros(5,5)
Array.eye(4)
X.transpose() 
B = Array.eye(5)
B
X*B
Explanation: Prueba de las clases
End of explanation
<END_TASK>