6. 함수#

데이터 분석용 프로그램 작성에 가장 중요한 요소는 적절한 함수를 적재적소에 활용하는 일이다. 파이썬 함수의 다양한 특성과 활용법을 잘 익혀 두어야 한다.

6.1. 함수 선언#

함수는 지정된 코드에 이름을 주어 필요할 때 간편하게 재사용할 수 있도록 도와주는 도구이다. 파이썬에서 함수는 def 키워드, 함수 이름, 매개변수, 함수 본문을 아래 형식으로 사용하여 선언된다.

def 함수이름(위치매개변수, ..., 키워드매개변수, ... ):
    함수본문

함수의 매개변수는 위치 매개변수키워드 매개변수로 구분된다. 예를 들어, 아래 함수에서 xy는 위치 인자를 받는 매개변수이고, z는 키워드 인자를 받는 매개변수이다.

def my_function(x, y, z=1.5):
    if z > 1:
        return 2 * (x + y) - z
    else:
        return (x + y) + z

my_function() 함수를 실행할 때 x, y 두 위치 매개변수가 가리킬 두 개의 인자를 반드시 지정해야 한다. 반면에 키워드 매개변수 z에 대해서는 인자를 굳이 지정하지 않아도 되며, 그 경우 셋째 인자로 미리 지정된 1.5가 사용된다.

아래 두 코드는 셋째 인자로 각각 1과 3.5를 지정한 결과를 보여준다.

my_function(5, 6, z=1)
12
my_function(3.14, 7, 3.5)
16.78

아래 코드는 셋째 인자를 지정하지 않아서 셋째 인자로 1.5가 자동 지정된 결과를 보여준다.

my_function(10, 20)
58.5

6.2. 변수 이름 공간과 활동 영역#

함수 본문에서는 전역 변수와 지역 변수 모두 사용될 수 있다.

  • 전역 변수: 함수 밖에서 선언된 변수

  • 지역 변수: 함수의 매개변수 또는 함수 내에서 선언된 변수

예를 들어 아래 함수 func1()는 두 개의 지역변수 ab 모두 사용한다.

def func1(b):
    a = []
    for i in range(5):
        a.append(i)
    
    b.extend(a)
    return b
print(func1([-4, -3, -2, -1]))
[-4, -3, -2, -1, 0, 1, 2, 3, 4]

아래 함수 func2()는 전역변수 a와 지역변수 b 모두 사용한다.

a = []

def func2(b):
    for i in range(5):
        a.append(i)
    
    b.extend(a)
    return b
print(func2([-4, -3, -2, -1]))
[-4, -3, -2, -1, 0, 1, 2, 3, 4]

전역변수가 가리키는 값을 함수 내에서 변경하려면 global 키워드를 이용해야 한다. global 키워드를 사용하지 않으면 의도대로 작동하지 않을 수 있다.

아래 코드는 bind_a_variable() 함수를 실행할 때 함수 본문에서 선언된 지역변수 a가 가리키는 값을 변경했지만 함수 밖에서 선언된 전역변수 a가 가리키는 전혀 변하지 않았다.

a = None                  # 전역변수

def bind_a_variable(b):
    a = [1, 2, 3]        # 지역변수
    a = a + b

bind_a_variable([4, 5])

print(a)
None

그런데 global 키워드를 사용하면 다르게 작동한다.

a = None

def bind_a_variable(b):
    global a
    a = [1, 2, 3]
    a = a + b

bind_a_variable([4, 5])

print(a)
[1, 2, 3, 4, 5]

주의사항

global 키워드는 조심스럽게 다루어어야 하기에 특별한 상황이 아니라면 사용을 피해야 한다. 이유는 복잡하기에 여기서는 그렇다고 언급만 한다.

모든 변수는 이처럼 역할에 따라 활동 영역이 달라진다. 변수의 활동 영역을 스코프scope라 부르며, 변수를 스코프에 따라 구분하여 관리하는 도구가 이름 공간name space이다.

예를 들어, 전역변수 이름 공간에 포함된 변수는 globals() 함수를 이용하여 확인할 수 있다. 아래 코드를 실행하면 매우 많은 변수를 확인하게 된다.

globals()

모든 함수는 자체의 네임 스페이스를 관리한다. 함수가 실행되는 도중에 locals() 함수가 호출되면 해당 함수가 사용할 수 있는 지역변수들을 확인할 수 있다.

a = []

def func2(b):
    for i in range(5):
        a.append(i)
        
    b.extend(a)
    
    print("지역 변수: ", locals())  # func2() 함수의 네임 스페이스 확인

func2() 는 실행 도중에 전역변수 이외에 bi 두 개의 지역변수를 사용할 수 있음을 아래와 같이 확인할 수 있다.

