고계함수(higher-order functions) 활용법

파이썬은 함수와 메소드를 1급 객체(first-class object)로 다루며, 이는 C, C#, Java 등과 매우 다른 특징 중의 하나이다. 1급 객체는 예를 들어 함수의 인자로 사용될 수 있다. 파이썬을 제외한 기타 언어에서는 함수는 2급 객체로 구분되는 반면에 숫자, 문자열, 리스트, 튜클 등등만을 1급 객체로 다루며, 따라서 따라서 함수 자체를 다른 함수의 인자로 사용할 수 없다.

C 언어의 경우 함수 포인터, Java의 경우 클래스를 활용하여 동일한 기능을 구현할 수 있다. 하지만 좀 더 복잡하며, 파이썬은 매우 간단히 함수를 활용하는 방법을 제공한다.

함수를 인자로 받는 함수를 통틀어 고계함수(higher-order function)라 한다. 여기서는 함수를 다른 함수의 인자값 또는 리턴값으로 사용하는 방법을 배운다.

함수를 인자로 입력 받기

숫자들의 리스트를 입력받아 각 항목의 제곱으로 이루어진 리스트를 리턴하는 함수를 작성해보자.


In [1]:
def x2_list(xs):
    L = []
    for x in xs:
        L.append(x**2)
    return L

In [2]:
x2_list([0, 0.5, 1, 1.5, 2, 2.5])


Out[2]:
[0, 0.25, 1, 2.25, 4, 6.25]

이제 숫자들의 리스트를 입력받아 각 항목의 세제곱으로 이루어진 리스트와 네제곱으로 이루어진 리스트를 리턴하는 함수들을 작성해보자.


In [3]:
def x3_list(xs):
    L = []
    for x in xs:
        L.append(x**3)
    return L

In [4]:
def x4_list(xs):
    L = []
    for x in xs:
        L.append(x**4)
    return L

이런 식으로 5승, 6승, 7승값으로 이루어진 리스트를 린터하는 함수들을 작성하려면 매번 전체코드를 복사해서 특정 부분을 수정해 주어야 한다. 그런데 이런 과정은 매우 비효율적이며 코드를 복잡하게 만든다.

코드를 작성할 때 가장 주의해야 하는 것중의 하나는 변하는 부분과 변하지 않는 부분을 구분해서 따로따로 처리하는 것이다. 앞서 다룬 세 개의 함수들의 경우 각각 4번째 줄에서 사용되는 지수값에 의해서만 구분된다. 이럴 경우 아래와 같이 변화되는 부분을 일반화시켜 인자로 빼서 활용할 수 있다.


In [5]:
def xn_list(n, xs):
    L = []
    for x in xs:
        L.append(x**n)
    return L

즉, 변하는 지수갑만 따로 빼내서 인자로 활용하면 들어오는 인자값에 따라 임의의 지수승값으로 이루어진 리스트를 리턴하는 함수를 만들게 되었다.

5승 해주는 함수

In [6]:
xn_list(5, [0, 0.5, 1, 1.5, 2, 2.5])


Out[6]:
[0, 0.03125, 1, 7.59375, 32, 97.65625]
10승 해주는 함수

In [7]:
xn_list(10, [0, 0.5, 1, 1.5, 2, 2.5])


Out[7]:
[0, 0.0009765625, 1, 57.6650390625, 1024, 9536.7431640625]

이제 xn_list 함수가 다음의 경우도 다룰 수 있는지 확인해보자.


In [8]:
from math import *

In [9]:
def sin_list(xs):
    L = []
    for x in xs:
        L.append(sin(x))
    return L

위 함수는 sin 함수값으로 이루어진 리스트를 리턴하는 함수이며, 불행히도 xn_list 함수를 이용하여 구현할 수 없다. 이유는 넷째 줄에서 변하는 부분이 x ** n의 꼴이 아니기 때문이다.

xn_list 함수를 수정해서 위 경우까지 다룰 수 있으려면 단순히 지수값만이 아니라 사용되는 함수 전체를 인자로 사용해야한다. 코드를 아래와 같이 수정하면 된다.


