본문 바로가기
프로그래밍/Python

[Python/파이썬] 함수 / 람다표현식(Lambda Expression)

by 별준 2020. 8. 21.
- 참고 문헌 및 사이트
https://docs.python.org/3/
https://wikidocs.net/book/1

이번 글에서 함수를 정의하는 다양한 방법과 함수의 DocString, Function Annotation 그리고 Lambda 표현식에 대해서 알아보겠습니다.

함수란

 

wikipedia

 

함수란 입력값을 가지고 어떠한 과정을 거쳐서 결과물을 내어놓는 것입니다.

수학에서 \(y = 5x + 2\)와 같은 식도 함수이죠. 

프로그래밍에서 함수는 어떻게 사용될까요?

코딩을 하다보면 똑같은 내용을 반복해서 작성할 때가 종종 있습니다. 이때 바로 함수가 필요하게 됩니다.

함수는 반복 및 재사용이 가능한 프로그램의 조각이며, 반복되는 일들을 특정 블록의 덩어리에 정의해서, 필요할 때마다 그 볼록이 포함된 명령들을 실행할 수 있도록 하는 것입니다.

전의 게시글에서 len()이나 range() 같은 것들을 함수라고 일컷습니다.

 

함수 정의하기

피보나치 수열을 임의의 수까지 출력하는 함수를 예제로 살펴봅시다.

>>> def fib(n):
...	"""Print a Fibonacci series up to n"""
...	a, b = 0, 1
...	while a < n:
...		print(a, end=' ')
...		a, b = b, a+b
...	print()
...
>>> # Now call the function we just defined:
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

 

위와 같이 함수는 키워드 def로 정의를 시작합니다. def 다음으로는 함수의 이름과 매개변수(parameter)의 목록이 괄호 안에 포함됩니다. 함수의 바디는 정의 다음 라인에서 시작되고, 꼭 들여쓰기를 해야합니다.(파이썬은 들여쓰기로 볼록을 구분합니다)

 

여기서 함수의 첫번째 라인은 문자열인데, 이 문자열 리터럴은 함수의 docstring이고, 주로 함수를 설명하는 코멘트를 달아놓아서 자동으로 열람할 수 있도록 되어있습니다. 이는 뒤에서 다시 설명하도록 하겠습니다.

 

이전글에서 변수를 설명하면서 심볼 테이블에 대해서 이야기를 한 적이 있습니다.

(이전글 : 2020/08/11 - [Language/Python] - [Python/파이썬] 파이썬의 변수에 대해서(+리터럴 상수))

 

함수를 정의하고, 함수를 실행하게 되면 함수의 지역 변수들을 위한 새 심볼 테이블이 생성됩니다. 그래서 함수에서의 모든 객체는 지역 심볼 테이블에 저장됩니다. 그리고 함수 내에서 변수를 참조할 때, 먼저 지역 심볼 테이블에서 해당 변수를 찾고, 지역 심볼 테이블에 없다면 전역 심볼 테이블을 찾아보고, 마지막으로 내장된 build-in names(내장 함수 등)을 찾습니다. 다만, 참조는 될 수 있더라도, 전역 변수나 다른 함수의 지역 변수는 해당 함수 내에서 직접 값이 대입될 수 없습니다.(전역 변수를 global문으로 명시하거나 다른 함수의 지역 변수를 nonlocal문으로 명시하지 않는 이상은 불가능합니다)

+)참조는 할 수 있어도 변경이 불가능하다는 것은 다음 예제에서 확인이 가능합니다.

 

 

전역변수 i에 10을 대입하고, change_value라는 함수에서 a라는 인자값을 i에 대입합니다. 이때, change_value함수 내의 i 변수는 지역변수이고, 전역변수가 변경되지는 않습니다. 그래서 인터프리터에서 함수 실행이 끝나고 i값을 확인해보면 전역변수 값이 그대로 있다는 것을 볼 수 있습니다.

 

 

하지만 함수내에서 global문으로 명시적로 전역변수를 가리킨다는 정보를 준다면, 전역변수 i를 함수 내에서 수정이 가능합니다.

 

 

함수 호출로 전달되는 실제 매개변수들, 즉 인자(argument)들은 호출될 때 호출되는 함수의 지역 심볼 테이블에 만들어집니다. 함수가 다른 함수를 호출할 때는 그 호출을 위한 새로운 지역 심볼 테이블이 만들어지게 됩니다.

 

심볼 테이블은 globals()나 locals()로 확인할 수 있습니다.

 

 

