본문 바로가기

Python

[Python] 5. 함수 (2) - 함수 심화 기능(재귀, 매개변수, 람다)

반응형

 

[목차]

 

1. 재귀 호출

 

2. 다양한 형태의 매개변수

 

3. 람다 표현식

 

 

  1. 재귀 호출

 

재귀 호출(recursive call)이란, 함수 안에서 함수 자신을 호출하는 방식을 말한다. 주로 알고리즘을 구현할 때 많이 이용된다. 

def func():
    print(1)
    func()

func()

 

위의 코드는 1이 무한히 출력되는 방식이다. 

파이썬에서는 재귀의 깊이(recursion depth)가 최대 1000으로 정해져 있어서, 1000번을 초과해서 재귀 호출되면 에러가 발생한다. ("RecursionError: maximum recursion depth exceeded while calling a Python object")

 

함수를 A라고 하고 함수 내에서 호출하는 자신을 A'이라고 하면, A에서 A'를 호출하고, 호출된 A'에서는 또 A''를 호출하게 되는 방식으로 무한히 호출하게 되므로 적절하게 종료시킬 수 있는 조건을 추가해주는 게 좋다. 

 

다음은 재귀 함수의 활용 방법들이다. 재귀 호출의 가장 대표적인 예시로는 피보나치 수열이 있다. 

# 피보나치 수열의 n번째 항을 구하는 함수

def fibonacci(n):
    if n == 0: return 0
    elif n == 1: return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)


for i in range(1, 11):
    print(fibonacci(i), end=' ')

# ans : 1 1 2 3 5 8 13 21 34 55

 

위의 함수를 보면 n이 0인 경우와 1인 경우는 재귀 호출을 하지 않고 바로 값을 반환하는 것을 알 수 있다. 

이렇게 조건을 달아두면 n이 무한히 작아지지 않으므로 최대 재귀 깊이를 초과하여 에러가 발생하는 경우를 방지할 수 있다. 

 

또 for문이나 while문으로 작성하던 코드를 재귀 함수를 통해서도 동일하게 구현할 수 있다. 

# 1부터 n까지의 합을 구하는 함수

def sum(n):
    if n == 0: return 0
    return n + sum(n-1)

print(sum(10)) # 55
print(sum(15)) # 120

 

이전에 반복문을 이용해서 1부터 n까지 자연수의 합을 구했던 것처럼, 재귀 함수를 이용해서도 구할 수 있다. 

1 ~ n까지의 합 = n + (1 ~ n-1까지의 합)을 이용한다. 

 

기본적으로 파이썬에서 최대 재귀 깊이는 1000으로 설정되어있지만, 이를 임의로 조절할 수도 있다.

sys.getrecursionlimit(숫자)를 통해서 최대 재귀 깊이를 더 늘릴 수도 있다.  

 

 

  2. 다양한 형태의 매개변수

 

1. 디폴트 매개변수 (Default Parameter)

 

함수를 정의할 때, 매개변수의 기본값을 지정해줄 수 있다. 그러면 함수를 호출할 때 인수를 생략하더라도 기본값이 매개변수에 전달되어 함수를 사용할 수 있게 된다. 

매개변수의 초깃값은 인수가 가끔씩만 다른 값을 가지는 경우에 유용하게 사용된다. 

대표적인 예시로 print 함수가 있다.  

print함수의 sep은 기본값이 ' '(공백)으로 되어있고, 우리가 공백을 사용하지 않고 다른 값을 이용하고 싶을 때 sep = ~~ 와 같이 추가해주는 형태인 것이다. 

 

사용하는 방식은 매개변수=값 형태로 지정한다.

def func(a, b=10, c=7):
    print(a + b + c)

func(1, 2, 3) # ans : 1 + 2 + 3

func(1, 3) # ans : 1 + 3 + 7

func(2) # ans : 2 + 10 + 7

 

기본값을 지정할 때 주의해야 하는 부분이 있다. 기본값이 지정되는 변수는 항상 뒤에서부터 선언되어야 한다. 

예를 들어 다음과 같은 함수를 보자. 

def func(a, b=10, c):
    print(a + b + c)

 

이 함수를 호출할 때 func(1, 2)로 호출한다면, 2라는 값이 b에 들어갈지 c에 들어갈지 알 수 없다. 

따라서, 위처럼 함수를 정의하면 에러가 발생하게 된다. 

 

 

2. n개의 매개변수

 

