파이썬에서 배열의 원소의 곱을 구하는 3가지 방법

파이썬에서 배열의 원소의 곱을 구하는 3가지 방법

2018, Oct 03    

들어가며

대부분의 프로그래밍 언어에서 ‘배열’을 내장하고 있다. 확실히 프로그래밍 입문 책을 보면 가장 먼저 접하는 자료구조 중 하나가 배열로, C나 자바 등은 Array로, 파이썬에서는 list라는 배열 자료구조를 가지고 있다.

논의를 원소를 실수로만 갖는 배열로 한정하고 생각해보자. 파이썬에는 배열의 합을 구하는 sum이라는 함수를 기본으로 가지고 있다. 사용법은 간단해서 따로 key 인자를 주지 않으면 기본적으로 배열 안의 모든 값을 더해서 반환한다.

arr = [1, 2, 3, 4, 5]
print(sum(arr))

>>> 15

더없이 단순한 코드로 파이썬을 조금이라도 배워본 사람들은 이해하는 코드다.

그런데, 내가 문제삼고 싶은 것은 파이썬에 배열의 합을 구하는 함수는 있는데, 배열의 곱을 구하는 내장함수는 없다는 것이다.
가령 저 위의 ‘arr’에서 배열 안의 모든 원소의 곱을 구하면 ‘120’이라는 값이 나올 것이다. 그런데 원소의 곱을 구하는 함수는 파이썬에 내장되어 있지 않을 뿐더러, 수학 연산을 지원하는 math 모듈에도 포함되어 있지 않다. 이 모듈에는 여러 복잡한 삼각함수 기능과 ‘factorial’ 함수도 있다는 것을 감안할 때 ‘곱 기능 또한 여기에 있을 법하지 않나?’하는 질문을 남긴다.

하지만 결국은 없고, 그러면 우리가 한번 만들어보자. 원소의 곱을 구하는 기능을 만드는 것은 그리 어려운 것은 아니다. 하지만 하나의 문제를 푸는 방법이 한 가지인 경우는 별로 없고, 기본적인 방식과 파이썬에서 제공하는 다른 기능들을 활용해서도 다양하게 구현해볼 수 있다.

그래서 이 포스트에서는 파이썬에서 배열의 곱을 구하는 기능을 3가지 방법으로 해보도록 하자.
이 포스트에서 소개할 방법은 1. 기본적인 구현, 2. reduce를 통한 구현, 3. 메타프로그래밍을 통한 구현 이렇게 3가지이다. 방법이 가면 갈수록 다른 언어에서도 구현 가능한 보편적인 방법에서 점차 Pythonic한 구현으로 넘어간다.

배열 안의 원소는 모두 실수라고 가정한다.

1. 기본적인 구현

먼저 다른 언어들에서도 흔히 구현할 수 있는 방법으로 구현해보자.
사실 많은 설명도 필요 없는데 기본값 1을 설정하고 반복문을 돌며 각 원소를 이 값에 곱해 나가면 된다.
코드는 다음과 같다.

arr = [1, 2, 3, 4, 5]

def multiply(arr):
    ans = 1
    for n in arr:
	if n == 0:
            return 0
        ans *= n
   return ans

multiply(arr)

>>> 120

곱을 구하는 일반적인 코드다. 반복문과 조건문은 어느 언어에나 있기 때문에 이 기본적인 statement를 사용해 값을 구한다. 다만 배열 안에 0이 있을 때는 언제나 결과가 0이기 때문에 그것을 검사하는 코드를 선택적으로 추가했다.

이번에는 좀 덜 보편적인, 그러니까 보다 Pythonic한 방법으로 풀어보도록 하자.

2. reduce를 통한 방법

함수형 프로그래밍(functional programming)에는 fold라는 개념이 있다. 위키피디아에 있는 정의를 번역하면 다음과 같다.

함수형 프로그래밍에서, fold(reduce, accumulate, aggregate, compress, inject라고도 함)는 재귀적인 자료구조를 분석하고 주어진 결합 동작을 사용해서 원 자료구조의 부분구조를 반복적으로 처리해 재결합해서 하나의 결과값으로 반환하는 고순위(higher-order) 함수 집합을 일컫는 용어이다.

  • By Wikipedi