현재 fib 함수 안에서 print(locals())로 fib 함수의 로컬 심볼 테이블을 확인할 수 있습니다. 안에서 사용되는 변수는 매개변수 n과 지역변수 a, b입니다. 

그리고 print(globals())를 통해서 전역 심볼 테이블을 확인할 수 있습니다. 내장되어 있는 변수나 함수들이 있고, 사용자가 정의한 fib도 있는 것을 확인할 수 있습니다.

 

함수 정의는 함수 이름을 현재 심볼 테이블의 함수 객체와 연결합니다. 다른 이름을 사용해서 같은 객체를 가리키게 할 수도 있습니다.

 

 

 

함수의 반환 Return

함수를 설명할 때, 결과값을 내어놓는 것이라고 했습니다. 파이썬의 함수는 return문을 통해서 함수의 결과값을 반환할 수 있습니다. 매개변수 a, b를 받아서 두 매개변수의 합을 리턴하는 함수를 살펴보자.

def add(a, b):
    return a+b

그리고 함수는 return문을 만나면 return 다음의 나타나는 값을 반환하고 함수는 종료됩니다.

def add_and_mul(a, b):
    return a+b
    return a*b

위와 같은 함수를 정의해서 실행해보면, 결과값은 항상 a+b로만 나오게 됩니다. return a+b를 만나고 값을 반환한 후에 함수를 빠져나왔기 때문이죠.

 

하지만 앞서 정의한 fib함수는 따로 결과값을 내어놓지는 않고 단순히 print만하고 있습니다. 

return문이 없어서 결과값을 반환하지 않는 것처럼 보이지만 사실 None이라는 값을 반환하고 있습니다(함수의 끝에 도달하면 None을 반환합니다). 

만약 None이 함수의 출력이라면 보통 인터프리터가 알아서 None은 출력하지 않습니다. 확인이 필요하다면 print()를 통해서 확인할 수 있습니다.

 

 

앞서 피보나치 수열을 나타내는 함수 대신에 피보나치 수열의 숫자들을 리스트에 담아서 반환하는 함수도 가능합니다.