In [10]:
def fun_list(f, xs):
    L = []
    for x in xs:
        L.append(f(x))
    return L

이제 sin_list(xs)fun_list(sin, xs)와 동일함을 확인할 수 있다.


In [11]:
sin_list([0, 0.5, 1, 1.5, 2, 2.5])


Out[11]:
[0.0,
 0.479425538604203,
 0.8414709848078965,
 0.9974949866040544,
 0.9092974268256817,
 0.5984721441039564]

In [12]:
fun_list(sin, [0, 0.5, 1, 1.5, 2, 2.5])


Out[12]:
[0.0,
 0.479425538604203,
 0.8414709848078965,
 0.9974949866040544,
 0.9092974268256817,
 0.5984721441039564]

In [13]:
fun_list(sin, [0, 0.5, 1, 1.5, 2, 2.5]) == sin_list([0, 0.5, 1, 1.5, 2, 2.5])


Out[13]:
True

따라서 sin_list 함수를 다음과 같이 재정의 할 수 있다.


In [14]:
def sin_list_1(xs):
    return fun_list(sin, xs)

앞서 다뤘던 x2_list, x3_list 함수들을 fun_list 함수를 이용하여 구현할 수 있다.


In [15]:
def x2_list_1(xs):
    def x_2(x):
        return x ** 2
    return fun_list(x_2, xs)

In [16]:
x2_list_1([0, 0.5, 1, 1.5, 2, 2.5]) == x2_list([0, 0.5, 1, 1.5, 2, 2.5])


Out[16]:
True

In [17]:
def x3_list_1(xs):
    def x_3(x):
        return x ** 3
    return fun_list(x_3, xs)

In [18]:
x3_list_1([0, 0.5, 1, 1.5, 2, 2.5]) == x3_list([0, 0.5, 1, 1.5, 2, 2.5])


Out[18]:
True

앞서 다뤘던 xn_list 함수 또한 fun_list 함수를 이용하여 구현할 수 있다.


In [19]:
def xn_list_1(n, xs):
    def x_n(x):
        return x ** n
    return fun_list(x_n, xs)

In [20]:
xn_list_1(5, [0, 0.5, 1, 1.5, 2, 2.5]) == xn_list(5, [0, 0.5, 1, 1.5, 2, 2.5])


Out[20]:
True

lambda(람다) 키워드 활용법

  • lambda는 이름이 없는 함수를 정의할 때 사용함.

  • 정의되는 함수의 이름이 없으므로, 함수를 호출하고자 할 때는 함수 정의 전체를 항상 사용해야 한다.

  • 예제 1: 인자를 하나 받는 함수

      lambda x : x ** 5 
    
    

    위 함수는 아래 함수와 동일한 역할을 수행함.

      def arbitrary_name1(x):
          return x**5
    
    

    즉, arbitrary_name1(5)(lambda x : x**5)(5)와 동일한 값임.

  • 예제 2: 인자를 두개 이상 받는 함수 경우는 사용되는 인자들을 콤마로 구분해서 나열하면 된다.

      lambda x, y: (x + y) * 2
    
    

    위 함수는 다음 함수와 동일하다.

      def arbitrary_name2(x, y):
          return (x + y) * 2
lambda 활용 예제 1: 숫자 3을 곱하는 함수

In [21]:
(lambda x: x*3)(5)


Out[21]:
15

위 함수는 아래 함수와 동일하다. 대신 사용법에 차이가 있음에 주의해야 한다.


In [22]:
def fx_3(x):
    return x * 3

fx_3(5)


Out[22]:
15
lambda 활용 예제 2: 두개의 수를 입력 받아 더해준 후 2배를 해주는 함수

In [23]:
(lambda x, y: (x + y) * 2)(3, 4)


Out[23]:
14

위 함수는 아래 함수와 동일하다.


In [24]:
def plus_2times(x, y):
    return (x + y) * 2

plus_2times(3, 4)


Out[24]:
14
lambda 활용 예제 3:

앞서 fun_list를 이용하여 재정의한 x2_list, x3_list, xn_list 함수를 아래와 같이 lambda를 이용하여 재정의할 수 있다.


In [25]:
def x2_list_2(xs):
    return fun_list(lambda x : x ** 2, xs)

In [26]:
x2_list_2([0, 0.5, 1, 1.5, 2, 2.5]) == x2_list_1([0, 0.5, 1, 1.5, 2, 2.5])


Out[26]:
True

In [27]:
def x3_list_2(xs):
    return fun_list(lambda x : x ** 3, xs)

In [28]:
x3_list_2([0, 0.5, 1, 1.5, 2, 2.5]) == x3_list_1([0, 0.5, 1, 1.5, 2, 2.5])


Out[28]:
True

In [29]:
def xn_list_2(n, xs):
    return fun_list(lambda x : x ** n, xs)

In [30]:
xn_list_2(5, [0, 0.5, 1, 1.5, 2, 2.5]) == xn_list_1(5, [0, 0.5, 1, 1.5, 2, 2.5])


Out[30]:
True

연습문제 1

함수를 입력 받아 x = {0, 0.5, 1, 1.5, 2, 2.5}에 해당하는 좌표들을 프린트하는 함수 fun_table(f)를 작성하라.

견본답안

In [31]:
def fun_table(f):
    for i in range(6):
        x = i * 0.5
        print("{} {}").format(x, f(x))

견본답안 활용 예제


In [32]:
# 제곱과 세제곱 함수

def square(x): return x ** 2
def cubic(x): return x ** 3

In [33]:
print("Square")
fun_table(square)


Square
0.0 0.0
0.5 0.25
1.0 1.0
1.5 2.25
2.0 4.0
2.5 6.25

In [34]:
print("Cubic"); fun_table(cubic)


Cubic
0.0 0.0
0.5 0.125
1.0 1.0
1.5 3.375
2.0 8.0
2.5 15.625

함수를 리턴값으로 사용하기

함수를 하나의 값으로 취급할 수 있으므로 함수를 리턴값으로 갖는 함수를 정의할 수 있다.

아래에서 정의된 linear_fun(a, b) 함수는 두 개의 실수 a, b를 입력받아 일차함수인

linear(x) = a * x + b

를 리턴한다.


In [35]:
def linear_fun(a, b):
    def linear(x):
        return a * x + b
    return linear

linear_fun(a, b)의 리턴값을 사용하는 방법은 함수를 호출하는 것과 동일하다.


In [36]:
g = linear_fun(2, 3)
g(5)


Out[36]:
13

위 코드에서 g(x) = 2*x + 3 함수를 의미하게 된다.

함수들의 리스트

함수를 1급 객체(first-class object)로 다룰 수 있다는 것은 함수들의 리스트를 만들 수 있다는 것도 포함한다.


In [37]:
funs = [sin, cos]

함수들의 리스트 역시 자료형은 list이다.


In [38]:
type(funs)


Out[38]:
list

이제 Square 함수와 Cubic 함수의 테이블을 연속으로 보여줄 수 있다.


In [39]:
for f in funs:
    print(str(f))
    fun_table(f)


<built-in function sin>
0.0 0.0
0.5 0.479425538604
1.0 0.841470984808
1.5 0.997494986604
2.0 0.909297426826
2.5 0.598472144104
<built-in function cos>
0.0 1.0
0.5 0.87758256189
1.0 0.540302305868
1.5 0.0707372016677
2.0 -0.416146836547
2.5 -0.801143615547

In [40]:
str(sin)


Out[40]:
'<built-in function sin>'

함수 이름을 좀 더 예쁘게 보여주고 싶으면 위 코드를 좀 더 섬세하게 수정해야 한다.


In [41]:
for f in funs:
    print("<" + str(f).split()[-1][:-1] + " 함수 테이블>")
    fun_table(f)
    print("\n")


<sin 함수 테이블>
0.0 0.0
0.5 0.479425538604
1.0 0.841470984808
1.5 0.997494986604
2.0 0.909297426826
2.5 0.598472144104


