파이썬 리스트 함수 만들기 - paisseon liseuteu hamsu mandeulgi


함수의 필요성 (추가 예정)

사실 함수의 필요성은 print, input, max, min을 함수로 해놓은 것만 봐도 쉽게 느낄 수 있습니다. 필요한 복잡한 기능을 언제나 어디서든지 사용하고 싶을 때에 사용할 수 있다는 치명적인 장점이 있는 함수입니다. 예제를 들어 설명하기엔 갑자기 귀찮으므로.. 일단 뒤로 미루고, 임시 예제를 살펴봅시다.


예제로 만들어 본 getMax

이미 파이썬 설계자가 만든 환상적인 max라는 함수가 있지만, 굳이굳이 설명을 위해 최대값을 구하는 (결함 많은) 함수를 만들어봅시다.

def getMax(numbers): ##a_1##
    result = -9999 ##a_2##
    for number in numbers:
        if result < number:
            result = number
    return result ##a_3##
# print(result, numbers) ##a_2##
examples = [4,1,7,5,8,3,1,3,1]
print(getMax(examples)) ##a_4##

결과는 다음과 같습니다.

8

a_1(1.) 설명

def getMax(numbers):

함수를 정의하는 부분입니다. def라는 생소한 키워드로 시작합니다. def는 define의 준말입니다. 이 함수는 numbers 라는 인수(argument) 를 취합니다. 그리고 콜론(:)으로 새로운 블록을 시작합니다.

인수는 함수의 호출 시점에서 항상 다르게 지정할 수 있습니다. 좀 더 다양한 상황에서 사용할 수 있도록 말이죠. 우리는 print 함수를 호출하면서 항상 다른 문자열 인수를 넣어주곤 했습니다. 그 인수를 정의하는 자리인 것입니다. 뒤에서 자세히 설명하도록 하겠습니다.

함수를 정의한다고 해서 프로그램이 실제로 어떤 동작을 하지는 않습니다. 함수의 실제 동작은 함수를 호출하는 시점에서 이루어집니다. 이것은 중요한 개념이라 뒤에서 다시 설명합니다.

a_2(2.) 설명

    result = -9999
(중략)
# print(result, numbers) #주석을 해제하면 아래와 같은 에러가 발생합니다.
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test.py", line 7, in <module>
    print(result, numbers)
NameError: name 'result' is not defined

함수를 정의할 때 numbers 인수가 쓰였다는 걸 기억하시나요? numbers함수 내에서만 사용되는 변수 입니다. 이를 변수의 범위(scope) 는 함수로 국한된다고도 표현합니다. 함수 밖에서 사용하려면 에러가 발생합니다.

result는 인수는 아닙니다. 함수 내부에서 정의한 변수입니다. 하지만 이 변수 또한 함수 내부로 범위가 국한됩니다. 그래서 함수 바깥 쪽에 있는 print에서 result를 이용하려고 하니 에러가 나버리게 됩니다.

왜 변수의 범위를 이렇게 제한할까요? 궁금하신가요? 그럼 좀 있다 다시 설명할게요.

a_3(3.) 설명

    return result

return 이라는 새로운 키워드가 등장합니다. return은 되돌려준다는 뜻입니다. 리턴한다, 반환한다 라고도 표현할 수 있습니다. 함수 내에서 return을 맞닥뜨리게 되면 함수는 즉시 끝나면서 return 바로 뒤에 따라오는 값을 호출하는 지점으로 되돌려줍니다.

다음과 같은 함수는 아무것도 반환(리턴)하지 않습니다.

  • 함수 내부에 return이 없는 경우
  • return 뒤에 아무런 값도 없이 덜렁 return만 있는 경우

a_4(4.) 설명

print(getMax(examples))

실제로 함수를 호출하는 부분입니다. 이제서야 getMax 함수가 동작하게 됩니다. 그리하여 함수의 numbers 인수에는 examples가 대입되어 들어갑니다. 즉 지금 getMax 함수를 지금 호출한 시점에서 함수 내부에서 numbers를 다루는 것은 examples를 다루는 것과 같다는 뜻입니다. 인수로서 examples가 아닌 다른 것을 넘겨준다면, 그 다른 것이 numbers가 되어 작동하게 되겠죠.

함수의 내부에서 return result 가 있었습니다. 그리하여 getMax 함수 호출의 결과값으로 result가 계산되어 반환됩니다. 마침내 그 값이 print 되는 것이죠.


기본적인 사용 방법