def fib2(n):
    """Return a list containing the Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

실행결과:

 

 

 

함수를 정의하는 다양한 방법

함수는 매개변수의 기본값을 지정하거나 정해지지 않은 개수의 매개변수들로 정의하는 것이 가능합니다.

 

1. 매개변수 기본값 지정

아래와 같은 함수를 정의해봅시다.

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)
        

위의 함수는 아래와 같은 여러가지 방법으로 호출이 가능합니다.

# prompt='Do you really want to quit?'
ask_ok('Do you really want to quit?')
# prompt='OK to overwirte the file?', retries=2
ask_ok('OK to overwirte the file?', 2)
# prompt = 'OK to overwirte the file?', retries=2, reminder='Come on, only yes or no!'
ask_ok('OK to overwirte the file?', 2, 'Come on, only yes or no!')

꼭 필요한 인자만 전달하거나(Line 2), 선택적인 인자 하나만 추가로 전달하거나(Line 4), 모든 인자를 전달(Line 6)해서 함수를 호출할 수 있습니다. 

 

함수 매개변수의 기본값을 지정할 때, 만약 기본값이 지정되어 있지 않은 매개변수와 같이 사용한다면 기본값이 지정되는 매개변수가 무조건 기본값이 지정되지 않은 매개변수의 뒤에 위치해야합니다. 이는 C++ 규칙과 동일합니다.

예를 들어서 아래와 같은 함수를 정의한다면 SyntaxError가 발생합니다.

# this is error code
def ask_ok(retries=4, prompt, reminder='Please try again!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

실행결과:

 

 

non-default argument(기본값이 없는 인자)가 default argument(기본값이 있는 인자) 뒤에 위치하여서 에러가 발생합니다.

 

또한, 기본값은 함수가 정의되고 있는 시점에서 정해지게 됩니다.

i = 5

def f(arg=i):
    print(arg)

i = 6
f() # print 5

따라서 위의 코드에서 함수 정의 시점에 i는 5를 가지고 있기 때문에, 함수 정의 후에 i 값을 변경해도 함수 f의 출력은 5로 동일합니다.

 

하지만, 함수의 기본값이 변경이 가능한 리스트나 딕셔너리 객체일 경우에는 주의해야합니다.

함수의 기본값은 함수를 정의할 때 한번만 구한다고 했었고, 따라서 매개변수 L은 함수 정의 시점에 리스트 객체를 가리키게 됩니다. 그리고 다음과 같이 함수를 호출할 때 호출로 전달된 인자(a)를 누적하게 됩니다.

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

실행결과:

 

 

 

2. 키워드 인자 Keyword Arguments

함수는 keyword = value 형식의 Keyword Arguments를 매개변수로 사용해서 정의/호출할 수 있습니다.

다음과 같은 함수를 살펴봅시다.

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

여기서는 하나의 필수 인자(voltage)와 세 개의 선택 인자(state, action, type)을 전달받습니다. 

위 함수는 아래와 같은 방법으로 호출될 수 있습니다.

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

그리고 아래와 같은 호출 방법은 불가능합니다.

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

함수를 호출할 때, 키워드 인자(Keyword argument)는 위치 인자(Positional argument) 뒤에 나와야 합니다. 모든 키워드 인자는 함수가 받아들이는 매개변수와 일치해야 하며(아래 Line 4: actor는 parrot의 매개변수가 아님), 순서는 중요하지 않습니다(위 Line4 : action이 voltage보다 먼저나옴). 그리고 어떤 인자도 두 개 이상의 값을 받을 수 없습니다(아래 Line 3).

 

3. 임의의 개수의 매개변수 지정하기

함수에 임의이 개수의 매개변수를 지정할 때, *와 **을 사용해서 VarArgs 매개변수를 사용할 수 있습니다. *name 형식은 튜플형으로 전달되고, **name 형식은 딕셔너리형으로 전달됩니다.

아래 함수를 살펴봅시다.

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

위 함수는 하나의 필수 인자와, 임의의 개수를 담은 튜플, 임의의 개수의 키워드 인자를 전달받습니다.

아래와 같이 호출될 수 있습니다.

cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

실행결과:

 

 

여기서 cheeseshop함수는 kind = "Limburger", arguments = ("It's very runny, sir.", "It's really very, VERY runny, sir."), keywords = {'shopkeeper':"Michael Palin", 'clinet':"John Cleese", 'sketch':"Chees Shop Sketch"} 를 인자로 전달받습니다.

 

4. 특수 매개 변수

파이썬은 인자가 위치나 명시적인 키워드로 함수의 매개변수로 전달될 수 있기 때문에, 때로는 가독성이 떨어질 수 있습니다. 파이썬에서는 개발자들이 매개변수가 위치, 위치나 키워드 또는 키워드로 전달되는지 판단할 때 함수 정의만 참고하면 되도록 선택적으로 사용될 수 있는 방법을 고안했고, 이 방법은 파이썬 3.8 이상에서 가능합니다.

함수 정의를 /와 *을 포함해서 다음과 같이 해봅시다.

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
    pass
    

여기서 / 와 * 은 선택적으로 사용이 가능하고, 필수는 아닙니다. 

위 함수에서 

pos1, pos2 : positional only 위치전용 매개변수

pos_or_kwd : positional or keyword 위치-키워드 매개변수

kwd1, kwd2 : keyword only 키워드전용 매개변수

로 사용이 가능합니다.

위치 전용 매개변수는 / 앞에 놓이며, / 다음의 매개 변수는 위치-키워드 매개변수나 키워드전용 매개변수가 올 수 있습니다.

키워드 전용 매개변수는 키워드 전용 매개변수 바로 앞에 * 이 위치하면 됩니다. 

만약 / 나 * 이 없으면 매개변수는 위치나 키워드 인자로 함수에 전달할 수 있습니다.

def standard_arg(arg):
    print(arg)


def pos_only_arg(arg, /):
    print(arg)
    

def kwd_only_arg(*, arg):
    print(arg)
    

def combined_example(pos_only, /, standard, *, kwd_only):
    print(pos_only, standard, kwd_only)
    

def other_example(pos_only, /, *, kwd_only):
    print(pos_only, kwd_only)

각각의 실행결과는 아래와 같습니다.

 

 

Standard_arg는 호출 규칙에 아무런 제한을 두지 않아서 위치나 키워드로 전달될 수 있습니다.

하지만 두번째 함수는 정의에 /가 있으므로 위치 매개변수만 사용하도록 제한합니다. 따라서 TypeError가 발생하게 됩니다.

 

 

 

세번째 함수는 정의에 *가 있으므로 키워드 인자만 허용합니다.

 

 

 

네번째 함수는 세 가지 호출 규칙을 모두 사용합니다.

마지막 함수는 네번째 함수에서 standard arg만 제거한 형태입니다.

 

 

위치 인자 a와 a를 Keyword로 가지는 경우

아래와 같이 위치 인자 매개변수와 키워드 매개변수를 가지는 경우에 함수 호출 시에 동일한 키워드가 사용될 수 있습니다. 

def foo(name, **kwds):
    return 'name' in kwds

위 경우에는 위치인자 name과 name을 키로 가지는 kwds 사이에서 잠재적인 충돌이 있을 수 있습니다.

호출결과:

 

 

그러나 / (위치전용인자)를 사용하면, name을 위치 인자로, 동시에 'name'을 키워드 인자의 Keyword로 사용할 수 있게 됩니다.

def foo(name, /, **kwds):
    return 'name' in kwds

foo(1,  **{'name':2})

실행결과:

 

 

 

 

인자 목록 Unpacking

인자들이 리스트나 튜플에 있지만, 분리된 위치 인자들을 요구하는 함수 호출을 위해 Unpacking을 해야하는 상황이 있습니다. 

>>> list(range(3, 6))
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))
[3, 4, 5]

range 함수는 별도의 start와 stop을 인자로 받을 수 있고, 리스트를 언패킹하기 위해 * 연산자를 사용해서 range 함수를 호출할 수 있습니다.

 

동일하게 딕셔너리도 ** 연산자를 사용해서 키워드 인자를 전달할 수 있습니다.

>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

 

DocString : Documentation 문자열

DocString은 우리가 만든 함수를 알아보기 쉽게 해주고, 또 함수에 대한 설명서를 작성할 때 유용하게 사용됩니다. 

위에서 보통 함수에 대한 설명을 적는다고 했는데, 아래와 같은 예시를 봅시다.

def my_function():
    """Do nothing, but document it.
    
    No, really, it doesn't do anything.
    """
    pass
    
print(my_function.__doc__)

실행결과:

 

 

위와 같이 사용되며, 프로그램이 실행 중일 때에도 __doc__ 메서드를 통해서 읽어올 수 있습니다.

DocString에 권장되는 규칙(관례)이 있는데, 첫 줄은 항상 객체의 목적을 짧고, 간결하게 요약하고 객체의 이름이나 형은 다른 방법으로 제공되기 때문에 언급하지 않아도 됩니다. 첫 줄은 대문자로 시작하고 마침표로 끝나야 합니다. 만약 DocString이 여러줄이라면 두번째 줄은 시각적으로 나머지 설명과 분리하기 위해서 비워두어야 합니다. 뒤따르는 줄은 문단으로 객체 호출의 Rule, Side Effect 등을 설명해야 합니다.

자세한 사항은 다음 사이트를 참조하기 바란다 : https://www.python.org/dev/peps/pep-0257/

DocString은 모듈이나 클래스에 동일하게 적용됩니다.

 

 

Function Annotation (python3 이상에서 사용가능)

전에 언급했었지만, 파이썬은 문법적으로 자유도가 높지만 그로인해 발생하는 불편함들이 많습니다. 특히 변수나 함수 사용에서 자료형에 대한 선언이 없이 자유롭게 사용이 가능하기 때문에 작성된 코드를 볼 때 명확하게 해석하기 어려운 부분이 있습니다.

Function Annotation은 사용자 정의 함수가 사용하는 형들을 명시적으로 나타내서 불편함을 줄이기 위해서 사용하는 방법 중 하나입니다.

 

아래 함수 정의를 살펴봅시다.

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

Annotation은 함수의 __annotations__의 attribute에 딕셔너리형으로 저장되며, 함수의 다른 부분에는 아무런 영항을 끼치지 않습니다. 매개변수의 annotation은 매개변수의 이름 뒤에 콜론(:)으로 정의되고, return값의 annotation은 '->'로 정의됩니다. return값의 annotation의 위치는 함수 매개변수 목록과 def문의 끝인 콜론(:) 사이에 위치합니다.

위의 예에서는 위치 인자(ham), 키워드 인자(eggs = 'eggs')와 return값의 자료형이 annotation 됩니다.

다만, annotation은 강제성이 없기 때문에 annotation과 다른 자료형을 인자로 전달해도 함수는 정상적으로 동작합니다.

 

람다표현식 Lambda Expression

lambda는 보통 짧고 간결한 함수를 한줄로 나타내기 위해 사용합니다. 즉, 작고 이름이 없는 함수를 만드는 것입니다. lambda 키워드를 사용해서 작성하며, 일반적인 함수 정의를 편의성을 위해 다르게 적용한 문법입니다. 

아래와 같이 사용될 수 있습니다.

 

'lambda 매개변수1, 매개변수2, ... : 표현식' 으로 작성이 가능합니다.

>>> add = lambda a, b: a+b
>>> result = add(3, 4)
>>> print(result)
7

a, b를 인자로 받아서 더한 값을 반환하는 lambda 함수

다음과 같은 방법으로도 사용할 수 있습니다.

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

 

 

 

댓글