Después de ver a Raymond Hettinger dando una conferencia de pycon, donde demostró una mejor manera de "hacer un bucle"

blocks = []
while True:
    block = f.read(32)
    if block == '':
        break
    blocks.append(block)

Es igual a:

blocks = []
for block in iter(partial(f.read, 32), ''):
    blocks.append(block)

Hay la misma estructura en un código. Pero si los argumentos de la función dentro de iter necesitaban cambiarse, no funciona "correctamente".

def get_data_from_user(user, type, token):
    data = []
    url = f'https://api.github.com/users/{user}/{type}?access_token={token}&page='
    i = 1
    while True:
        a = get_json_from(url + str(i))
        if not a:
            break
        data.extend(a)
        i += 1
    return data

i = 1
data = []
for piece in iter(partial(get_json_from, url+str(i)), False):
    data.append(piece)
    i += 1

¿Hay alguna manera de hacerlo funcionar?

0
Elforeee 16 oct. 2018 a las 15:04

2 respuestas

La mejor respuesta

Te estás perdiendo un punto importante: iter() toma un objeto invocable estático , donde los argumentos no pueden cambiar, pero las llamadas repetidas a f.read() devuelven valores diferentes. La función iter() con dos argumentos repetidamente llamará a partial(f.read, 32) (entonces f.read(32)) hasta que el valor devuelto coincida con el valor centinela, y esto hace que la lectura de un archivo en un bucle funcione de manera eficiente.

Su función get_json_from() no hace esto. Las llamadas repetidas a get_json_from() con los mismos argumentos no alterarán el valor de retorno, porque get_json_from() no tiene ningún estado al que recurrir.

Sus argumentos no son dinámicos, pasando url + str(i) ya que el argumento tampoco tomará i del bucle, ya que partial() registra el valor solo una vez:

>>> from functools import partial
>>> i = 42
>>> p = partial(str, i + 10)
>>> p.args
(52,)
>>> i = 81
>>> p.args
(52,)
>>> p()
'52'

La expresión i + 10 no es 'live'; el resultado se calcula una vez y se pasa a partial() como 52; no importa que i esté configurado en 81 antes de llamar al objeto partial().

Puede usar un objeto invocable que vuelva a calcular los argumentos a get_json_from() cada vez que se llama; una expresión lambda haría eso (tomando url y i como cierres del ámbito primario):

for part in iter(lambda: get_json_from(url + str(i)), None):
    # ...

Esto calcula url + str(i) cada vez que se llama al objeto lambda. Supongo que get_json_from() devuelve None cuando la url no existe, y no False.

Sin embargo, en su caso, puede hacer que el código sea mucho más claro vinculando 'estado' como un valor i cambiante a un iterable, utilizando una función de generador :

def gen_data_from_user(user, type, token):
    url = f'https://api.github.com/users/{user}/{type}?access_token={token}&page='
    i = 1
    while True:
        a = get_json_from(url + str(i))
        if not a:
            break
        yield a
        i += 1

En una función generadora, el código se detiene hasta que comience a iterar sobre el objeto que devuelve la llamada a la función. Cuando itera, el código se ejecuta hasta que se encuentra la siguiente expresión yield, en cuyo punto el código de función se detiene nuevamente y se le da el valor de la expresión.

Entonces, en lo anterior, hacer un bucle sobre gen_data_from_user(....) le dará cada a en el bucle while True:. La función mantiene el estado entre las variables locales, por lo que i se mantiene (así como url) para usar la próxima vez que el código no esté en pausa.

Entonces puedes usar:

for piece in gen_data_from_user(...):
    # ...

No se necesita iter(), y lo anterior es mucho más limpio que una definición iter(lambda: ..., None).

1
Martijn Pieters 16 oct. 2018 a las 12:52

Puede usar lambda en lugar de partial para permitir la reevaluación de las variables dentro cada vez que se llama:

for piece in iter(lambda: get_json_from(url+str(i)), False):
1
blhsing 16 oct. 2018 a las 12:45