# 함수의 정의
def 함수명(인수명1, 인수명2, ...):
    내용
    return 무언가. # return 은 있어도 되고, 없어도 된다.

# 함수의 호출
함수명(인수1, 인수2, ...)

함수명과 인수명을 정할 때의 규칙은 변수명을 정하는 규칙과 동일합니다.

블록은 if, while과 동일하게 들여쓰기를 이용합니다. 해당 블록에 있는 모든 내용이 곧 함수의 내용입니다.

인수 란 함수에게 넘겨주는 값입니다. 함수명 바로 뒤로 이어지는 소괄호 안에 인수가 들어가게 됩니다. 인수는 다음과 같은 특성을 가집니다.

  • 인수는 쉼표(,)로 구분됩니다.
  • 인수의 개수는 제한이 없습니다. 하나도 취하지 않을 수도 있습니다.

인수매개변수, 인자, argument(아규먼트), parameter(파라미터) 등의 이름으로 불립니다. 미묘한 차이가 있지만 함수에 어떤 값을 넘겨준다는 의미에서 전부 큰 차이가 없으므로 일단은 넘어가되 용어의 존재만 확실하게 기억하고 가도록 합시다. 총 5가지 입니다.

함수 블록 내부에는 return 문이 삽입될 수 있습니다. return 의 특징은 다음과 같습니다.

  • 없어도 되고, 한번 써도 되고, 여러번 써도 됩니다.
  • return 을 맞닥뜨리는 순간 함수가 종료됩니다.
  • return 바로 뒤에 값이 있다면 그 값이 반환되고, 값이 없다면 아무것도 반환되지 않습니다.

함수는 호출(call) 해야 비로소 작동합니다. 호출에 대한 특징은 다음과 같습니다.

  • 호출하는 방법은 함수명을 적고 바로 뒤에 소괄호를 사용하면 됩니다.
  • 함수 호출시 인수가 순서대로 전달(pass) 됩니다.
  • 함수 호출시 정의했던 인수의 개수가 다르다면 에러가 납니다.

함수의 특징 (장점)

  • 변수명을 더 자유롭게 사용할 수 있습니다. (변수의 범위를 제한하는 이유)
  • 코드 작성의 효율이 높아집니다.
  • 가독성이 좋아집니다.
  • 작동 방식을 이해하기 위하여 코드를 직접 분석하는 일은, 필요 없습니다.

변수명을 더 자유롭게 사용할 수 있습니다. (변수의 범위를 제한하는 이유)

금방 전 함수의 특징 중 하나가 변수의 범위를 제한한다고 하였습니다. 그 이유는 그렇게 함으로써 변수명을 더 자유롭게 사용할 수 있기 때문입니다. 이게 무슨 말일까요? 범위를 제한하는 것이 오히려 더 자유롭게 사용할 수 있다니요?

함수의 밖과 안은 기본적으로 서로 격리되어 있습니다. 그렇기 때문에 서로에게 영향을 줄 수 없습니다. 즉 함수 밖에서 정의된 변수는 내부에서 접근할 수 없고, 내부에서 정의된 변수는 바깥에서 접근할 수 없습니다. 다른 말로 하자면 함수 내부에서 변수를 정의할 때 함수 외부의 이름과 겹치지 않을까 같은 걱정은 전혀 필요없다 는 뜻입니다. 반대의 상황도 마찬가지고요.

만약 다음과 같은 코드가 있다고 생각합시다.

result = "안녕?"
def getMax(numbers):
    result = -9999
    for number in numbers:
        if result < number:
            result = number
    return result
print(result)

사실 result라는 이름은 꽤 자주 쓰이는 단어입니다. 그래서 예제와 같이 이름이 중복될 가능성이 있습니다. 하지만 외부의 이름과 겹칠까에 대한 걱정은 전혀 필요가 없습니다. 함수 내부에서의 result는 오직 이 함수 안에서만 작동하고 외부의 result와는 전혀 연관이 없으니까요. 그래서 다음과 같이 외부의 result는 변동이 없다는 것을 확인할 수 있습니다.

안녕?

이렇듯 함수는 기본적으로 외부와 격리하려고 합니다. 하지만 함수 내부에서 외부의 값을 직접 변경하고 싶을 때가 있을 수 있습니다. 이는 심화 내용이므로, 아래쪽 더 나아가기 에서 확인해보세요.


코드 작성의 효율이 높아집니다.

