본문 바로가기

진리는어디에/Python

[Python] 함수 #5 함수 객체

파이썬에서는 모든 것이 객체로 취급 된다고 이야기 했다. "함수" 역시 예외가 아니다.
오늘은 "파이썬에서는 함수도 객체"다 라는 주제를 가지고 이야기를 풀어 보겠다.

목차

  1. 함수 소개
  2. 디폴트 파라미터 주의 사항
  3. 파라미터 언패킹
  4. 파라미터 패킹
  5. >> 함수 객체
  6. 일급 객체(first class object)
  7. 람다(lambda)

함수도 객체다

파이썬에서는 모든 것을 객체로 관리한다고 했다. 파이썬 함수 역시 객체다. 자세한 설명은 뒤로 미루고, "함수는 객체다"라는 것만 기억하며 아래 예제 부터 보도록 한다.

import sys

def add(x, y) :
    return x + y
    
print(hex(id(add)))          # 함수 객체의 주소
print(sys.getrefcount(add))  # 함수 객체의 레퍼런스 카운트
print(sys.getsizeof(add))    # 함수 객체의 사이즈

f = add      
print(f(10, 20))

add = 10
print(add)
  • 6라인 : 함수 객체의 주소를 출력 한다.
  • 7라인 : 함수 객체의 레퍼런스 카운트를 출력한다. 실제 출력해보면 1이 아닌 2가 나오는데, 이는 getrefcount() 함수에서 내부적으로 객체의 레퍼런스 카운트를 1 증가 시키기 때문이다.
  • 8라인 : 함수 객체의 사이즈를 출력한다.
  • 10라인 : 함수의 이름은 함수 객체를 가리키는 변수일 뿐이다.
    변수 f에 add가 가리키는 함수 객체의 주소를 넘겨줬다. 이제 부터 f는 add 함수와 동일하다.
  • 13라인 : 함수의 이름에 10을 할당했다. 이제 add는 정수형 변수다.

위 예제에서 알 수 있듯이 파이썬은 일반 변수 f에도 함수를 대입하여 호출 할 수 있고, add와 같이 함수로 선언 되었다고 하더라도 일반 변수가 되어 정수값을 저장 할 수 있다. 또한 함수 이름을 통해 함수 객체의 주소를 출력하거나 레퍼런스 카운트를 조사하는 것도 일반 변수와 동일하다.

파이썬에서 함수의 이름이란 함수 객체를 가리키고 있는 변수일 뿐이다.

함수의 이름이든, 변수의 이름이든 단지 객체를 가리킬뿐..

어떻게 함수가 객체일수 있을까?

위에서 우리는 add는 단지 함수 객체를 가리키는 변수일 뿐이라고 했다. 실제로 우리가 함수를 선언하게 되면, 파이썬은 내부적으로 아래와 같은 함수를 관리하기 위한 PyFunctionObject 객체를 생성하게 된다.

PyFunctionObject

함수 객체에는 다른 파이썬 객체들과 마찬가지로 PyObject에서 파생되어 나온 레퍼런스 카운트(ob_refcnt)와 객체 타입을 가리키는 포인터(ob_type)를 가지고 있고, 그외 함수 객체에 필요한 별도의 속성들 - 이 속성 값들에 대해서는 다음에 바로 설명한다 - 을 가지고 있다.

마지막으로 함수 객체를 add라는 변수가 가리키게 되면 이제 부터 add는 함수로써 __code__속성에서 가리키고 있는 바이트 코드를 실행 할 수 있는 변수가 되는 것이다. 만일 f가 위 함수 객체를 가킨다고 하더라도 동일한 결과를 얻는다.

add역시 변수이므로 함수 객체 대신 다른 값, 예를 들어 정수 10을 대입하게 된다면, 이제 부터는 정수 객체를 가리키는 정수형 변수가 되는 것이다.

함수 객체의 속성값들

앞에서 함수를 만들면, 함수를 관리하기 위해서 함수 객체가 만들어지고, 그 함수 객체는 다양한 속성값을 가진다고 했다. 이번 장에서는 함수 객체의 속성값들에 대해 설명하는 시간을 가져 보도록 하겠다.

함수 객체의 속성들을 알아 두면 코드를 작성할때 많은 도움이 되니 꼭 숙지하도록 하자.

def foo(a=10, b=20, c=30, d=40) :
    '''
       foo 함수 설명. __doc__ 어트리뷰트에 저장됨
    '''
    print('foo')
    
print('doc:', foo.__doc__)
print('name:', foo.__name__)
print('qualified name:', foo.__qualname__)
print('default positional argument:', foo.__defaults__)
print('default keyword argument:', foo.__kwdefaults__)
  • __code__ : 실제 함수의 실행 내용을 담고있는 바이트 코드를 가리키는 포인터
  • __doc__ : 함수를 정의 할 때 함수의 첫 부분에 멀티라인 주석을 사용해서 함수에 대한 설명문 만들면 __doc__ 어트리뷰트에 저장된다.
  • __name__  :  함수의 이름을 출력한다.
  • __qualname__ : 클래스 이름까지 포함하여 함수 이름 출력
  • __defaults__ : 함수의 기본 파라미터들을 튜플(tuple) 형태로 저장한다.
  • __kwdefaults : keyword argument가 있다면 딕셔너리(dictionary) 형태로 저장한다.

일단 이정도 살펴 보고 한숨 쉬고 가자. 위 어트리뷰트들에 대해 추가 설명이 필요하다.