func2([])
지역 변수:  {'b': [0, 1, 2, 3, 4], 'i': 4}

6.3. 다중 값 반환#

함수는 실행 중에 return 키워드를 만나는 순간에 지정된 값을 반환하고 실행을 멈춘다. 즉, 원칙적으로 하나의 값만 반환할 수 있다는 의미이다. 그런데 튜플을 이용하여 여러 개의 값을 하나로 묶어서 하나의 값으로 반환할 수 있다. 예를 들어, 아래 함수는 a, b, c 세 개의 값을 튜플로 묶어 반환한다.

def f():
    a = 5
    b = 6
    c = 7
    return a, b, c

위 함수는 마치 세 개의 값을 반환하는 것처럼 보이지만 실제로는 (a, b, c)를 반환한다.

f()
(5, 6, 7)

튜플 풀어헤치기를 이용하면 세 개의 변수를 동시에 선언해서 활용할 수 있다.

a, b, c = f()

print((a + 2*b)/c)
2.4285714285714284

6.4. 제1종 객체와 함수#

변수 선언, 리스트의 항목, 함수의 인자 또는 반환값으로 사용될 수 있는 객체(값)를 제1종 객체first-class object라 한다. 정수, 부동소수점, 문자열, 리스트, 튜플, 사전 등이 대표적인 제1종 객체이다.

파이썬은 그런데 함수의 반환값 뿐만 아니라 함수 자체도 제1종 객체로 취급한다. 반면에 C, 자바 등의 언어에서는 함수 자체는 변수 선언, 배열의 항목, 다른 함수의 인자 또는 반환값으로 사용될 수 없다.

참고

‘제1종 객체’ 표현 대신에 ‘1급 객체’ 표현이 참고서에 많이 사용된다. 하지만 객체를 1급, 2급 등으로 나누는 것은 표현상 적절하지 않다는 판단하에 여기서는 제1종, 제2종 등의 표현을 사용한다. 프로그래밍 언어에 따라 제1종 객체의 범위가 다르다. 예를 들어, C와 자바 등에서는 함수는 제1종 객체가 아니다.

예제: 함수들의 리스트

정돈되지 않은 문자열들의 리스트가 아래와 같이 주어졌을 때 필요 없는 기호를 제거하는 작업을 진행하려 한다.

states = ['   Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
          'south   carolina##', 'West virginia?']

예를 들어, 스페이스, 느낌표, 물음표 등의 기호를 삭제하거나, 단어의 첫글자를 대문자로 변경하는 작업이 필요하다. 언급된 작업 모두 문자열 메서드 또는 문자열과 관련된 함수로 처리할 수 있다.

  • strip() 문자열 메서드: 단어 양끝의 스페이스 제거

"\n   Alabama\t   ".strip()
'Alabama'
  • title() 문자열 메서드: 단어의 첫글자만 대문자로 변경

"souTh   cArolina".title()
'South   Carolina'
  • re 모듈의 sub() 함수: 문자열의 일부를 다른 문자열로 대체

import re

# [!#?]: 느낌표, 샵, 물음표 를 대상으로 한다는 의미임.
re.sub('[!#?]', '', "Geo#rgi?a!")
'Georgia'

세 함수를 for 반복문에 함께 이용하면 리스트에 모든 문자열을 예쁘게 처리할 수 있다.

참고: 이와 같이 데이터를 다루기 좋게 처리하는 과정을 __데이터 전처리__라고 한다.

result = []
for value in states:
    value = value.strip()
    value = re.sub('[!#?]', '', value)
    value = value.title()
    result.append(value)
    
result
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

아래 함수는 임의의 리스트에 대해 앞서 언급된 전처리를 수행하는 함수이다.

def clean_strings(strings):
    result = []
    for value in strings:
        value = value.strip()
        value = re.sub('[!#?]', '', value)
        value = value.title()
        result.append(value)
    return result

위 함수를 이용하면 동일한 결과를 얻는다.

clean_strings(states)
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

clean_strings() 함수가 갖는 하나의 한계는 전처리 과정에 다른 종류의 작업을 처리하는 함수가 추가되어야 하거나 처리 작업의 종류가 달라질 때 발생한다. 왜냐하면 그럴 때는 함수 자체의 정의를 수정해야 하는 수고를 들여야 하기 때문이다.

이에 대한 해결책은 함수의 본문에서 처리하는 과정을 작업 기능에 따라 분리해 내어 clean_string() 함수와 별도로 관리하는 것이다. 예를 들어, 각각의 작업처리 함수를 하나의 리스트에 담아 놓은 후 clean_sting() 함수는 그 리스트에 포함된 함수를 필요할 때 활용하도록 할 수 있다. 그런데 이렇게 하려면 함수를 리스트의 항목으로 사용할 수 있어야 하는데, 파이썬에서는 함수의 이름으로 구성된 리스트를 작성하기만 하면 된다.