이번에는 인수의 개수를 유동적으로 받을 수 있는 매개변수의 형태이다. 주로 입력되는 인수의 개수가 몇 개가 될지 알 수 없을 때, 혹은 매번 인수의 개수가 달라질 수 있는 경우에 이용된다. 가변 인수(variable argument)라고도 한다. 

 

가변 인수 함수는 매개변수 앞에 '*'를 붙여서 만들어주며, 입력받은 인자는 튜플로 전달된다. 따라서 함수 내에서 iterable한 객체를 이용할 수 있는 구문들과 주로 이용된다. 

# 입력받은 수들의 합을 반환하는 함수
def func(*args):
    res = 0
    for i in args:
        res += i
    return res

print(func(2)) # 2
print(func(2, 4, 10)) # 16
print(func(3, 7, 11, 5)) # 26

 

위 코드와 같이 여러 인수를 입력하면 해당 인수들을 원소로 갖는 튜플 args가 만들어져서 함수 내로 전달되게 된다. 

혹은 튜플 자체를 인자로 넘겨줄 수도 있다. 

 

일반적으로 이용하는 '고정 인수'와 '가변 인수'를 함께 사용하고 싶다면, 가변 인수는 항상 맨 뒤에 선언되어야 한다. 물론 가변 인수가 2개 이상일 수는 없다. 앞에서 디폴트 매개변수를 항상 맨뒤에 선언해야 하는 이유와 동일하다. 

a = (4, 10, 7, 6)
def func(*args):
    for i in args:
        print(i, end=' ')
func(a) # 튜플로 함수에 전달 / ans : (4, 10, 7, 6)


def student(n, *args):
    print("학생의 수는 총 %d명입니다." %n)
    print("학생 이름 : ", end ='')
    for name in args:
        print(name, end=' ')

student(3, 'rebro', 'john', 'minsu') 
# 학생의 수는 총 3명입니다.
# 학생 이름 : rebro john minsu

 

만약 튜플이 아니라 리스트를 인자로 넘겨주고 싶다면 '함수(*리스트명)'과 같이 호출해야 한다.

단순히 '함수(리스트명)'으로 호출하면 리스트 자체를 원소로 갖는 튜플이 인자로 넘어가게 된다. 

a = [4, 10, 7, 6]
def func(*args):
    print(args)
func(a) # ans : ([4, 10, 7, 6], )

func(*a) # ans : (4, 10, 7, 6)

 

키워드 인자도 받을 수 있다. 이 경우에는 튜플이 아니라 딕셔너리로 넘어가며, 함수를 정의할 때에는 매개변수 앞에 **를 붙인다. 

def func(**kwargs):
    print(kwargs)

func(a=2, b=True, c='abc') 
# ans : {'a': 2, 'b': True, 'c': 'abc'}

 

 

3. 키워드 매개변수

 

이전까지는 함수에 인수를 넘겨주면 작성한 순서대로 매개변수에 인수가 할당된다. 이런 방식으로는 항상 매개변수의 순서를 알고 있어야 한다는 단점이 있다. 따라서 이를 해결하기 위해서 '키워드 매개변수'라는 기능을 제공한다. 

호출하는 방식은 단순하다. '매개변수명 = 값'으로 내가 원하는 매개변수에 직접 값을 넣어주는 형태이다.

def func(a, b, c):
    print(a+2, b-3, c+5)

func(1, 2, 3) # 3 -1 8

func(b=1, a=2, c=3) # 4 -2 8

 

 

 

  3. 람다 표현식

 

람다 함수 또는 람다 표현식(lambda expression)은 간단히 말해 이름이 없는 함수이다. 

지금까지의 함수는 def로 정의해둔 다음 언제든지 다시 사용할 수 있었는데, 람다 함수는 필요한 곳에 일시적으로 만들어서 사용하고 버리는 함수이다. 

따라서, 메모리를 절약할 수 있는 장점이 있고 코드 또한 간결해진다. 

 

함수의 구조는 식의 형태로 이루어져 있으며, 'lambda 매개변수: 식' 의 형태이다. 

'식'에 해당하는 부분에서 계산된 결과를 반환값으로 가진다. 

 

간단한 예시를 하나 보자.

def plus(a):
    return a + 1
print(plus(5)) # 6

plus = lambda x: x+1
print(plus(7)) # 7

 

위의 함수는 기존에 def를 이용하여 정의하던 방식이고, 아래의 방식이 람다 표현식을 이용한 경우이다. 두 함수는 동일하게 입력된 값 + 1을 반환한다.

 

