Python es un lenguaje de programación interpretado, multiplafatorma, con tipos implícitos y checkeos en tiempo de ejecución (lo que se conoce comúnmente como tipado dinámico o débilmente tipado).
Es de código abierto y bajo licencia GNU GPL.
Expliquemos algunas características mencionadas:
No vamos a entrar demasiado en detalla aquí. Pero se refiere a que nuestro código fuente escrito en Python no sufre de un proceso de compilación directa a código binario (ejecutable por la máquina), previo a su ejecución.
Si no que existe un programa llamado interprete que es quien se encarga de traducir nuestro programa Python a lenguaje máquina. Pudiendo pasar por un lenguaje intermedio, como el bytecode de Java, o en .NET.
Pero además, pudiendo ser traducido just-in-time es decir, en el momento en que se necesita ejecutar, y no antes.
Esto provee flexibilidad al lenguaje, ya que no hace falta, por ejemplo determinar de antemano los tipos de las variables.
Por otro lado introduce un overhead o costo adicional, dado por el intérprete.
Ya vamos a ver en código el impacto que tiene esto a nivel de flexibilidad.
De esto vamos a hablar aún menos :)
Se refiere a que comparte características o mecanismos comúnmente relacionados a diferentes paradigmas de programación:
Por ahora (para esta materia) nosotros nos vamos a centrar en el primer paradigma, tal vez chusmeando un poquito algunas cosas del paradigma de objetos.
Como consecuencia de ser interpretado, es posible evitar declarar los tipos de las variables. Luego el ambiente hará los checkeos de tipos en tiempo de ejecución.
Pasemos a entender el lenguaje mediante código y ejemplos, el famoso hola mundo en Python sería:
print "Hola Progr2"
Donde print es el nombre de una función, y le estamos pasando un string. Ya vemos que entonces existe el concepto de función.
Cambiemos el código a:
aQuien = "Progr2"
print 'Hola ' + aQuien
Esto declara una variable con nombre aQuien que referencia al valor (string) "Progr2". Luego el print recibe como parámetro el string resultado de concatenar el string "Hola" con el valor de la variable aQuien, es decir con "Progr2".
El resultado es el mismo que en el ejemplo anterior:
Hola Progr2
Interesante notar que:
Acá ya nos vamos a detener porque es importante conocer la diferencia con respecto a lo que veníamos viendo en C.
Cuando en C declarábamos una variable, eso automáticamente reservaba memoria para el tipo de la variable, incluso sin contenido.
int main() {
int edad;
edad = 30;
}
La primer linea ya reservaba los 4 bytes para guardar un int. La segunda linea alteraba el contenido con el número 30.
En Python (y en otros lenguajes como Java, C# etc), no es la declaración de la variable la que reserva memoria. Una variable, desde el punto de vista del programador, no ocupa memoria. Lo que ocupa memoria es el objeto al que apunta. En nuestro caso el entero 30.
edad = None
edad = 30
Acá vemos un código similar al de C. La primer linea es forzada, ya que no se puede declarar una variable sin que referencia a algo (acá referencia a None, que sería como que referencia a nada, o a null en otros lenguajes).
No es exáctamente así que funciona (por optimizaciones, etc), pero a fín de simplificar, imaginemos que al ejecutarse la segunda linea, se crea el objeto "30" y se guarda en algún lugar en memoria. Transparente para nosotros. Luego, la variable edad es símplemente una referencia a ese objeto. Similar, aunque mucho más simple, que los punteros en C.
Si luego hacemos:
edad = 31
Pasará que la variable ahora referencia a un nuevo objeto entero alocado en memoria. El viejo objeto sigue existiendo en memoria, solo que nadie lo apunta ahora. Con lo cual el garbage collector va a ir tras el en algún momento.
Esto ocurre de la misma forma para todas las variables y parámetros.
Y no existen variables de tipo puntero como en C. Como así tampoco se puede saber la dirección de memoria de un dato.
De hecho, en esta sección hablamos bastante de memoria solo para explicar la diferencia con C, solo por el hecho de que ustedes vienen de programar en C, y hay que romper con esa idea de la memoria.
En general de aquí en más, para programar en python u otro lenguaje como Java, Smalltalk, C#, etc, no hará más falta pensar en términos de memoria (de reservar y liberar memoria) o punteros.
Podemos definir funciones con la palabra reservada def
def imprimirPares(unString):
print unString[::2]
Acá definimos una función llamada imprimirPares que recibe un objeto de tipo string (en principio es nuestra intención). Y el cuerpo de la función imprime los caracteres en posiciones pares (comenzando con indice 0) del string dado por parámetro.
Lo usamos así:
abecedario = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
imprimirPares(abecedario)
>>>> ACEGIKMOQSUWY
Observar que:
Flexibilidad del tipado dinámico
Deciamos que los strings soportan los mismos operadores que las listas y tuplas, entonces.. qué pasa si llamamos a nuestra función con una lista como parámetro ?
lista = [0,1,2,3,4,5,6,7,8,9,10]
imprimirPares(lista)
>>> [0, 2, 4, 6, 8, 10]
Funciona perféctamente !
Ver que las listas se definen con el literal que se forma por corchetes y con elementos separados por coma.
Pasaje de parámetros
Dijimos que en C el pasaje de parámetros siempre se hacía por valor, es decir que se realiza una "copia" del valor de la variable a una nueva variable que estará disponible para la función, y vivirá solo el tiempo en que dure la ejecución de esa función (se guarda en la pila).
Ahora si estábamos trabajando con un objeto compuesto, por ejemplo un Producto con "codigo", "descripcion", "precio", etc, y queríamos que la función altere uno de estos miembros, necesitábamos pasar este parámetro por referencia, y eso en C se podía emular, usano punteros. Es decir que pasamos la dirección de la variable.
Veamos como es en Python. Primero con un ejemplo simple, utilizando un valor (value objects)
def funcionQueIntentaModificarUnParametro(numero):
numero = 3
print "Numero dentro de funcion:", numero
Esta función recibe un parámetro. Dentro, reasigna la variable y la imprime.
Ahora veamos como se utiliza:
unNumero = 5
print "Numero fuera de funcion (antes):", unNumero
funcionQueIntentaModificarUnParametro(unNumero)
print "Numero fuera de funcion (despues):", unNumero
Y esto mostrará lo siguiente:
Numero fuera de funcion (antes): 5
Numero dentro de funcion: 3
Numero fuera de funcion (despues): 5
Porque en python (así como en otros lenguajes como Java, smalltalk etc), las variables son referencias a objetos o valores. No existe el concepto de una dirección de memoria que se altera. Las variables son las que se re-asignan apuntando a otro objeto/valor. Pero si teníamos otra variable referenciando al valor original, no se ve alterada.
Veamos, como seria. Al realizar el llamado a la función, y antes de la re-asignación:
Luego, cuando reasignamos:
Ahí vemos, por qué no se afecta a la variable "unNumero".
En este caso, para valores se puede pensar que el pasaje es por valor.
Lo importante a entender es que las variables son distintas. El parámetro es una variable de la función, y no es la misma variable "exterior".
Ahora, qué sucede con un objeto compuesto ?
Por ejemplo, una lista. Tenemos una función que recibe como parámetros, una lista y un numero. Y lo que hace es mutiplicar cada elemento de la lista por el numero dado. Actualizando la lista en cada posición con los nuevos valores.
def multiplicarElementos(lista, numero):
for indice, elemento in enumerate(lista):
lista[indice] = elemento * numero
Acá vemos de paso como recorrer una lista y a su vez tener acceso al índice. Para eso usamos la función enumerate que dado un objeto iterable como una lista, va proveyendo pares que contienen un contador (indice) y el elemento original.
Luego estamos reasignando la posición al resultado de la multiplicación.
Así utilizamos esta función:
lista = [2, 10, -3]
print "Antes:", lista
multiplicarElementos(lista, 2)
print "Despues:", lista
E imprime lo siguiente:
Antes: [2, 10, -3]
Despues: [4, 20, -6]
Como vemos la lista fue modificada.
Esto es, de nuevo, por el hecho de que las variables son referencias. Podemos considerar entonces que para objetos (a diferencia de para valores) el pasaje es por referencia.
A diferencia de C, no hace falta utilizar ningun operador especial como el "&", ni explícitamente pasar la dirección de memoria, ni nada por el estilo.
Para esto hay que entender que las listas no son "arrays", en cuanto a que no tienen slots de memoria para cada elemento. Si no, que lo que tienen son referencias a objetos.
Veamoslo en diagramas. Al entrar a la función:
Luego la función va multiplicando, con lo cual se generan nuevos objetos resultado de la mutliplicación, y se actualiza, en este caso la primer posición de la lista al nuevo objeto.
Eventualmente se cambian todas las referencias. Fíjense que los valores/objetos originales, pasan a estar "sueltos", es decir, siguen en memoria (quizás, no lo sabemos a ciencia cierta), solo que no hay ninguna variable que los referencie. Eventualmente el Garbage Collector se va a encargar de encontrarlos y liberar la memoria. Realmente no nos importa demasiado, ya que es parte de la ventaja de la gestión automática de memoria :)
Retorno de funciones (scope de variables y no de objetos)
Habíamos visto que en C el valor de retorno de una función se devolvía, valga la redundancia, por copia del valor. Y por otro lado, vimos que era peligroso, e incorrecto devolver desde una función un puntero a un objeto (struct) "local" que declaramos en la función.
Para esos casos lo que hacíamos era, desde la función construir la estructura en el heap, a través de malloc. Y así sí, podíamos devolver el puntero, ya que se en realidad iba a copiar la dirección de memoria, que no se iba a destruir, por estar en el heap (solo se destruye, al llamar explícitamente a la función free).
Bueno, todo esto último es transparente en Python.
El retorno de una función siempre se hace por referencia. Referencia al objeto. Y si el objeto lo creamos en el contexto de la función, no importa, porque en realidad se alocó en el heap. Lo que vive en la pila son las variables, pero no sus valores.
Veamos un ejemplo. Tenemos una función repetir, que dado un elemento y un número de veces, devuelve una lista que contendrá el elemento repetido tantas veces como se indicó.
def repetir(elemento, n):
repetidos = []
for _ in range(n):
repetidos.append(elemento)
return repetidos
Como vemos, en la primer linea, estamos declarando una variable que apunta a una nueva lista vacía.
Luego hacemos un for de 0 a N, y vamos agregando el elemento a la lista.
Finalmente retornamos la lista.
Luego usamos esa función así:
muchosSaludos = repetir("Hola", 5)
print muchosSaludos
Hay algún problema con que usemos la lista. que fue creada dentro de la función, aquí afuera de ella, y luego de que retornó ?
No, no lo hay.
Porque la variable "repetidos" dejó de existir, pero no así la lista.
Acá se ve en un diagrama que esquematiza lo que sucede al retornar de la función:
Eventualmente cuando dejemos de apuntar a esta lista, el GarbageCollector liberará la memoria.