음… 이게 무슨 말인가 싶다. 내가 한 문장으로 정의하면 ‘fold(reduce)는 주어진 순회가능한 자료구조에 원소 각각에 차례로 연산을 해나가면서 최종적으로 단 하나의 반환값만을 내뱉는 기능이나 용어를 말한다.’

말로 하면 어려운데, 이를 그림으로 보면 좀더 이해가 쉽다.

fold

fold연산을 그림으로 표현한 위키피디아 자료이다. 지금 난 이 방법으로 우리의 예제 arr 배열의 곱을 구하려고 한다. arr은 ‘1, 2, 3, 4, 5’를 순서대로 원소로 가지고 있다.

그림의 오른쪽 부분을 해석하면 fold(reduce)는 z라는 초기값에, arr의 원소를 하나씩 주어진 함수 ‘f‘로 처리한 뒤 최종 결과물을 반환하는 것이 된다.

아까 곱을 구하는 첫 번째 방법과 일맥상통하지 않은가? 코드를 다시 살펴보자.


arr = [1, 2, 3, 4, 5]

def multiply(arr):
    ans = 1
    for n in arr:
	if n == 0:
            return 0
        ans *= n
   return ans


이 함수는 1(z)라는 초기값에 arr의 원소를 하나씩 주어진 함수(여기서는 곱하기)로 처리한 뒤 최종 결과물 ans로 반환한다.

이와 같이 재귀적 자료구조에 위와 같은 처리를 해 하나의 단일 값으로 반환하는 것을 fold라고 하며, 파이썬에서는 이 기능을 functools모듈의 reduce라는 함수로 지원하고 있다.

바로 이 방법을 통해 곱을 구해보자. functools 모듈의 reducehelp 함수를 적용해보면 다음과 같다.

from functools import reduce

print(help(reduce))

-----

Help on built-in function reduce in module _functools:

reduce(...)
    reduce(function, sequence[, initial]) -> value
    
    Apply a function of two arguments cumulatively to the items of a sequence,
    from left to right, so as to reduce the sequence to a single value.
    For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
    ((((1+2)+3)+4)+5).  If initial is present, it is placed before the items
    of the sequence in the calculation, and serves as a default when the
    sequence is empty.


언제나 헬핑(helping)을 생활화해야 한다.
이 함수는 함수sequence를 인자로 받고, 추가적으로 선택적으로 initial 값을 받을 수 있으며 value를 반환한다.
일단 sequencelist라고 생각해도 된다. 이에 대한 것도 이후에 다루도록 하겠다.


reduce는 리스트의 두 아이템(원소)에 함수를 왼쪽에서 오른쪽으로 누적적으로 적용해서 하나의 단일 값으로 줄인다.(reduce)


그리고 우리의 예제에서 단일 값을 반환하는 계산 순서는 ((((1*2)*3)*4)*5)가 된다.
원할 경우 initial 값을 넣어 계산 순서를 (((((initial*1)*2)*3)*4)*5)과 같이 만들 수도 있다.

이를 파이썬 코드로 구현하면 다음과 같다.

from functools import reduce

arr = [1, 2, 3, 4, 5]

def multiply(arr):
    return reduce(lambda x, y: x * y, arr)


multiply(arr)

>>> 120

음, 여기서 처음 보는 lambda라는 용어를 만났다. lambda는 파이썬에서 def와 함께 함수를 정의하는 예약어로서, 람다는 보통 제한적이고 짧은 함수를 작성하는 데 쓰인다. lambda 예약어를 통해 함수를 작성하는 예는 다음과 같다.

my_sum_lambda = lambda x, y: x + y

print(my_sum_lambda(1, 2))


>>> 3

lambda의 사용법은 다음과 같다. 먼저 : 전에 함수에서 쓰일 입력을 적는다. 위의 예에서는 x, y가 인자가 되고 이 함수는 실행 시 인자를 2개 받는다.

그리고 : 후에 함수의 리턴값을 적는다. lambda 함수는 따로 return statement를 적을 필요가 없다.

