понедельник, 28 марта 2011 г.

Python: Вычисляемые аргументы функций

Введение

Для более-менее сознательного Python программиста следующая форма определения функции не секрет, и вообще довольно часто используемая:

>>> def f(a, b=4)
...     return a+b

Совершенно дурацкая функция, не приносящая практической пользы, но тем не менее удобна в данном случае. В нашей функции b - необязательный аргумент, в чем мы можем убедится сделав несколько вызовов:

>>> f(1,3)
4
>>> f(0)
4
>>> f(2)
6

Итак, тут все очевидно. Что же случится с функцией, если мы передадим аргументу значение по умолчанию как вычисляемое значение? Документация по Python нас строжайше предупреждает: значение по умолчанию вычисляется только один раз, кроме изменяемых значений (например, списков, по ссылке даже пример есть, можете взглянуть).

Давайте попробуем:

>>> from random import random
>>> def f2(d=random())
...         return d
>>> f2()
0.62437741519371093
>>> f2()
0.62437741519371093
>>>

Вы, скорее всего, получите другое значение, это ж рандом :), но смысл в том, что он действительно вычисляется один раз, что и показали 2 последовательных вызова. 

Замыкания (closures)

Немножко по другому дело обстоит с замыканиями. Казалось бы, обычная функция, просто внутри другой функции, но тут уже дело обстоит коренным образом по другому. Рассмотрим пример:

>>> def f():
...     def f1(b=random()):
...             return b
...     return f1()
... 
>>> f()
0.041199469165380531
>>> f()
0.48751939270617983

Вот незадача. Аргумент по умолчанию, вроде бы, вычисляется один раз, а на самом деле получается, что нифига не один раз. На самом деле все очень просто, даже если не понятно с первого присеста, но, чтобы все окончательно стало ясно, надо копнуть чуть глубже - а именно в то, как же выполняется код внутри, то есть байт-код данных функций. Вот как выглядит байткод самой первой функции этой статьи:

>>> from dis import dis
>>> dis(f2)

  2           0 LOAD_FAST                0 (d)
              3 RETURN_VALUE      


Итак, запихиваем в стек значение аргумента и возвращаем значение. Как видим, никакого вычисления нет при вызове функции. Теперь рассмотрим вариант с замыканием:

>>> dis(f)
  2           0 LOAD_GLOBAL              0 (random)
              3 CALL_FUNCTION            0
              6 LOAD_CONST               1 (<code object f1 at 0x7f726ccded50, file "", line 2>)
              9 MAKE_FUNCTION            1
             12 STORE_FAST               0 (f1)

  4          15 LOAD_FAST                0 (f1)
             18 CALL_FUNCTION            0
             21 RETURN_VALUE        

О, тут уже байт-код гораздо более интересен. Как видим, сначала выполняется random() (первые 2 строчки), потом загружается код (code object), и из всего вышеперечисленного создается функция, которая потом уже вызывается, чтобы получить результат и выйти из внешней функции.

Таким образом мы видим, что в первом случае MAKE_FUNCTION была вызвана ранее в коде, поэтому значение вычисляемого аргумента одинаковое. Оно вычисляется тогда, когда Python доходит в процессе исполнения до этой фунции (например, при импорте модуля). 

Но, если сдлеать вызов внутренней функции 2 раза внутри внешей, результат обоих вызовов будет одинаковым. 
>>> def f():
...     def f1(b=random()):
...             return b
...     print f1()
...     print f1()
... 
>>> f()
0.952015246599
0.952015246599
>>> 
Так как опкод MAKE_FUNCTION выполняется всего один раз, то и вычисление аргументов будет происходить 1 раз. 

Заключение

Как работает MAKE_FUNCTION и CALL_FUNCTION можно узнать из файла ceval.c, который есть частью исходников интерпретатора Python.

1 комментарий:

В этом гаджете обнаружена ошибка