Вступ
Для більш-менш притомного 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
>>>
Ви, швидше за все, отримаєте інше значення це ж random, але сенс у тому, що він справді обчислюється один раз, що і показали 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 доходить в процесі виконання до цієї функції (наприклад, при імпорті модуля).
Але, якщо виконати виклик внутрішньої функції двічі всередині зовнішньої, результат обох викликів буде однаковим.
>>> 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.
Коментарі
Дописати коментар