본문 바로가기

Python/Study

Lazy Evaluation

여러 언어에서 함수형 프로그래밍을 지향한다.

이번 포스팅은

  1. Lazy evaluation vs Eager evaluation
  2. Pass by value vs pass by name

Lazy evaluation VS Eager Evaluation

지연 평가는 말 그대로 평가를 원하는 시점까지 미루어 두었다가 필요한 순간에 한다는 의미이다.

그림으로 보면 아래와 같다.

Eager Evaluation

전체를 한번에 다 돌고 1단계를 통과한 Source 전체가 2단계로 간다.

eagerevaluation

이미지 출처:http://filimanjaro.com/blog/2014/introducing-lazy-evaluation/

Lazy Evaluation

인자를 하나하나 돌며 1단계 2단계를 순차적으로 돌고, 2단계의 조건(개수 3개가 만족되면)이 충족되면 Source의 순회를 멈춘다.

lazyevaluation

그림으로만 보았을 때는 이 둘의 차이가 잘 와닿지 않을 것이다.

또한 오히려 지연 평가가 연산을 더 미리 하는 것이 아닌가라는 의문이 들었다.

결론부터 말하자면, 지연 평가에서 평가( = 연산)는 Take3의 source 가 활용되는 시점에 발생한다.

  • python example

파이썬에서 [] 는 list이고, ()는 generator() 객체를 반환한다.

list → Eager evaluation

generator → lazy evaluation

def test_one():
    print("make 1")
    return 1

print("[list example!]")
test_list = [test_one() for x in range(3)]

print("[print list example!]")
for test in test_list:
    print(test)

""" 출력값
[list example!]
make 1
make 1
make 1

[print list example!]
1
1
1
"""

리스트를 만들면서 test_one() 메소드를 연산한다

test_list = [1,1,1]

반면에 , Generator example을 보면

def test_one():
    print("make 1")
    return 1

print("(generator example!)")
test_list = (test_one() for x in range(3))

print("(print generator example!)")
for test in test_list:
    print(test)

""" 출력값
(generator example!)
(print generator example!)

make 1
1
make 1
1
make 1
1
"""

이런식으로 test_list가 실제로 출력되는 순간까지 함수의 실행이 지연되었다.

함수 자체의 객체가 담겨 있다고 생각된다.

test_list = (test_one(0), test_one(1), test_one(2))

그래서 yield로 값을 순회할 때까지 연산을 지연 시키는 효과가 있다.

  • Java example
final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

System.out.println(
    list.stream()
        .filter(i -> {
            System.out.println("i < 6");
            return i<6;
        })
        .filter(i -> {
            System.out.println("i%2 == 0");
            return i%2==0;
        })
        .map(i -> {
            System.out.println("i = i*10");
            return i*10;
        })
        .findFirst()
        .get()
);

/**
i < 6
i%2 == 0
i < 6
i%2 == 0
i = i*10
20

출처: https://dororongju.tistory.com/137 [웹 개발 메모장]
**/

위의 예제는 하나씩 순회하면서 , 값을 만족하면 더이상의 연산이 이뤄지지 않는다는 점에서 성능상의 이점을 보여주는 예제이다.

파이썬 처럼 객체를 리턴하는 예제를 찾다가 발견한 블로그의 코드를 첨부해 본다.

Java 8 Lambda를 이용한 lazy evaluation

import java.util.function.Supplier;

class Evaluation {
    public static boolean evaluate(final int value) {
        System.out.println("evaluating ..." + value);
        return value > 100;
    }
    public static void eagerEvaluator(final boolean input1, final boolean input2) {
        System.out.println("eagerEvaluator called...");
        System.out.println("accept?: " + (input1 && input2)); }

    public static void lazyEvaluator(final Supplier<Boolean> input1, final Supplier<Boolean> input2) {
        System.out.println("lazyEvaluator called...");
        System.out.println("accept?: " + (input1.get() && input2.get())); }

}

public class LazyEvaluation2 {
    public static void main(String[] args) {

        Evaluation.eagerEvaluator(Evaluation.evaluate(1), Evaluation.evaluate(2));
        Evaluation.lazyEvaluator(()->Evaluation.evaluate(1), ()->Evaluation.evaluate(2));
    }

}

/**
evaluating ...1
evaluating ...2
eagerEvaluator called...
accept?: false

lazyEvaluator called...
evaluating ...1
accept?: false
**/

eagerEvaluator의 경우 리턴 값을 계산하여 매개변수로 보내고,

lazyEvaluator의 경우 객체 자체를 매개 변수로 보낸다.

즉, input1.get()을 하는 순간에 evaluate함수가 실행 되는 것이다.

이점으로는 , input1.get() 이부분이 이미 거짓이므로, 뒤의 연산은 수행되지 않는다.

Pass by value vs pass by name

Parameter passing(매개 변수 생성 방법)

함수의 매개변수를 어떻게 생성하냐에 따라 lazy Evaluation과 eager Evaluation이 나뉘게 된다.

Pass by value 는 이름 그대로 값 자체를 전달하는 것이다.

위의 자바 코드에서 EagerEvaluator의 매개 변수 전달 방식이 이에 해당

연산을 모두 끝낸 후 값을 전달한다.

def test(a, b):
    if a == 1:
        return a
    else:
        return a+b


if __name__ == '__main__':
    a = 0+1
    b = 100/0
    test(a,b)
# error! ZeroDivisionError: division by zero

Pass by name은

매개 변수 자체를 이름 으로 본다는 의미이다.

def test():
    if 0+1 == 1:
        return 0+1
    else:
        return (0+1)+100/0

if __name__ == '__main__':
    a = test()
    print(a)

주어진 파라미터가 위와 같이 치환되므로 100 / 0 에 대한 에러는 고려하지 않아도 된다

추가적으로 pass by need도 있는데

이는 0+1 의 연산을 한번 수행하면 다음 줄에서 리턴하는 0+1 을 저장하고 있다가 이미 연산된 값을 리턴해 준다고 한다.

Parameter passing과 Lazy evaluation