__qualname__은 정확하게는 qualified name으로써 만일 함수가 클래스 메소드인 경우에는 클래스 이름까지 같이 출력하지만 위와 같이 일반함수인 경우에는 __name__과 동일하다.

__defaults__와 __kwdefaults__는 각각 디폴트 파라메터들을 튜플(tuple)과 딕셔너리(dictionary)형태로 저장한다. positional argument인 경우에는 __defaults__에, keyword argument의 경우에는 __kwdefaults__에 저장된다. 위 예제에서는 positional argument 밖에 없기 때문에 __defaults__를 출력하면 (10, 20, 30, 40)과 같이 출력되고, __kwdefaults__는 None이 출력 된다.

만일 __kwdefaults__에 저장되도록 keyword argument로써 디폴트 파라미터를 정의하고 싶다면 [이전]에 배웠던 keyword only argument를 사용해보자.

def foo(a=10, b=20, *, c=30, d=40) :
    '''
       foo 함수 설명. __doc__ 어트리뷰트에 저장됨
    '''
    print('foo')

print('default positional argument:', a.__defaults__) # (10, 20)
print('default keyword argument:', a.__kwdefaults__)  # {'c': 30, 'd': 40}

파라미터 리스트에 아스테리스크를 선언하면, 그 뒤(오른쪽)에 오는 모든 파라미터들은 keyword argument가 된다.

뭔가 있어 보이기 위해 넣은 의미 없는 이미지

이제 나머지 어트리뷰트들에 대해서도 마저 알아보자.

  • __annotations__ : 아래와 같이 함수 선언시 인자와 리턴값에 대한 타입을 지정할 수 있다.  타입을 지정한다고 해서 실제 해당 타입만 받을 수 있도록 제약 사항을 걸어주는 것은 아니고, 그냥 __annotations__ 속성에 정보를 저장하고 있다가 help 함수 호출 시 도움말 출력하는데 사용한다.
# def add(x, y) :
def add(x : int, y : int) -> int: # annotation 문법. 표시만 다르다. 일반함수 선언과 기능은 같다.
    return x + y
    
print(add.__annotations__)
help(add)

# {'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
# Help on function add in module __main__:
# 
# add(x: int, y: int) -> int
#     # def add(x, y)
  • __closure__ : 내부 함수(inner)가 외부 함수(outer)의 변수들에 접근하기 위해, 외부 함수들의 변수 주소들을 저장한다. 아래 변수 x, y와 내부 함수 객체의 __closure__를 출력해보면 각각 같은 주소 값을 가지고 있는 것을 볼 수 있다.
def outer(x) :
    y = 0
    print(hex(id(x)), hex(id(y)))
    
    def inner() :
        nonlocal x, y
        x = 10
        
    return inner
    
f = outer(10)  # f는 inner 함수다
print(f.__closure__)
  • __dict__ : 딕셔너리(dictionary)다. 함수에 관련된 데이터들을 키와 값으로 저장 할 수 있다. 나중에 나올 데코레이터에서 유용하게 사용되는 어트리뷰트다. 자세한 설명은 데코레이터의 사용과 함께 하도록 한다.
def foo() :
    pass
    
print(foo.__dict__)     # 아직은 아무것도 없다

foo.__dict__['x'] = 10  # 키가 x인 10을 저장
print(foo.__dict__)

foo.y = 20              # foo.__dict__['y'] = 20 와 동일
print(foo.__dict__)

def bar() :
    print('bar')

foo.bar = bar           # 함수가 또 다른 함수를 데이터로 가질 수 있다

사용자 정의 함수와 빌트인 함수의 차이

파이썬에서 빌트인 함수는 최적화를 위해 C언어로 작성 되어 있어 사용자 정의 함수가 가지고 있는 몇몇 속성들이 없다. 이 사실을 모르면 나중에 파이썬에 숙달하여 함수의 속성들을 주물럭 거리고 싶어 빌트인 함수를 뒤져봤는데 필요한 속성을 찾지 못해 당황하는 경우가 생긴다. 이번 장은 그렇게 중요하지는 않으므로 이런게 있었다는 정도만 숙지하고 넘어가자. 나중에 상황이 닥치면 다시 찾아봐도 된다.

import sys

def add(x, y) :
    return x + y
    
print(add.__dict__)   
print(print.__dict__) # error. print 빌트인 함수는 __dict__ 어트리뷰트가 없다

print(add)            # <function add at 0x000002DD1F10CF70>
print(print)          # <built-in function print>

print(sys.getsizeof(add))   # 136
print(sys.getsizeof(print)) # 72. 최적화 되어 있음

print(dir(add))
print(dir(print))

정확히 차이점을 알고 싶다면 dir을 통해 확인하면 어떤 어트리뷰트가 있고, 어떤 어트리뷰트가 없는지 알 수 있다.

마치며

이상 파이썬에서는 함수도 객체의 한 종류이며, 객체로써 가지는 메모리 구조와 어트리뷰트들에 대해 알아보았다. 특히 어트리뷰트들은 추후 설명하게 될 데코레이터에서 핵심적으로 사용되게 되므로 꼭 숙지하도록 한다(아니면 나중에 데코레이터 보면서 다시 한번 더 보던지).

다음 장에서는 파이썬의 일급 객체(first class object)의 개념과 활용 방법에 대해 알아 보도록 하겠다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!