Hace unas semanas tuve una entrevista de trabajo para una posición como desarrollador Python en la empresa x, esta empresa necesitaba alguien que supiera de algoritmos, optimizacion y un conocimiento aceptable de Python. La segunda entrevista fue técnica sobre Python en la cual falle por que no esperaba tales preguntas, llevo aproximadamente unos 3 años usando Python para proyectos pequeños pero nunca lo aprendí formalmente, simplemente lo empece a usar como cualquier otro lenguaje y aprendí lo que iba necesitando.

La primera pregunta fue sobre los objetos mutables e inmutables en Python.

En Python TODO es un objeto, pero unos son mutables y otros no, la diferencia entre uno y el otro es simplemente que uno puede ser alterado (mutable) y el otro no (inmutable) después de ser creados.

Objetos mutables:

  • list
  • dict
  • set
  • byte array

Objetos inmutables:

  • Numeric types: int, float, complex
  • string
  • tuple
  • frozen set
  • bytes

El problema referente a la mutabilidad y la manera en que los argumentos y las funciones trabajan en python fue siguiente:

def mutable_add_element(element, elements=[]):
    elements.append(element)
    return elements

mutable_add_element('a')
mutable_add_element('b')
mutable_add_element('c')

La pregunta fue cual seria el output de cada uno de esas llamadas a la función. Como pueden ver la función toma un elemento y una lista de elementos, si solo pasas como argumento el elemento y no la lista, setea elements (la lista) como una lista vacía. Nosotros solo estamos pasando un elemento sin la lista por lo tanto el output esperado seria el siguiente:

[‘a’]

[‘b’]

[‘c’]

Pero en Python las funciones son evaluadas en la definición de las mismas y no a la hora de ser llamadas y como las listas son elementos mutables el output es el siguiente:

[‘a’]

[‘a’, ‘b’]

[‘a’, ‘b’, ‘c’]

La manera de resolver esto seria seteando la lista como None y en caso de no a ver una lista setear elements a una lista vacía:

def add_element(element, elements=None):
    if not elements:
        elements = []
    elements.append(element)
    return elements

Otro ejemplo de mutabilidad que encontré en un post cuando buscaba sobre el tema fue sobre strings:

def slow_string_concat(container):
    string_build = ""
    for data in container:
        string_build += str(data)
    return string_build

Esta funcion concatena una lista de elementos en un string, pero como string es inmutable lo que hace es tomar el valor actual de el string y el valor a concatenar, los concatena y crea otro objeto con el resultado y asi sucesivamente con todos los elementos de la lista a concatenar, por lo tanto es muy ineficiente, una mejor manera de resolver esto seria:

def fast_string_concat(container):
    return "".join([str(data) for data in container])

De esta manera aprovechas la mutabilidad de las listas y al final creas un solo elemento string.

La segunda pregunta fue sobre division:

Si, divisiones, simples divisiones, el problema fue el siguiente, cual seria el output de lo siguiente en python 2 y en python 3:

5/2
5.0/2
5//2
5.0//2.0

Ya tenia noción de divisions en python, pero me confundió un poco con lo de en python 2 y en python 3. El output (y una explicación de cada output) es el siguiente:

Python 2:

5/2 = 2 #Por que si los valores a evaluar son enteros, el resultado es un entero.
5.0/2 = 2.5 #Por que si uno de los valores o ambos es float el resultado es float.
5//2 = 2 #Por que si pones doble slash “//” el resultado es entero sea o no sea uno de los elementos flotantes.
5.0//2.0 = 2.0 #Lo mismo que la anterior.

En Python 3 las divisiones son evaluadas de manera diferente:

Python 3:

5/2 = 2.5 #En python 3, hace lo que tiene que hacer una division completa.
5.0/2 = 2.5 #Como ven no importa si uno o ambos elementos son enteros o flotantes, siempre da el resultado real de la division.
5//2 = 2 #El comportamiento  del doble slash “//” sigue siendo el mismo.
5.0//2.0 = 2.0 #Lo mismo que la anterior.