<cos 함수 테이블>
0.0 1.0
0.5 0.87758256189
1.0 0.540302305868
1.5 0.0707372016677
2.0 -0.416146836547
2.5 -0.801143615547


위 코드에서 2번 줄이 조금 복잡해 보인다. 특히

str(f).split()[-1][:-1]

코드를 면밀히 살펴보아야 한다. 위 문장에서 변수 fsin 또는 cos 함수를 가리킨다. 예를 들어, str(sin)을 실행하였을 때 리턴되는 값을 확인하면 다음과 같다.

'<built-in function sin>'

따라서 위 문자열을 먼저 split 메소드로 쪼개어 리턴되는 리스트의 마지막 항목인 sin> 문자열을 선택한 후, 마지막 문자인 >을 제외한 나머지 문자열을 프린트 한다.

키워드 인자가 있는 함수

이전 과제에서 살펴보았던 sorted 정렬함수의 경우에 인자의 개수가 다양하게 변할 수 있음을 보았다. sorted 함수의 기본 사용법은 아래와 같다.


In [42]:
sorted([2, 1, 3])


Out[42]:
[1, 2, 3]

즉, 주어진 시퀀스 자료형을 크기 순서대로 오름차순으로 정렬한다. 그런데 내림차순으로 정렬하고자 하면 다음과 같이 사용해야 한다.


In [43]:
sorted([2, 1, 3], reverse=True)


Out[43]:
[3, 2, 1]

sorted 함수의 인자가 하나 더 늘어났다. 실제로 sorted 함수는 최대 네 개까지 인자를 받을 수 있다. help 명령어를 사용하면 아래와 같은 정보를 확인할 수 있다.


In [44]:
help(sorted)


Help on built-in function sorted in module __builtin__:

sorted(...)
    sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list

위 정보는 sorted 함수는 네 개의 인자를 받는다. 하지만, 두 번째부터 네 번째 인자들은 이미 기본값이 정해져 있다. 그래서 그 인자들은 굳이 입력하지 않아도 파이썬 해석기 내부에서는 지정된 기본값으로 처리한다. 따라서 다음이 성립한다.


In [45]:
sorted([2, 1, 3]) == sorted([2, 1, 3], reverse=False)


Out[45]:
True

sorted 함수의 다른 두 개의 키워드 인자인 cmpkey의 경우도 사용법은 동일하다. 두 키워드 인자의 역할은 다음과 같다.

  • cmp 키워드 인자: 두 개의 값을 어떻게 비교할지를 정해주는 함수가 입력되어야 한다. 기본값으로 None이라 함은 파이썬에서 기본적으로 정의된 방식으로 값을 비교한다는 의미이다.

  • key 키워드 인자: 문자열, 리스트, 튜플 등 시퀀스 자료형 값을 정렬할 때 무엇을 기준으로 정렬할지를 정하는 데에 사용한다. 기본값인 None을 사용하면 파이썬에서 정해진 순서대로 정렬한다.

주의: cmp 키워드 인자는 파이썬 3.x 버전에서는 사용되지 않는다. 따라서 파이썬 3.x 버전에서 sorted 함수의 인자로 세 개가 된다.

key 인자 사용 예제 1

문자열을 정렬할 때 기본적으로 대문자가 소문자보다 앞선다는 식으로 순서가 정해졌다.


In [46]:
'A' < 'a'


Out[46]:
True

따라서 문자열을 정렬할 때 대문자가 시작하는 단어가 먼저 정렬된다.


In [47]:
sorted("This is a test string from Andrew".split())


Out[47]:
['Andrew', 'This', 'a', 'from', 'is', 'string', 'test']

대, 소문자 구별없이 정렬하고자 한다면 key 인자 값을 수정해야 한다. 예를 들어, 문자열 클래스의 lower 메소드인 str.lower를 이용하여 문자열을 모두 소문자로 바꾼 뒤에 순서를 정하는 식을 활용할 수 있다.


In [48]:
sorted("This is a test string from Andrew".split(), key=str.lower)


Out[48]:
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
key 인자 사용 예제 2

