파이썬 프로그래밍 기초 2부 3편

주요 내용

함수

함수는 지정된 코드에 이름을 주어 필요할 때 간편하게 재사용할 수 있도록 도와주는 수단이다.

아래 함수는 셋째 인자, 즉, 키워드 인자를 1보다 큰 값으로 지정할 때와 아닐 때를 구분하여 다른 값을 계산하여 반환한다. 만약에 키워드 인자를 따로 지정하지 않으면 기본값이 1보다 크기에 2배 연산이 사용된 값이 반환된다.

네임 스페이스와 스코프

함수는 전역 변수와 지역 변수 모두 사용할 수 있다.

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

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

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

global 예약어를 사용하면 다르게 작동한다.

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

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

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

globals()

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

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

여러 개의 값 반환하기

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

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

제1종 객체와 함수

변수를 선언할 때 또는 함수의 인자 및 반환값 등으로 사용되어 저장 및 변경이 가능한 객체를 제1종 객체(first-class object)라 한다. 정수, 부동소수점, 문자열, 리스트, 튜플, 사전 등이 대표적인 제1종 객체이다. 그런데 파이썬에서는 함수도 제1종 객체이다. 앞서 파이썬에서 다루는 모든 값은 객체라고 하였는데 함수 역시 객체로 정의된다.

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

예제

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

연습문제

'Georgia'처럼 중복 사용된 리스트의 항목을 제거하는 방법은 무엇인가?

견본 답안 1

리스트에서 중복 항목을 제거하는 가장 간단한 방법은 집합 자료형으로 형변환했다가 다시 리스트로 형변환을 실행하는 것이다. 즉, 다음과 같이 실행한다.

견본 답안 2

리스트에서 집합으로, 집합에서 리스트로 형변환하면 기존 리스트에 포함된 항목들의 순서가 반드시 지켜지지 않는다. 이유는 집합 자체에 순서 개념이 없기 때문이다. 따라서 기존 리스트에 포함된 항목들의 순서를 유지하려면 for 반복문을 사용하는 것 이외에 달리 특별한 방법이 없다.

고계 함수

함수가 제1종 객체이기 때문에 함수를 다른 함수의 인자 또는 반환값으로 사용할 수 있다. 예를 들어, 아래 함수는 인자로 들어오는 함수를 정수 1과 함께 호출하여 반환된 값에 1을 더한 값을 반환한다.

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

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

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

람다 함수

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

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

주의사항: 반환값을 지정할 때 return 예약어가 사용되지 않는다.

예제

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

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

예제

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

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

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

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

예제

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

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

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

참고: apply_to_list() 함수는 파이썬에서 기본으로 제공하는 map() 함수의 일부 기능을 구현하였다. 단, map() 함수의 반환값은 리스트가 아닌 map이라는 이터러블 객체다. 이터러블 객체에 대한 자세한 설명은 잠시 뒤에 이루어진다.

new_ints 에 담긴 항목을 확인하려면 리스트로 형변환하면 된다.

물론 이터러블 자료형의 특성을 for 반복문에 적용해도 된다.

참고: new_ints와 같은 이터러블 자료형은 for 반복문에 한 번 사용되면 더 이상 사용될 수 없다. 아래 결과에서 아무 것도 출력되지 않는 이유가 거기에 있다. 이유는 map과 같은 이터러블 자료형은 포함된 항목을 한 번만 순회하도록 만들어졌기 때문이다. 단, 리스트보다 메모리를 훨씬 덜 차지하고 속도도 빠르다는 장점이 있어서 매우 많은 항목을 한 번 순회하는 용도로 많이 사용된다.

map() 함수는 여러 개의 인자를 받는 함수도 사용할 수 있다. 단, 인자의 개수만큼의 리스트가 인자로 사용되어야 한다. 그러면 각 리스트의 동일한 인덱스에 위치한 값을 묶어 첫째 인자로 사용된 함수의 인자로 전달된다.

리스트의 길이가 다르면 가장 짧은 리스트의 항목 수만큼만 함수를 적용한 결과를 리스트로 반환한다.

예제

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

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

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

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

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

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

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

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

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

부분 적용 함수

앞서 다룬 add_three() 함수는 add_numbers() 함수의 첫째 인자를 3으로 고정한 함수로 볼 수 있다. 즉 아래 관계가 성립한다.

add_three(y) == add_numbers(3, y)

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

주의사항: add_three() 라고 선언하지 않음에 주의하라.

위 식은 엄밀히 말하면 add_numbers() 함수와 람다 표현식을 이용하여 변수 add_three를 선언하는 과정이다. 하지만 변수 add_three를 바로 함수처럼 사용하면 된다.

이처럼 주어지 함수의 일부 인자를 특정 값으로 지정하는 방식으로 정의되는 함수를 원래 함수의 부분 적용 함수(partilly-applied functions)라 한다. 즉, add_three() 함수는 add_numbers() 함수의 부분 적용 함수이다.

참고: 일부 참고 문헌에 부분 함수라고 번역하였지만 옳지 않은 표현이다. 부분 함수(partial function)는 전혀 다른 의미로 사용되는 개념이다.

functools 모듈의 partial() 활용하기

앞서 설명한 부분 적용 함수 기능은 기존에 정의된 함수를 활용하는 유용한 기법이다. 람다 함수를 이용하는 방식 보다 편하게 부분 적용 함수를 정의하는 것을 도와주는 도구가 있다. 바로 functools 모듈의 partial() 함수이며, 기본 사용법은 다음과 같다.

partial() 함수를 이용하여 여러 인자가 미리 부분 적용된 함수도 정의할 수 있다.

add_3_5() 함수를 정의할 때 미리 지정된 인자는 위치상 처음 두 개의 인자였다. 그런데 아래처럼 첫째, 셋째 인자를 미리 지정하여 부분 적용 함수를 정의할 때는 상당히 조심해야 한다.

둘째 인자를 지정하기만 하면 되는데 아래와 같이 실행하면 오류가 발생한다.

오류가 발생하는 이유는 기존 함수의 매개변수가 선언된 순서대로 인자가 전달되기 때문이다. 즉, 4가 y가 아닌 x에게 전달되어, x에 대한 인자가 두 번 지정되는 오류가 발생하게 된다. 해결책 중에 하나는 매개변수를 키워드로 사용하는 것이다.

하지만 이 방식은 매개변수를 기억해야 한다는 점에서 매우 불편하다. 이럴 때는 그냥 람다 함수를 이용하여 부분 적용 함수를 정의하는 게 보다 좋다.

이제 키워드 없이 바로 사용할 수 있다.