따라서 위의 my_sum_lambda 함수는 인자 두 개를 받아 그 합을 반환하는 함수다.
이 함수는 def를 통해서도 만들 수 있다.

def my_sum_def(x, y):
    return x + y

위 두 함수가 하는 기능은 완벽히 똑같다.


그렇다면 왜 reduce의 인자로 def가 아닌 lambda를 굳이 썼을까?
lambda 함수는 파이썬에서 함수를 쓰는 일상적인 형태가 아닌데 말이다.

그 이유는 편리성에 있다. reduce를 쓰는 코드를 다시 보자.

from functools import reduce

arr = [1, 2, 3, 4, 5]

def multiply(arr):
    return reduce(lambda x, y: x * y, arr)


multiply(arr)

>>> 120

먼저, lambda를 쓰면 단 한 줄로 함수를 정의할 수 있다.

def를 쓰면 저 lambda 자리에 들어갈 용도로 한 줄로 만들 수 없다. 최소 두 줄이 필요하다. 그런데 보통 reduce 등의 첫 인자 함수로는 복잡한 함수보다는 간단한 함수를 사용하기 때문에 lambda로도 충분하다.


또 결정적으로, lambda파이썬에서 익명 함수(anonymous function)로 많이 쓰인다.

익명 함수는 이름이 없는 함수로, 함수에 이름을 지정할 필요가 없을 때 쓸 수 있는데 파이썬에서 익명함수를 정의하는 예약어는 lambda이다. ‘이름’을 지정하지 않아도 된다는 것이 무슨 뜻인가.


우리는 함수를 왜 쓸까? 함수 사용의 장점은 여러 가지가 있지만 그중에는 코드의 재사용성을 높인다는 장점이 있다.
우리는 코드 조각을 ‘이름’을 가진 함수로 정의해서 그 이름을 통해 코드를 다시 쓴다.
즉, 함수를 호출하기 위해 함수에는 이름이 필요하다.

하지만 이름이 필요하지 않은 경우도 있다. 우리의 multiply 함수를 보자. reduce의 첫 인자는 함수를 받는데,
저 함수를 재사용할 일이 있을까?

lambda 함수는 단순하게 두 수의 곱을 반환하는 함수고, 저런 기능은 함수로 만들 필요도 없다.
단지 reduce가 첫 인자로 함수를 받기 때문에 만들었고, 저 함수의 재사용의 가능성은 없다.

이와 같이 1회용으로 간단하게 함수를 인자로 넣어야 할 때 lambda 함수를 많이 쓴다.

익명함수의 활용은 함수형 프로그래밍의 대표적인 기능인 map, filter 등에도 똑같이 적용가능하다.
reduce는 그중 하나인 것이다.


멀리 돌아왔는데 결국 저 reduce식은 두 수의 곱을 반환하는 lambda 함수와, 원소를 차례로 적용할 배열을 받아서 모두 곱해 단일값을 반환한다.

3. eval과 str.join을 활용한 방법

이 방법은 진정 Pythonic한 방법이다. 다른 언어에서 이렇게 풀 수 있을지 모르겠다.
이 방법은 내장된 eval 함수와 str 클래스의 join 메소드를 활용하는 방법이다.

이 둘에 대해 각각 설명하면 다음과 같다.

eval

메타프로그래밍(Metaprogramming)이란 개념이 있다. 위키피디아에서는 이 개념을 ‘컴퓨터 프로그램이 다른 프로그램을 자신의 데이터처럼 취급하는 능력이 있게 만드는 프로그래밍 기법’이라고 설명하고 있다.
말이 어려운데 내가 이해하는 대로 설명하면 다음과 같다.

우리가 일상적으로 코딩하는 환경에서는 내가 최종적으로 프로그램 코드를 전부 작성하는데, 메타프로그래밍은 이와 달리 코드 작성의 일부를 컴퓨터에 위임하는 것을 말한다.

파이썬에서는 메타프로그래밍을 지원하는 내장함수를 evalexec으로 지원하는데 우리 예제에서는 eval로 충분하다.