사실 함수를 하나도 작성하지 않아도 프로그램을 작성할 수 있습니다. 하지만 함수는 다음과 같은 상황에서 아주 강력합니다.

  • 동일한 (비슷한) 기능을 여러 번 사용해야 할 때
  • 여러 곳에서 쓰인 동일한 (비슷한) 기능을 한꺼번에 수정해야 할 때

완전히 같은 기능만 함수로서 만들어야 할까요? 그렇지 않습니다. 함수를 만들 때 미묘하게 다른 기능에 대해서는 바로 인수를 통해서 기능을 쉽게 세분화시킬 수 있기 때문이죠. 이로 인하여 함수 하나를 정의하였다 하더라도 인수를 다양한 방식으로 조합하여 호출할수 있다는 말이죠. 함수 호출 시 매번 같은 점과 다른 점은 다음과 같습니다.

같은 점다른 점
함수의 용도와 이름, 인수의 위치 및 용도 인수가 가지는 값

가독성이 좋아집니다.

함수는 정의하는 부분과 실제로 사용하는 부분(호출)이 나뉘어져 있습니다. 프로그램은 앞서 얘기한 것처럼 위에서부터 아래로 차례대로 진행되는데요, 함수 호출을 만나게 되면 프로그램 흐름은 그 함수의 정의된 부분으로 순간이동합니다. 함수가 return 문을 만나거나 끝까지 실행을 다 하게 되면 호출한 부분으로 다시 돌아와 원래의 진행으로 돌아옵니다. 프로그램 흐름이 여기로 갔다 저기로 갔다 하니 좀 헷갈리게 되는 건 아닐까요?

전혀요. 헷갈리지 않습니다. 왜냐하면 우리는 함수 내부에 대해서는 별 생각을 안하고 있기 때문입니다. 우리가 print 함수를 사용할 때 내부가 어떻게 되었는지 고민해본 적이 있나요? 파이썬 인터프리터는 print 함수의 호출을 만나는 순간 파이썬이 언어 설계자가 만들어놓은 print 함수가 정의한 내용으로 순간이동하여 적절한 작업을 하겠지요. 하지만 뭐, 상관없습니다. print 함수는 항상 말썽없이 잘 작동하기 때문입니다. 만약 에러가 발생한다면 우리는 print 함수의 문제가 아니라 우리의 인수에 대해 문제가 있는지를 살펴봅 것입니다.

결론은 이렇습니다. 코드를 기능 별로 구분해놓으니 일관된 논리에 따라 코드를 작성할 수 있어 가독성이 크게 향상됩니다.


작동 방식을 이해하기 위하여 코드를 직접 분석하는 일은, 필요 없습니다.

일반적으로 함수의 다음 네 가지 요소를 안다면 함수의 실제 내용을 몰라도 곧장 이용할 수 있습니다.

  • 이름은 무엇인가? (호출하려면 어떻게 해야 하는가?)
  • 어떤 역할인가? 어떤 역할을 하는가?
  • 어떤 인수를 받는가? 각 인수의 용도는 무엇인가?
  • 결과값(return)이 존재하는가? 있다면, 어떤 값인가?

코드 몇 줄 단위보다는 함수 하나하나에 대해 문서를 작성하는 것이 작성하는 사람에게도, 그 기능을 이용하려는 사람에게도 아주 편합니다. 이용하려는 사람은 굳이 소스 코드를 뜯어보지 않아도 그 함수에 대한 문서를 읽고 적절하게 사용하면 됩니다. 즉 내부가 어떻게 동작하는지 알 필요가 없다는 것 인데요, 이런 개념을 캡슐화라고 합니다.

물론 문서가 없는 함수는 직접 코드를 뜯어봐야 알 수 있겠지요. 함수를 만들 일이 있다면 이름을 기억하기 좋게 만들거나 짧은 설명 한 줄 정도는 미래의 나 를 위해 써놓읍시다.


더 나아가기

공유에 의한 전달 (pass by sharing)

우리는 함수를 호출할 때 변수로 호출할 수도 있습니다. 그럼 다음과 같은 코드는 어떻게 작동할까요?

def plus(number):
    number += 1
po = 10
plus(po)
print(po)
10

얼핏 생각하면 인수로 po를 넘겼으니 po에 대해서 수정이 일어날 것 같습니다. 하지만 po에게는 아무런 일도 일어나지 않습니다. po 또한 함수 입장에서는 바깥 변수이기 때문에 영향이 가지 않습니다. numberpo의 값을 가지고 있었지만 po와는 다른 존재입니다.

