3부 객체로서의 함수 - 5. 일급 함수

seokj·2023년 2월 20일
0

FluentPython

목록 보기
6/9

일급 함수는 일급 객체로서의 함수를 말한다. 일급 객체는 다음의 조건을 만족한다.

  • 런타임에 생성 가능
  • 변수에 할당 가능
  • 함수 인자로 전달 가능
  • 함수 결과로 반환 가능

파이썬에서는 정수, 문자열, 딕셔너리는 일급 객체이다. 심지어 함수도 일급 객체이다. 일급 함수일 경우 생기는 여러 유용한 활용법을 알아보자.


중요 키워드
help, __doc__
고위함수, map, filter, functools.reduce, sum, all, any
lambda
callable
데코레이터, 클로저
dir, __dict__
inspect, signature, _ParameterKind, annotation
operator, functools, itemgetter, attrgetter, methodcaller, partial, partialmethod

덜 중요한 키워드
operator.add
메모이제이션, lru_cache, singledispatch, wraps

참고 자료
http://docs.python.org/3/howto/functional.html
https://docs/djangoproject.com/en/1.5/ref/contrib/admin
http://bit.ly/1Vm8dv2
http://bit.ly/1FHiOXf
http://bit.ly/1FHiN5F
https://www.python.org/dev/peps/pep-0362/
http://bit.ly/1FHiTdh
http://bit.ly/1FHjdZv


def 키워드를 통해 런타임에 함수를 생성할 수 있다. (변수명=함수명)을 하여 변수명에 함수를 대입할 수 있다. type(함수명)을 하면 function클래스의 객체라는 사실을 알 수 있다. 함수명.__doc__을 하여 함수의 docstring을 가져올 수 있다. help(함수명)을 하여 함수에 대한 설명을 가져올 수 있다. map함수의 인자는 함수를 받는다.


함수를 인자로 받거나 결과로 반환하는 함수를 고위함수라 한다. map, filter, functools.reduce가 고위함수이다. 이는 지능형 리스트, 제너레이터 표현식 등으로 대체할 수 있다. reduce로 합을 구할 땐 대신 sum을 사용하는 것이 가독성 측면에서 더 좋다. all, any함수도 유용하다.


lambda로 익명함수를 생성할 수 있는데 고위함수의 인자로 주로 사용된다. lambda는 하나의 표현식으로 만들어져야 한다는 제한이 있어서 while, try같은 구문을 사용하지 못해 자주 쓰이지는 않는다.


()로 호출할 수 있는 객체를 콜러블이라 한다. callable함수를 통해 해당 객체가 콜러블인지 확인할 수 있다. 파이썬에서 콜러블은 7가지가 있다.

  • 사용자 정의 함수: def나 lambda로 생성한 함수
  • 내장 함수: C언어로 구현된 함수 (CPython) (예를 들어 len이나 time.strftime)
  • 내장 메서드: C언어로 구현된 메서드 (예를 들어 dict.get)
  • 메서드: 클래스에 정의된 함수
  • 클래스: 클래스명()은 클래스의 __new__메서드를 호출한다.
  • 클래스 객체: 객체명()은 객체의 __call__메서드를 호출한다.
  • 제너레이터 함수: yield가 포함된 함수나 메서드

제너레이터 함수는 여타 콜러블과 다르고 코루틴으로 활용될 수도 있다.


다음과 같이 __call__을 구현하여 클래스 객체가 콜러블로 작동하게 할 수 있다.

import random
class Cage:
    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except:
            raise LookupError('Empty Cage')
    
    __call__ = pick
  • __call__=pickdef __call__를 통해 선언하여 pick의 결과를 반환하도록 구현해도 된다.
  • __init__의 items를 로컬에 복사할 때 list생성자를 사용하지 않으면 바깥으로부터 넘어온 items객체와 로컬의 self._items가 같은 객체를 공유하기 때문에 위험하다.

함수에 정보를 저장해놓고 사용하는 이런 구조의 경우 데코레이터로 만들 수도 있고 메모이제이션같은 상황에 활용될 수 있다. 함수의 내부 상태를 구성해서 반환하는 함수인 클로저도 있다.


function클래스의 객체는 dir을 통해 어떤 속성이 있는지 확인할 수 있다. 클래스와 마찬가지로 __dict__가 있어서 함수에 사용자 속성을 저장할 수 있다. 장고에서 이런 기능이 활용된다.

function클래스도 클래스이므로 클래스가 가지고 있는 속성을 모두 가지고 있다. 클래스에는 없는, function클래스에만 있는 속성에는 __annotations__, __call__, __closure__, __code__, __defaults__, __get__, __globals__, __kwdefaults__, __name__, __qualname__이 있다.


함수의 매개변수는 다음과 같이 사용할 수 있다.

def f(a, *b, c, **d):
    print('positional parameter'.ljust(40), a)
    print('arbitrary positional parameters'.ljust(40), b)
    print('keyword parameter'.ljust(40), c)
    print('arbitrary keyword parameters'.ljust(40), d)
f(1, 3, 2, c=1, d=2, e=51)
---결과---
positional parameter                     1
arbitrary positional parameters          (3, 2)
keyword parameter                        1
arbitrary keyword parameters             {'d': 2, 'e': 51}

