NLP lab/파이썬

[numpy] + 연산과 += 연산의 차이점

heavyteil 2022. 2. 7. 21:09

0. 요약

import numpy as np


a = np.array([1, 2])
print(id(a))
a += 1
print(id(a))

b = np.array([1, 2])
print(id(b))
b = b + 1
print(id(b))
1906123730816
1906123730816
1906122615264
1906162448144

Process finished with exit code 0

위 코드를 실행 시 알 수 있는 것처럼, numpy 배열 연산 시

a += 1

a = a + 1

은 다르게 동작한다.

위의 코드(제자리 연산)는 변수 a의 주소를 바꾸지 않고, 아래의 코드는 a의 주소를 바꾼다.

이를 알지 못하면 함수에 매개변수로 전달 시 예상치 못한 결과를 가져올 수 있다.

1. 문제 인식

def gradient_descent(f, init_x, lr=0.01, step_num=5):
    for i in range(step_num):
        init_x -= gradient(f, init_x) * lr
        print(id(init_x))
    return init_x


init_array = np.array([-3.0, 4.0])
print(id(init_array))
print(gradient_descent(f1, init_array, lr=0.1))
print(init_array)

실행 시, 

1971976333872
1971976333872
1971976333872
1971976333872
1971976333872
1971976333872
[-0.98304  1.31072]
[-0.98304  1.31072]

Process finished with exit code 0

가 출력된다.

init_x 변수가 원래 [-3.0, 4.0] 이었고 값이 바뀌지 않을 거라 생각했는데, 기대와는 달리 값이 바뀌었다.

그래서 다음과 같이 #1 에서 #2로 코드를 바꾸었다.

#1

init_x -= gradient(f, init_x) * lr  # 1

#2

init_x = init_x - gradient(f, init_x) * lr  # 2

처음엔 결과가 같으리라고 예상했지만 다른 출력 결과를 보였다.

1887893252656
1887894083184
1887894443232
1887894083184
1887894443232
1887894083184
[-0.98304  1.31072]
[-3.  4.]

Process finished with exit code 0

#1번 코드는 init_xid값을 바꾸지 않지만 #2번 코드는 id값을 바꾼다. 따라서 함수 안에서 실행 시 마치 call by value인 것처럼 매개변수의 원래 값에 영향을 주지 않았다. 왜 그럴까?

 

2. int형 값들끼리는 문제 없는데...

a = 5
print(id(a))
a += 1
print(id(a))

b = 4
print(id(b))
b = b + 1
print(id(b))
140718013822880
140718013822912
140718013822848
140718013822880

Process finished with exit code 0

+= 연산을 하든 + 연산을 하든 변수의 id값이 바뀌는 걸 알 수 있다.

그 와중에 같은 5를 저장하는 변수는 같은 id를 가지는 걸 알 수 있다. 메모리를 절약하기 위해 최적화된 결과다.

찾아보니까 +__add__를, +=__iadd__를 호출하는데 int형 변수에 dir로 메소드를 확인해봤을 때 __iadd__가 정의되어 있지 않음을 확인했다. __iadd__가 정의되어 있지 않으면 __add__를 호출한다고 한다.

그러므로 numpy 배열 연산시 -=-가 다른 이유는 __iadd__가 오버로딩되어 있기 때문일 것이라고 생각하고 확인해보았다.

역시 __add____iadd__가 따로 정의되어 있었다.

3. 제자리 연산과 그렇지 않은 연산의 속도 차이

고성능 파이썬 2판(미샤 고렐릭, 이안 오스발트) 책에 따르면,

제자리 연산(+=)시 추가로 메모리를 할당하지 않기 때문에(id값 바뀌지 않음) 속도가 빠른 반면 그렇지 않은 연산(+)은 운영체제에 메모리를 할당하는 과정 거치므로 다른 프로세스, 즉 커널과 통신해야 하고 이는 속도를 매우 느려지게 한다.

하지만 원래 변수의 값을 보존해야 할 때는 제자리 연산이 아닌 연산을 사용해야 할 수 있다.

4. 돌아보며

파이썬의 Call by assignment에 대해 잘 알면 매우 당연한 결과인 것을 알 수 있다.

위의 코드에서 += 연산은 in-place 연산인 반면 + 연산은 새로운 메모리 주소를 할당하기 때문이다.