객체와 클래스까지 배우게 된다면, 인수로 넘겨질 때의 특정한 메커니즘이 발동해서, 인수로 넘겨진 본체가 수정이 가해질 수도 있는데, 그 특정 메커니즘을 공유에 의한 전달(pass by sharing)이라고 하고, 이것에 대한 설명은 다음 시간 언젠가 하도록 합시다.

(추가 예정)

global, nonlocal

함수 내부에서 외부의 값을 수정하고 싶을 때도 있다고 했습니다. 그럴 때는 global 키워드를 사용하여 "이 변수는 외부에서 정의한 것이라도 갖다 쓰겠다!" 라고 파이썬 인터프리터에게 일방적으로 통보하면 됩니다. 다음 예시를 봐주세요.

def getOld():
    global boy
    boy += 1
boy = 10
getOld()
print(boy)
11

global boy로 나는 외부의 boy를 가져다 쓰겠다고 파이썬 인터프리터에게 입장을 분명히 했습니다. 하지만 입장을 분명히 한다고 해서 특별히 무슨 일이 벌어지는 것은 아닙니다. 실제 getOld 함수가 호출되고 boy를 수정하려는 시도인 boy += 1에 와서야 외부에 정의된 boy를 탐색합니다. 함수가 정의되는 시점에는 boy가 없어도 되지만 함수가 호출되는 시점에 boy가 없다면 에러가 나리라는 것은 당연하겠지요?

global을 간략하게 알아보았는데, 이런 global과 비슷한 nonlocal이라는 키워드가 있습니다. 이는 변수의 범위(scope)에 대한 자세한 내용을 설명할 때 함께 이야기하도록 하겠습니다.

(추가 예정)


연습 문제

  • 함수의 특징 (장점) 크게 네 가지는 무엇인가?
  • 인수를 뜻하는 다른 말 네 가지는 무엇인가?
  • 함수 정의시, 인수 여러 개를 사용하고자 할 때 그들의 구분은 어떻게 하는가?
  • 함수 정의시, 인수를 하나도 사용하지 않아도 되는가?
  • 내부가 어떻게 동작하는지 알 필요가 없다는 개념을 무엇이라고 하는가?
  • 함수의 실제 내용을 몰라도 ‘이것’들만 알면 함수를 곧장 이용할 수 있다는데, ‘이것’ 네 가지는 무엇인가?
  • 함수 내부에서 외부의 값을 수정하고자 한다면 어떻게 해야 하는가?
  • 함수를 호출하는 방법은 무엇인가?

프로그래밍 문제

아래 함수를 정의하여, 기능이 제대로 동작하는지 테스트해보세요.

  1. 인수를 하나도 받지 않고, "왈왈"이라고 출력하는 bark 함수를 만들어보세요. 리턴 값은 없습니다.

  2. 숫자 하나를 인수로 받으세요. 그리고 이 숫자의 모든 약수를 출력하는 함수를 만들어보세요. 리턴 값은 없습니다.

  3. 숫자 하나를 인수로 받으세요. 그리고 이 숫자의 모든 약수를 담은 리스트를 반환하는 (리턴하는) 함수를 만드세요. 이 함수는 아무것도 출력하지 않습니다.

  4. 숫자 두 개를 입력받고, 이들의 중간 값을 반환하는 함수를 만드세요. 이 함수는 아무것도 출력하지 않습니다.


프로그래밍 문제 정답

  1. 코드입니다.

    def bark():
        print('왈왈')
    
    bark()
    
    왈왈
    
  2. 코드입니다.

    def ali(num):
        for i in range(1, num+1):
           if num % i == 0:
                print(i)
    
    ali(192)
    
    1
    2
    3
    4
    6
    8
    12
    16
    24
    32
    48
    64
    96
    192
    
  3. 코드입니다.

    def ali1(num):
        numbers = []
        for i in range(1, num+1):
            if num % i == 0:
                numbers.append(i)
        return numbers
    
    def ali2(num):
        return [i for i in range(1, num+1) if num % i == 0]
    
    print(ali1(192))
    print(ali2(192))
    
    [1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 192]
    [1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 192]
    

    ali1 단계만 구현해도 성공하셨습니다! 축하드립니다. ali2는 리스트 컴프리핸션(list comprehension)(추가 예정)을 이용한 것입니다. 추후 다시 내용을 다뤄보도록 하겠습니다.

  4. 코드입니다.

    def middle(num1, num2):
        return (num1 + num2)/2
    
    print(middle(12, 21))
    
    16.5