일정한 양식으로 정리된 튜플들의 리스트를 정렬하고자 할 때 어떤 항목을 기준으로 정렬할지 결정할 수 있다.

예를 들어 (이름, 학점, 점수)의 양식으로 구성된 리스트가 아래와 같이 있다.


In [49]:
students_grades = [
    ('john', 'B', 12),
    ('jane', 'C', 10),
    ('dave', 'A', 15),
]

sorted 함수를 기본값으로 적용하면 이름 순서대로 정렬된다.


In [50]:
sorted(students_grades)


Out[50]:
[('dave', 'A', 15), ('jane', 'C', 10), ('john', 'B', 12)]

이제 위 리스트를 학점 순으로 정렬하려면 다음과 같이 key 인자 값을 변경해야 한다.

주의: 'A' < 'B' < 'C'


In [51]:
sorted(students_grades, key=lambda student: student[1])


Out[51]:
[('dave', 'A', 15), ('john', 'B', 12), ('jane', 'C', 10)]

위에서 key인자에 입력된 lambda student: student[1] 함수는 students_grades 리스트의 각 항목을 인자로 받았을 때 인덱스 번호 1에 해당하는 항목, 즉 두 번째 항목인 학점을 기준으로 정렬하라고 알려주는 역할을 수행한다.

등급을 오름차순으로 정렬하고자 한다면 reverse 인자값도 변경해야 한다.


In [52]:
sorted(students_grades, key=lambda student: student[1], reverse=True)


Out[52]:
[('jane', 'C', 10), ('john', 'B', 12), ('dave', 'A', 15)]

이제 위 리스트를 점수 순으로 정렬하려면 다음과 같이 key 인자 값을 변경해야 한다.


In [53]:
sorted(students_grades, key=lambda student: student[2])


Out[53]:
[('jane', 'C', 10), ('john', 'B', 12), ('dave', 'A', 15)]
키워드 인자가 있는 함수 생성하기

앞서 다룬 예제들은 파이썬에 내장된 함수들을 사용하였다. 이제 직접 키워드 인자가 있는 함수를 생성해보자.

함수 area(a,b)는 직사각형의 면적을 계산하는 함수이다.

  • 첫재 인자 a는 가로의 길이를 나타낸다.
  • 둘째 인자 b는 세로의 길이를 나타낸다.

In [54]:
def area(a, b):
    return a * b

print("가로, 세로 길이가 각각 {}, {}인 직사각형의 넓이는 {}이다.".format(3, 3, area(3, 3)))
print("가로, 세로 길이가 각각 {}, {}인 직사각형의 넓이는 {}이다.".format(3, 5, area(3, 5)))


가로, 세로 길이가 각각 3, 3인 직사각형의 넓이는 9이다.
가로, 세로 길이가 각각 3, 5인 직사각형의 넓이는 15이다.

이제 세로의 길이를 3고정해서 사용하고자 한다면 다음과 같이 키워드 인자를 선언하면 된다. 그러면 인자를 하나만 입력할 경우 자동으로 세로의 길이는 3으로 삽입되어 계산된다. 반면에 세로의 길이를 변경하고자 한다면 해당 키워드 인자값을 변경하면 된다.


In [55]:
def area(a, breadth=3):
    return a * breadth

print("가로, 세로 길이가 각각 {}, {}인 직사각형의 넓이는 {}이다.".format(3, 3, area(3)))
print("가로, 세로 길이가 각각 {}, {}인 직사각형의 넓이는 {}이다.".format(5, 3, area(5)))
print("가로, 세로 길이가 각각 {}, {}인 직사각형의 넓이는 {}이다.".format(5, 7, area(5, 7)))


가로, 세로 길이가 각각 3, 3인 직사각형의 넓이는 9이다.
가로, 세로 길이가 각각 5, 3인 직사각형의 넓이는 15이다.
가로, 세로 길이가 각각 5, 7인 직사각형의 넓이는 35이다.

이제 키워드가 두 개 이상일 경우 인자의 순서에 주의해야 한다.

  • 키워드 이름을 언급하는 경우: 키워드 인자끼리의 순서는 중요하지 않다.
  • 키워드 이름을 언급하지 않는 경우: 인자의 순서가 중요하다.