이렇게 정의하면 사실 기존 방식이랑 무슨 차이가 있나 싶다. 

print((lambda x:x+1)(6)) # ans : 7

 

이제 조금 차이점이 보일 것이다. 함수를 미리 정의해두지 않고 일시적으로 생성하여 이용한 뒤 버린다. 함수의 이름 또한 없다. 이를 이용하면 함수를 한 줄로 간결하게 표현할 수 있게 된다. 

 

람다 표현식 안에서는 새로운 변수를 만들 수 없다는 단점이 있다. 만약 새로운 변수가 필요하다면 def를 이용해서 함수를 작성해야 한다. 

 

이 람다 표현식을 여러 함수(map, filter, reduce 등)와 함께 이용하면 훨씬 효율적으로 사용할 수 있다. 

 

1. 조건문과 함께 사용하기

 

람다 표현식에 조건문을 넣어서 사용할 수 있다.

구조는 다음과 같다.

 

lambda 매개변수 : 식1 if 조건 else 식2

 

조건을 만족하면 식1을 수행하고, 그렇지 않으면 식2를 수행하는 방식이다. 

리스트에서 짝수인 숫자는 음수로 바꿔주는 함수를 만들어보자.

a = [1, 2, 4, 3, 5, 8, 6, 9]
b = list(map(lambda x: -x if x%2 == 0 else x, a))

print(b) # [1, -2, -4, 3, 5, -8, -6, 9]

 

이렇게 람다 표현식을 이용하면 한 줄만에 새로운 리스트를 만들어낼 수 있다. 

람다 표현식 내에서 조건문을 이용할 땐, if와 else에 콜론을 붙이지 않는다. 또 반드시 else를 사용해야 하며, elif를 사용할 수 없다. 

만약 여러 조건을 사용하고 싶다면 if와 else 쌍 여러 개를 사용하는 방법도 있으나, 알아보기가 힘들기 때문에 def를 사용하는 것이 좋다. 

 

 

2. map과 함께 사용하기

 

map은 map(함수, 리스트)의 구조로 이루어져 있다. 여기서 함수 부분에 람다 표현식을 이용하면 간단하게 코드를 구현할 수 있다. 

 

# 5 이하의 자연수의 제곱수를 원소로 갖는 리스트 구하기
a = []
for i in range(1, 6):
    a.append(i*i)

b = list(map(lambda x: x**2, range(1, 6)))

print(a == b) # True

# 두 리스트의 곱
a = [1, 2, 3, 4, 5]
b = [2, 3, -1, 2, 4]
c = list(map(lambda x, y: x*y, a, b)) # [2, 6, -3, 8, 20]

# 세 리스트의 곱
a = [1, 2, 3, 4, 5]
b = [2, 3, -1, 2, 4]
c = [2, 2, 2, 2, 2]
d = list(map(lambda x, y, z: x*y*z, a, b, c))
print(d) # [4, 12, -6, 16, 40]

 

먼저, 5 이하의 자연수들의 제곱수를 원소로 갖는 리스트를 구하는 방법이다. 전자는 직접 1부터 5까지 제곱수를 구하여 리스트 a에 넣어주는 방식이고 후자는 람다 표현식을 이용하여 곧바로 리스트를 만들어준다. 

리스트 두 개 이상을 처리할 때에는 람다 표현식에서 매개변수를 두 개 이상으로 지정하고, 리스트들을 콤마로 구분해서 넣어주면 된다. 

 

 

3. filter와 함께 사용하기

 

filter함수는 반복 가능한(iterable) 객체에서 특정 조건에 맞는 원소들만 가져온다. filter함수 또한 map과 마찬가지로 filter(함수, 리스트)의 형태로 이루어져 있다. 아래 예시를 통해서 이해해보자.

# 리스트에서 홀수인 수만 뽑아내기

a = [1, 4, 5, 8, 6, 3, 10, 7]
print(list(filter(lambda x: x%2 == 1, a))) # [1, 5, 3, 7]

# 100 이하의 자연수에서 3과 5의 배수인 수만 뽑아내기

print(list(filter(lambda x: x%3 == 0 and x%5 == 0, range(1, 101)))) 
# [15, 30, 45, 60, 75, 90]

 

 

 

 

PC로 보시는 것을 권장합니다. 

피드백은 언제나 환영입니다. 댓글로 달아주세요 ^-^

 

 

반응형