예를 들어, strip()title() 두 문자열 메서드의 이름으로 구성된 리스트는 아래와 같다.

주의사항: 함수를 명기 할 때 괄호를 사용하면 함수가 실행되어 반환한 값을 가리킨다. 따라서 괄호를 사용하지 않아야 함수 자체가 값으로 취급된다.

[str.strip, str.title]
[<method 'strip' of 'str' objects>, <method 'title' of 'str' objects>]

위 리스트에 느낌표, 샵, 물음표 기호를 제거하는 함수를 추가하려면 먼저 해당 함수를 정의해야 한다.

def remove_punctuation(value):
    return re.sub('[!#?]', '', value)

이제 앞서 사용된 세 작업을 수행하는 함수들의 리스트는 다음과 같다.

clean_ops = [str.strip, remove_punctuation, str.title]

위 리스트를 이용하여 clean_string() 함수를 재정의할 수 있다.

  • 첫째 인자: 단어들의 리스트

  • 둘째 인자: 작업 처리 함수들의 리스트

def clean_strings(strings, ops):
    result = []
    for value in strings:
        for function in ops:
            value = function(value)
        result.append(value)
    return result
clean_strings(states, clean_ops)
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

좋아 보인다. 그런데 ‘South Carolina’의 경우 두 단어 사이에 스페이스가 너무 많이 들어가 있는데 언급된 세 작업은 이를 처리하지 못한다. 이런 문자열을 처리하는 방법은 문자열을 스페이스 기준으로 쪼갠 다음에 다시 하나의 문자열로 합치는 것이다.

  • split() 문자열 메서드: 지정된 인자를 기준으로 문자열 쪼개기. 반환값은 쪼개진 문자열들의 리스트. 인자를 지정하지 않으면 스페이스가 기본 인자로 사용됨.

'South   Carolina'.split()
['South', 'Carolina']
  • join() 문자열 메서드: 지정된 문자열을 매개체로 이용하여 여러 문자열 이어붙이기

예를 들어, 스페이스(” “)를 기준으로 SouthCarolina를 이어붙이는 방법은 다음과 같다.

" ".join(['South', 'Carolina'])
'South Carolina'

쉼표와 스페이스(, )로 구분하고 싶다음 다음과 같이 한다.

", ".join(['South', 'Carolina'])
'South, Carolina'

위 두 작업을 처리하는 함수를 clean_ops가 가리키는 함수 리스트에 추가해야 한다. split() 문자열 메서드는 str.split 으로 지정하면 되지만, join() 문자열 메서드는 여기서 필요한 스페이스를 이어붙이기의 매개체로 지정해야 하기에 아래와 같이 함수를 새로 정의해서 사용한다.

def space_join(a_list):
    return " ".join(a_list)

이제 위 두 함수를 clean_ops 리스트에 추가한 후에 clean_string() 함수를 다시 실행하면 보다 깔끔하게 정돈된 문자열들의 리스트가 생성된다.

주의사항: clean_string() 함수는 전혀 수정하지 않는다.

clean_ops.extend([str.split, space_join])
cleaned_strings = clean_strings(states, clean_ops)

cleaned_strings
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South Carolina',
 'West Virginia']

6.5. 고계 함수#

함수가 제1종 객체이기 때문에 함수를 다른 함수의 인자 또는 반환값으로 사용할 수 있다.

예제

아래 함수는 인자로 들어오는 함수를 정수 1과 함께 호출하여 반환된 값에 1을 더한 값을 반환한다.

def higher_func1(f):
    return f(1) + 1

add_three() 함수는 입력값에 3을 더한 값을 반환한다.

def add_three(y):
    return 3 + y

higher_func1() 함수에 add_three를 인자로 사용하면 아래 결과가 나온다.

higher_func1(add_three)
5

예제

아래 함수는 숫자 x가 인자로 들어오면 그 x를 자신의 인자 y와 더한 값을 반환하는 함수 f()를 반환한다. 즉, 함수 내에서 다른 함수를 정의하고 그 함수를 반환값으로 지정한다.

def higher_func2(x):
    def f(y):
        return y + x
    return f

위 함수를 이용하여 add_three() 함수를 아래와 같이 정의할 수 있다.

add_three_new = higher_func2(3)

실제 실행 결과가 동일하다.

add_three_new(7) == add_three(7)
True
add_three_new(99) == add_three(99)
True