아래 예제를 잘 살펴보면서 키워드 인자를 어떻게 활용하는지 다시 한 번 확인할 수 있다.


In [56]:
def strange_volume(x, y_axis = 3, z_axis=4):
    return x * y_axis * (z_axis + 1)

print(strange_volume(3, y_axis=5, z_axis=7) == strange_volume(3, z_axis=7, y_axis=5))
print(strange_volume(3, 5, 7) == strange_volume(3, 7, 5))


True
False

전역변수와 지역변수

함수를 선언할 때 사용되는 변수들이 있다. 각각의 변수들이 어디에서 정의되었는가에 따라 지역변수(local variable) 또는 전역변수(global variable)라 불린다.

예를 들어 아래 함수에서 사용되는 변수 y는 어디에서도 정의되어 있지 않기에 호출할 때 에러가 발생한다.


In [57]:
def f():
    print(y)

In [58]:
f()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-58-0ec059b9bfe1> in <module>()
----> 1 f()

<ipython-input-57-3bde1177804c> in f()
      1 def f():
----> 2     print(y)

NameError: global name 'y' is not defined

위와 같은 경우를 대비해 에러처리를 해둘 필요도 있다.


In [59]:
def f():
    try:
        print(y)
    except:
        raise NameError("the varible y is not defined yet")

In [60]:
f()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-60-0ec059b9bfe1> in <module>()
----> 1 f()

<ipython-input-59-accc206fc433> in f()
      3         print(y)
      4     except:
----> 5         raise NameError("the varible y is not defined yet")

NameError: the varible y is not defined yet

다음은 함수 내부에서 정의되어 사용되는 변수, 즉, 지역변수에 대한 간단한 예제이다.


In [61]:
def f1():
    x = '지역변수이다'
    print("x는 {}.".format(x))

In [62]:
f1()


x는 지역변수이다.

반면에 함수 외부에서 정의된 변수는 전역변수이다.


In [63]:
x = '전역변수이다'

In [64]:
def f2():
    print("x는 {}.".format(x))

In [65]:
f2()


x는 전역변수이다.

x가 전역변수로 이미 선언이 되었어도 f1 함수를 호출하면 그 함수 안에 선언된 x 값이 사용된다. 함수가 호출되었을 때 사용되는 변수에 할당된 값을 먼저 함수 내부에서 찾으며, 함수 내부에서 찾지 못할 경우 함수 밖에서 찾기 때문이다.


In [66]:
f1()


x는 지역변수이다.

함수가 호출되었을 때 사용되는 변수에 할당된 값을 먼저 함수 내부에서 찾으며, 함수 내부에서 찾지 못할 경우 함수 밖에서 찾기 때문이다.

함수 f1에서 정의된 지역벽수 x는 함수 밖에서 정의된 전역변수 x와 이름만 같을 뿐 전혀 상관이 없으며 서로 알지 못한다. 지역변수와 전역변수의 이름을 가능하면 다르게 지정하는 것이 좋다.

또한 지역변수는 함수 밖으로는 절대로 알려지지 않음에 주의해야 한다.


In [67]:
def d_is_only_defined_here():
    d = 10
    print("d 값은 여기서만 {}으로 정의되었습니다".format(d))

In [68]:
d_is_only_defined_here()


d 값은 여기서만 10으로 정의되었습니다

위 함수 외부에서 d값을 물으면 모른다고 에러가 발생한다.


In [69]:
d


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-69-e29311f6f1bf> in <module>()
----> 1 d

NameError: name 'd' is not defined

함수 내부에서 선언된 지역변수를 전역변수처럼 사용하려면 global 키워드를 사용한다.


In [70]:
def e_is_made_global():
    global e
    e = 10
    print("e 값은 여기서도 보이며 현재 {} 값이 할당되어 있습니다".format(e))

In [71]:
e_is_made_global()


e 값은 여기서도 보이며 현재 10 값이 할당되어 있습니다

In [72]:
e


Out[72]:
10