La tercera fue sobre la creación y manipulación de una lista:

list = [[]] * 5 #Cual seria el output
list[0].append(10) #Cual seria el output

El output de la primera sentencia seria una lista con 5 listas a dentro:

[[], [], [], [], []]

Y el output de la siguiente sentencia serian las misma 5 listas pero con el entero 10, dentro de cada una de ellas:

[[10], [10], [10], [10], [10]]

Por que pasa esto? Por que lo que hace no es crear 5 listas diferentes lo que hace es crear 5 referencias a la misma lista.

La 4ta fueron una seria de pregunta sobre slices en python:

En python puedes hacer uso de slices de diferentes manera pero que pasa cuando haces esto:

list = [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]
print list[10:]

Tienes una lista de 5 elementos con indices de 0 a 4, pero tratas de imprimir del indice 10 en adelante, en teoría te debería de dar un error de que el indice indicado esta fuera del rango o algo por el estilo, o al menos esa fue mi respuesta. Lo que pasa realmente es que te retorna una lista en blanco, sin ningún error.

La quinta era relacionada con el Method Resolution Order de Python

class A(object):
   x = 1

class B(A):
   pass

class C(A):
   pass

Aquí tenemos 3 clases A, B y C, donde B y C heredan de A que tiene x inicializado a 1.

Al imprimir x de A, B y C todos imprimen 1

print A.x, B.x, C.x
1 1 1

Aquí cambiamos el valor de x en B a 2:

B.x = 2
print A.x, B.x, C.x
1 2 1

Y aqui por ultimo cambiamos el valor de x en A a 3 y el resultado es el siguiente:

A.x = 3
print A.x, B.x, C.x
3 2 3

Uno espera que el resultado sea 3, 2 y 1, pero es 3, 2, 3. Por que pasa esto? La razón es que las variables de las clases son tratadas como elementos de un diccionario, por lo cual si no encuentra x en C (por que nunca fue inicializado) busca x en su padre que es A y el cual fue cambiado a 3.

La ultima pregunta fue sobre closures y como se usan en Python:

Un closure es una función anidada que tiene acceso a las variables de el entorno en que se declaro y que recuerda estas variables aun cuando este entorno ya no este en memoria.

Esto es un closure:

def make_printer(msg):
    def printer():
        print msg
    return printer

Esto no es un closure:

def make_printer(msg):
    def printer(msg=msg):
        print msg
    return printer

La diferencia es que en el closure msg esta siendo agarrado de su escope y el que no es scope esta asignando a otra variable llamada igual.

Esto tampoco es un closure:

def make_printer(msg):
    def printer():
        print msg
    return printer()

Esta es solo una función anidada.

En concreto el problema relacionado con closures y late binding fue el siguiente:

def late_create_multipliers():
    return [lambda x : i * x for i in range(4)]

for multiplier in late_create_multipliers():
    print multiplier(2)

Uno esperaría que el output fuera 0 2 4 6, pero en realidad es 6 6 6 6. Por que pasa esto? por que los closures en python son late binding, lo que significa que busca por los valores de las variables a la hora de ser llamadas no al ser inicializadas, por lo tanto i vale 3 al final (range va de 0 a 3) entonces a la hora de ser llamada y multiplicar 2 por i, siempre multiplica 2 por 3 en lugar de 2 por cero, uno, dos y tres.

Nota: Al final me di cuenta que todas las preguntas estan basadas en este post de toptal

It's only fair to share...Share on Facebook85Tweet about this on TwitterShare on Google+0Share on LinkedIn0Share on Reddit0Pin on Pinterest0Email this to someone

One thought on “Cosas raras de Python

  1. Gabriel says:

    muy informativo gracias

Leave a Reply

Your email address will not be published. Required fields are marked *

Comment moderation is enabled. Your comment may take some time to appear.