만약 위 코드에서 b, d가 필요없어서 지웠다면 c는 positional parameter가 되어버린다. 그대로 keyword-only로 받고 싶다면 def f(a, *, c)로 선언한다.


함수명.__defaults__로 파라미터 기본값을 tuple로 확인할 수 있다.
함수명.__kwdefaults__로 키워드 파라미터의 기본값을 dict로 확인할 수 있다.
함수명.__code__.co_argcount로 positional 파라미터의 수를 확인할 수 있다.
함수명.__code__.co_varnames로 파라미터와 내부 변수의 이름을 tuple로 확인할 수 있다.

inspect.signature로 어떤 함수에 대한 Signature객체를 얻을 수 있는데, 이 객체의 parameters 속성으로 파라미터의 정보가 담긴 OrderedDict를 얻을 수 있다. 각 파라미터는 Parameter객체로 되어있는데 name, default, kind의 속성이 있다. default값이 없을 경우 inspect._empty로 되어있다. kind는 아래에서 설명하는 inspect._ParameterKind형으로 확인할 수 있다.

함수의 파라미터 종류는 inspect._ParameterKind에 정의되어있다. 총 5가지로, 다음과 같다.

from inspect import _ParameterKind
list(_ParameterKind)
---결과---
[<_ParameterKind.POSITIONAL_ONLY: 0>,
 <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>,
 <_ParameterKind.VAR_POSITIONAL: 2>,
 <_ParameterKind.KEYWORD_ONLY: 3>,
 <_ParameterKind.VAR_KEYWORD: 4>]

각 파라미터 종류별로 하나씩 가지고 있는 함수는 다음과 같이 선언한다.

def f(a=3, /, b=2, *c, e=3, **f):
    pass
for k, v in signature(f).parameters.items():
    print(v.kind)
---결과---
POSITIONAL_ONLY
POSITIONAL_OR_KEYWORD
VAR_POSITIONAL
KEYWORD_ONLY
VAR_KEYWORD

매개변수로 /*가 올 수 있는데, /왼쪽은 positional파라미터, *오른쪽은 keyword파라미터, 그 사이는 두 방식 모두 인자를 전달할 수 있는 positional or keyword파라미터이다.

Parameter에는 annotation 속성도 있다. 파라미터에 적용한 어노테이션 정보가 담긴다.

Signature객체의 bind함수를 통해 함수를 실행시키기 전에, 전달하려는 인자가 함수에 맞는지 확인할 수 있다. 맞으면 BoundArguments객체를 얻을 수 있고 각 파라미터에 어떤 값이 할당되었는지 arguments 속성을 통해 dict로 확인할 수 있다. 맞지 않으면 오류가 발생한다.


다음과 같이 함수의 파라미터와 반환값에 어노테이션을 붙일 수 있다.

def f(a:int, b:'int>0'=3) -> None:
    pass
f.__annotations__
---결과---
{'a': int, 'b': 'int>0', 'return': None}

Signature객체의 return_annotation속성을 통해 반환형의 어노테이션을 얻을 수 있고 Parameter객체의 annotation속성을 통해 파라미터의 어노테이션을 얻을 수 있다. 얻은 어노테이션을 통해 파라미터의 자료형을 체크하거나 조건을 검사할 수 있다. linter와 같은 도구에서 활용된다.


operator 모듈에는 다양한 간단한 함수가 선언되어있다. 이를 활용하여 고위함수에서 함수 인자로 전달하기 위한 함수를 lambda 대신 대체할 수 있다.

import functools
import operator
print(functools.reduce(lambda x, y: x * y, range(1, 10)))
print(functools.reduce(operator.mul, range(1, 10))) #윗 줄과 실행결과가 같다.

operator.mul은 두 수를 입력받아 곱하여 반환하는 함수이다. add, sub 등 다양한 함수가 미리 정의되어있다. 그 외에도 itemgetter, attrgetter, methodcaller도 있다.

itemgetter(i)는 lambda x: x[i]를 대체한다. slice객체를 전달해도 되고 인자를 여러 개 넣으면 각각 인덱싱한 결과를 튜플로 묶어 반환한다.

attrgetter(i)는 lambda x: x.i를 대체한다. 이때 i는 문자열로 전달해야 한다. 마찬가지로 인자 여러 개를 넣으면 튜플의 결과를 얻을 수 있고 연달아 .연산으로 접근해야 하는 경우 i에 .을 넣어 한 번에 접근하는 것이 가능하다.

methodcaller(f, a, ...)는 lambda x: x.f(a, ...)를 대체한다. 가장 처음 인자가 메소드 명이고 그 뒤로 이어지는 인자가 f의 인자로 들어간다.

functools 모듈에는 함수형 프로그래밍에 유용한 partial, partialmethod 함수가 있다. partial은 함수와 그 함수의 인자 중 일부를 전달하여, 전달된 일부의 인자로 고정시키고 나머지 인자들로 함수를 호출할 수 있다. partialmethod는 partial과 똑같지만 클래스의 메소드에 대하여 작동한다.

functools 모듈에 있는 lru_cache, singledispatch, wraps 등 데코레이터와 함께 메모이제이션을 구현할 수 있다.

profile
안녕하세요

0개의 댓글