eval은 ‘evaluate’, 즉 ‘평가한다는 뜻으로 입력으로 받은 ‘expression’을 해석해 단일값으로 반환한다.

이 함수를 간단하게 사용해보자.

# 1.
eval('1+2+3')

# 2.
name = "Parkito"
sentence = eval(name + ', what is your hobby?')

# 3.
eval('1 * 2 * 3 * 4 * 5')   !!!


>>> 6
>>> 'Parkito, what is your hobby?'
>>> 120   !!!

eval은 문자열 형태의 ‘expression’을 받아 값을 평가해서(환원해서) 반환한다.
1, 2, 3번 중에서 3번이 우리의 문제를 해결하는 핵심이다.

우리의 예제에서 arr에는 1부터 5까지의 정수가 들어있다. 그들의 곱을 구하는 가장 무식한 방법은 모든 원소를 다 곱하는 식을 일렬로 쓰는 방법, 즉 ‘1 * 2 * 3 * 4 * 5’와 같은 방법이 있을 것이다.

저 문자열은 평가(evaluate) 가능한 수식으로, 값은 120이다. 저렇게 원소 사이를 ‘*‘로 채운 뒤, 값을 평가하면 배열 내 원소의 곱을 구할 수 있을 것이다. 이렇게 평가하는 도구가 eval인 것이다.

그러면 다음으로 배열을 받아서 그 원소 사이에 ‘*‘를 끼워 문자열로 출력하는 기능만 있으면 되는데,
여기에 str.join 메소드를 사용하면 된다.

str.join

내장 str 클래스의 join 메소드는 인자로 받은 리스트 사이에 자신을 뀌워넣어 하나의 문자열로 반환하는 함수이다.
이건 보면 바로 이해가 가능하니 바로 예제를 살펴보자.

# 1.
' / '.join(['a', 'b', 'c', 'd', 'e'])

# 2.
''.join(['가', '나', '다', '라', '마'])

# 3. 
'*'.join(['1', '2', '3', '4', '5'])   !!!


>>> 'a / b / c / d / e'
>>> '가나다라마'
>>> '1 * 2 * 3 * 4 * 5'  !!!

보는 바와 같이 str.join은 받은 리스트 사이에 자신을 끼워넣어 문자열을 만들어 반환한다.
3번과 같이 만들어서 eval에 넣으면 될 것 같은 느낌적인 느낌을 받았다면 성공이다.

바로 구현하자!

구현

def multiply(arr):
    return eval('*'.join([str(n) for n in arr]))

arr = [1, 2, 3, 4, 5]

multiply(arr)

>>> 120

길이는 reduce를 쓴 것과 같이 짧다. 다만 join 안에 리스트 컴프리헨션이 들어가 있는데,

원래 배열이 아닌 리스트 컴프리헨션을 쓴 이유는 join 안에 넣는 배열의 원소는 모두 str이어야 한다는 조건 때문이다.
하지만 입력으로 받는 배열에는 숫자가 보통 정수형이나 실수형일 것이기 때문에 이를 문자화해줘야 한다.
이 점만 이해하면 이 코드도 이해할 수 있다.

이렇게, 숫자 배열의 곱을 구하는 방법을 3가지 알아보았다.

마치며

숫자 배열의 원소의 곱을 구하는 방법을 일반적인 구현, 함수형 프로그래밍 기법을 통한 구현, 메타프로그래밍을 통한 구현을 통해 살펴보았다.

이 중에서, 실제로 만들어 써야 한다면 난 2번을 택하겠다. 1번은 재미가 없고, 3번은 다른 개발자들이 익숙하지 않아 가독성이 떨어질 수 있기 때문이다. 하지만 reduce 같은 경우는 대중적인 자바나 자바스크립트 등에도 구현되어 있어 3번보다는 더 대중적인 방법이다. 코드 자체도 깔끔하기 때문에 이 방법을 추천한다.

하지만 꼭 2번이 정답일 필요는 없다. 자신이 마음에 드는 구현을 최적화하는 재미가 또 있을테니까.

다른 방법이 더 있지 않을까 하는데 일단 여기서 넘어가자.