higher_func1()higher_func2() 처럼 함수를 인자로 받거나 반환하는 함수를 고계 함수higher-order function라 한다.

6.6. 람다 함수#

람다 함수는 이름 없이 정의된 함수를 의미한다. k개의 인자를 받는 함수를 람다 함수로 정의하는 양식은 아래와 같다.

lambda 변수1, 변수2, ..., 변수k: 반환값

주의사항: 반환값을 지정할 때 return 키워드가 사용되지 않는다.

예제

예를 들어 두 수의 합을 반환하는 함수는 다음과 같이 정의한다.

lambda x, y: x + y
<function __main__.<lambda>(x, y)>

람다 함수는 이름이 없기 때문에 사용하려면 항상 람다 함수 자체를 하나의 이름처럼 사용해야 한다. 예를 들어, 2와 4를 더하려면 다음과 같이 사용한다.

(lambda x, y: x + y)(2, 4)
6

예제

higher_func2() 함수의 본문에서 정의된 함수 f()는 함수의 리턴값으로만 사용되며 다른 곳에서는 전혀 사용할 수 없다. 이처럼 한 번만 사용할 함수를 정의하기 위해 굳이 이름을 줄 필요가 없으며, 함수가 간단하게 정의되기에 아래와 같이 람다 함수를 바로 반환하도록 하는 게 이해에 보다 도움이 된다.

def higher_func2(x):
    return (lambda y: y+x)

예제

고계 함수의 인자로 종종 람다 함수를 사용한다. 아래 함수는 리스트의 각 항목에 지정된 함수를 적용하여 새로운 리스트를 생성하는 함수이다.

def apply_to_list(f, some_list):
    return [f(x) for x in some_list]

각 매개변수의 역할은 다음과 같다.

  • some_list: 리스트를 인자로 받음.

  • f: 하나의 인자를 받는 함수를 인자로 받음.

반환값은 some_list의 각 항목값과 함께 f() 함수를 호출하여 반환되는 값들로 이루어진 리스트이다. 예를 들어, 각 항목의 두 배로 이루어진 리스트는 다음과 같이 생성한다.

ints = [4, 0, 1, 5, 6]

apply_to_list(lambda x: x * 2, ints)
[8, 0, 2, 10, 12]

예제

함수를 키워드 인자로 받는 경우에 람다 함수가 유용하게 활용된다. 예를 들어, 리스트의 sort() 메서드는 항목을 크기 순으로 정렬한다. 그리고 크기 기준의 기본으로 숫자인 경우는 숫자 크기, 문자열인 경우는 사전식 알파벳 순서와 같이 일반적으로 알려진 기준을 사용한다.

하지만, 예를 들어, 문자열의 크기 기준을 문자열에 포함된 서로 다른 문자들의 개수로 정하면 크기 기준을 다르게 적용해서 정렬해야 한다. 이때 sort() 메서드의 key 키워드가 크기 기준으로 사용되는 함수를 지정하는 데에 사용된다.

key 키워드의 인자는 하나의 인자만 사용하는 함수이어야 하며, 해당 함수의 반환값을 이용하여 크기 순서를 정하게 된다. 따라서 문자열의 길이를 기준으로 정렬하려면 아래 함수를 key의 인자로 사용해야 한다.

참고: set() 함수는 중복된 항목을 하나의 항목으로 처리한다.

def count_chars(x):
    return len(set(x))

아래 문자열들의 리스트를 새로운 기준으로 정렬해보자.

strings = ['foo', 'card', 'bar', 'aaaa', 'abab']

위 문자열들에 사용된 서로 다른 알파벳의 수는 아래처럼 확인할 수 있다. 아래 코드는 리스트 조건제시법을 사용한다.

[count_chars(x) for x in strings]
[2, 4, 3, 1, 2]

이제 key 매개변수의 인자를 count_chars로 지정하여 정렬하면 된다.

주의사항

count_chars() 라고 적지 않음에 주의하라. 만약, 그렇게 하면 count_chars 함수가 아니라 해당 함수를 호출하여 반환된 값이 key에 대한 키워드 인자로 사용된다. 하지만 count_chars 함수는 인자를 반드시 하나 받아서 호출되어야 하기에 오류가 발생한다.

strings.sort(key=count_chars)
strings
['aaaa', 'foo', 'abab', 'bar', 'card']

잘 작동한다. 하지만 count_chars() 함수가 한 번만 사용된다면 굳이 이름을 지정하여 정의할 필요 없이 람다 함수로 직접 key 매개변수의 인자로 지정하는 것이 좀 더 편하다.

strings.sort(key=lambda x: len(set(x)))
strings
['aaaa', 'foo', 'abab', 'bar', 'card']