일급함수
파이썬의 함수는 일급 객체다. 프로그래밍 언어 이론가들은 다음과 같은 작업을 수행할 수 있는 프로그램 개체를 일급 객체로 정의한다.
- 런타임에 생성할 수 있다.
- 데이터 구조체의 변수나 요소에 할당할 수 있다.
- 함수 인수로 전달할 수 있다.
- 함수 결과로 반환할 수 있다.
정수, 문자열, 딕셔너리도 파이썬의 일급 객체다. 일급 객체로서의 함수인 일급 함수에 대해 알아본다.
인수를 하나 받는 함수는 모두 key 인수로 사용할 수 있다.
def reverse(word):
return word[::-1]
fruits = ['apple', 'banana', 'fig', 'raspberry', 'cherry', 'strawberry']
print(sorted(fruits, key=reverse))
> ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
함수형 프로그래밍에서는 map(), filter(), reduce() 등의 고위 함수가 널리 알려져 있다.
map(), filter() 함수는 지능형 리스트와 제너레이터 표현식이 소개된 이후 이 함수의 중요성이 떨어졌다. 가독성이 더 좋기 때문이다.
from math import factorial
list(map(factorial, range(6)))
> [1, 1, 2, 6, 24, 120]
[factorial(n) for n in range(6)]
> [1, 1, 2, 6, 24, 120]
list(map(factorial, filter(lambda n: n % 2, range(6))))
> [1, 6, 120]
[factorial(n) for n in range(6) if n % 2]
> [1, 6, 120]
파이썬에서 map() 과 filter() 는 제너레이터를 반환하므로, 제너레이터 표현식이 이 함수들을 직접 대체한다.
reduce() 함수는 파이썬 3에서부터 functools 모듈로 떨어져 나왔다. reduce() 는 주로 합계를 구하기 위해 사용되는데, 파이썬 내장 함수인 sum() 을 사용하는 것이 가독성과 성능 면에서 훨씬 낫다.
from functools import reduce
from operator import add
reduce(add, range(100))
> 4950
sum(range(100))
> 4950
sum()과 reduce() 는 연속된 항목에 어떤 연산을 적용해서, 이전 결과를 누적시키면서 일련의 값을 하나의 값으로 reduction 한다(하나의 결과로 집약함)는 공통점이 있다.
이러한 함수를 사용할 때, 일회성의 익명 함수를 사용하는 것이 편리할 때도 있다.
lambda 키워드는 파이썬 표현식 내에 익명 함수를 생성한다.
람다 본체에서는 할당문이나 try, while 등의 파이썬 문장을 사용할 수 없다.
fruits = ['apple', 'banana', 'fig', 'raspberry', 'cherry', 'strawberry']
sorted(fruits, key=lambda word: word[::-1])
> ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
람다는 파이썬에서 제공하는 여러 콜러블 객체 중 하나다.
호출 연산자인 () 는 사용자 정의 함수 이외의 다른 객체에서도 적용할 수 있다. 호출할 수 있는 객체인지 알아보려면 callable() 내장 함수를 사용한다.
파이썬 데이터 모델 문서는 다음 일곱 가지 콜러블 객체를 나열하고 있다.
사용자 정의 함수
- def 문이나 람다 표현식으로 생성한다.
내장 함수
- le() 이나 time.strftime() 처럼 C언어로 구현된 함수(CPython의 경우)
내장 메서드
- dict.get() 처럼 C언어로 구현된 메서드
메서드
- 클래스 본체에 정의된 함수
클래스
- 호출될 때 클래스는 자신의 __new__() 메서드를 실행해서 객체를 생성하고, __init__() 으로 초기화한 후, 최종적으로 호출자에 객체를 반환한다. 파이썬에는 new 연산자가 없으므로 클래스를 호출하는 것은 함수를 호출하는 것과 동일하다
클래스 객체
클래스가 __call__() 메서드를 구현하면 이 클래스의 객체는 함수로 호출될 수 있다.
제너레이터 함수
yield 키워드를 사용하는 함수나 메서드. 이 함수가 호출되면 제너레이터 객체를 반환한다.
파이썬에서는 다양한 콜러블형이 존재하므로, callable() 내장 함수를 사용해서 호출할 수 있는 객체인지 판단하는 방법이 가장 안전하다.
print(abs, str, 13)
> <built-in function abs> <class 'str'> 13
print([callable(obj) for obj in (abs, str, 13)])
> [True, True, False]
파이썬 함수가 실제 객체일 뿐만 아니라, 모든 파이썬 객체가 함수처럼 동작하게 만들 수 있다. __call__() 인스턴스 메서드를 구현하면 된다.
import random
class BingoCage:
def __init__(self, items):
self._items = list(items)
random.shuffle(self._items)
def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')
def __call__(self):
return self.pick()
bingo = BingoCage(range(3))
print(bingo.pick())
> 0
print(bingo())
> 1
print(callable(bingo))
> True
위와 같이 __call__() 메서드를 정의하면 인스턴스를 함수처럼 호출만 하더라도 __call__() 메서드에 정의된 구문이 실행된다.
함수 객체는 __doc__ 이외에도 많은 속성을 가지고 있다. 일반 객체에는 존재하지 않는 함수 고유의 속성을 아래와 같이 확인해 볼 수 있다.
class C: pass
obj = C()
def func(): pass
print(sorted(set(dir(func)) - set(dir(obj))))
> ['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
차집합 연산을 통해 함수에는 존재하지만 기본 클래스의 객체에는 존재하지 않는 속성들을 정렬한 리스트를 만든다.
- __annotations__ (딕셔너리) : 매개변수 및 반환값에 대한 주석
- __call__ (메서드-래퍼) : 콜러블 객체 프로토콜에 따른 () 연산자 구현
- __closure__ (튜플) : 자유 변수 등 함수 클로저
- __code__ (코드) : 바이트코드로 컴파일된 함수 메타데이터 및 함수 본체
- __defaults__ (튜플) : 형식 매개변수의 기본값
- __get__ (메서드-래퍼) : 읽기 전용 디스크립터 프로토콜 구현
- __globals__ (딕셔너리) : 함수가 정의된 모듈의 전역 변수
- __kwdefaults__ (딕셔너리) : 키워드 전용 형식 매개변수의 기본값
- __name__ (문자열) : 함수명
- __qualname__ (문자열) : random.choice() 와 같은 전체 함수 명칭
파이썬 함수에서는 키워드 전용 인수를 이용해서 융통성 있게 매개 변수를 처리한다.
def tag(name, *content, cls=None, **attrs):
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value) for attr, value in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' % (name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)
tag('br')
<br />
tag('p', 'hello')
<p>hello</p>
tag('p', 'hello', 'world')
<p>hello</p>
<p>world</p>
tag('p', 'hello', id=33)
<p id="33">hello</p>
tag('p', 'hello', 'world', cls='sidebar')
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
tag(content='testing', name='img')
<img content="testing" />
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
tag(**my_tag)
<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />
위치 인수(positional argument) 는 첫번째를 제외한 나머지 변수가 *content 매개변수에 튜플로 전달된다.
명시적으로 이름이 지정되지 않은 키워드 인수(keyword argument) 는 딕셔너리로 **attrs 에 전달된다.
별도로 선언된 딕셔너리 앞에 ** 를 붙이면 딕셔너리 안의 모든 항목을 별도의 인수로 전달하고, 명명된 매개변수는 해당 변수에 맞게 전달되며 나머지는 **attrs 에 전달된다.
참고: https://velog.io/@monsterkos/TIL-2020.05.26
함수에서 어떤 매개변수가 필요한지, 매개변수에 기본값이 있는지 여부를 알 수 있는 방법은 무엇인가
함수의 __defaults__ 속성은 위치 인수와 키워드 인수의 기본값을 가진 튜플이 있다. __kwdefaults__ 속성에는 키워드 전용 인수의 기본 값이 들어 있다. 인수명은 __code__ 속성을 보면 된다.
def clip(text, max_len=80):
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # no spaces were found
end = len(text)
return text[:end].rstrip()
print(clip.__defaults__)
> (80,)
print(clip.__code__)
> <code object clip at 0x10d0da190, file "chapter_5.py", line 74>
print(clip.__code__.co_varnames)
> ('text', 'max_len', 'end', 'space_before', 'space_after')
print(clip.__code__.co_argcount)
> 2
위의 clip 함수는 원하는 길이 가까이에 있는 공백에서 잘라서 문자열을 단축하는 함수이다.
인수의 기본값은 __default__ 속성을 통해 80인 것을 확인할 수 있다.
__code__ 속성은 code 객체를 가리키며, 그 안의 co_varnames 에는 인수명이 들어 있다. 다만, 여기에는 함수 본체에서 생성한 지역 변수명도 들어 있다. __code__.co_argcount 가 함수의 인수 개수다. 이를 보다 깔끔하게 보려면 inspect 모듈을 사용하면 된다.
from inspect import signature
sig = signature(clip)
print(str(sig))
> (text, max_len=80)
for name, param in sig.parameters.items():
print(param.kind, ":", name, "=", param.default)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80
signature 객체를 생성하고, 이를 문자열로 출력하면 인수와 기본값을 확인할 수 있다.
또한 signature 객체 안의 parameters 속성을 이용하면 정렬된 inspect.Parameter 객체를 읽을 수 있다. 각 Parameter 객체 안에는 name, default, kind 등의 속성이 있다. inspect._empty 는 매개변수에 기본값이 없음을 나타낸다.
kind 속성은 _ParameterKind 클래스에 정의된 다음 다섯 가지 값 중 하나를 가진다.
POSITIONAL_OR_KEYWORD : 위치 인수나 키워드 인수로 전달할 수 있는 매개 변수
VAR_POSITIONAL : 위치 매개변수의 튜플
VAR_KEYWORD : 키워드 매개변수의 딕셔너리
KEYWORD_ONLY : 키워드 전용 매개변수
POSITIONAL_ONLY : 위치 전용 매개변수. 현재 파이썬 함수 선언 구문에서는 지원하지 않지만, 키워드로 전달한 매개변수를 받지 않는 divmod() 처럼 C언어로 구현된 기존 함수가 여기에 속한다.
inspect.Signature 객체에는 bind() 메서드가 정의되어 있다. bind() 메서드는 임의 개수의 인수를 받고, 인수를 매개변수에 대응시키는 일반적인 규칙을 적용해서 그것을 시그니처에 들어 있는 매개변수에 바인딩한다. bind() 메서드는 프레임워크에서 실제 함수를 호출하기 전에 인수를 검증하기 위해 사용할 수 있다.
import inspect
sig = inspect.signature(tag)
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
bound_args = sig.bind(**my_tag)
print(bound_args)
> <BoundArguments (name='img', cls='framed', attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>
for name, value in bound_args.arguments.items():
print(name, "=", value)
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
del my_tag['name']
bound_args = sig.bind(**my_tag)
Traceback (most recent call last):
File "chapter_5.py", line 109, in <module>
bound_args = sig.bind(**my_tag)
File "/Users/monsterkos/.pyenv/versions/3.8.5/lib/python3.8/inspect.py", line 3025, in bind
return self._bind(args, kwargs)
File "/Users/monsterkos/.pyenv/versions/3.8.5/lib/python3.8/inspect.py", line 2940, in _bind
raise TypeError(msg) from None
TypeError: missing a required argument: 'name'
위의 inspect 예제를 통해, 함수 호출 시 인수를 매개변수에 바인딩하기 위해 인터프리터가 사용하는 파이썬 데이터 모델 메커니즘이 작동하는 방식을 알 수 있다.
프레임 워크 및 IDE 등은 이 정보를 이용해서 코드를 검증할 수 있다.
파이썬 3는 함수의 매개변수와 반환값에 메타데이터를 추가할 수 있는 구문을 제공한다. 함수 선언에서 각 매개변수에는 콜론(:) 뒤에 애너테이션 표현식을 추가할 수 있다.
def clip(text:str, max_len:'int > 0'=80) -> str:
pass
애너테이션은 인수명과 등호(=) 사이에 들어 간다. 반환값에 애너테이션을 추가하려면 매개변수를 닫는 괄호와 함수 선언의 제일 뒤에 오는 콜론 사이에 -> 기호와 표현식을 추가한다. 표현식은 어떤 자료형도 될 수 있으며, 'int > 0' 과 같은 문자열도 사용 가능하다.
애너테이션은 함수 객체 안의 dict 형 __annotations__ 속성에 저장된다.
print(clip.__annotations__)
> {'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}
파이썬은 애너테이션을 함수의 __annotations__ 속성에 저장할 뿐이다. 검증하지 않는다. 따라서 애너테이션은 파이썬 인터프리터에 아무런 의미가 없다. 애너테이션은 IDE, 프레임워크, 데코레이터가 사용할 수 있는 메타데이터일 뿐이다.
sig = inspect.signature(clip)
print(sig.return_annotation)
> <class 'str'>
for param in sig.parameters.values():
note = repr(param.annotation).ljust(13)
print(note, ":", param.name, "=", param.default)
<class 'str'> : text = <class 'inspect._empty'>
'int > 0' : max_len = 80
표준 라이브러리에서 함수형 프로그래밍을 지원하기 위해 제공하는 유용한 패키지들에 대해 알아본다.
함수형 프로그래밍을 할 때 산술 연산자를 함수로 사용하는 것이 편리할 때가 종종 있다. 예를 들어 팩토리얼을 계산하기 위해 재귀적으로 함수를 호출하는 대신 숫자 시퀀스를 곱하는 경우를 볼 수 있다. 이 때, reduce 를 사용하면 편리하게 구현이 가능하다.
from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
이 때, 익명 함수를 작성하는 것보다 간단하게 operator 모듈의 mul 함수를 사용할 수 있다.
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
operator 모듈은 이 외에도 시퀀스 항목을 가져오는 람다를 대체하는 itemgetter() 함수와 객체의 속성을 읽는 람다를 대체하는 attrgetter() 함수를 제공한다.
metro_data = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
from operator import itemgetter
for city in sorted(metro_data, key=itemgetter(1)):
print(city)
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
itemgetter(1) 은 lambda fields: fields[1] 과 동일하다. 주어진 컬렉션에 대해 1번 인덱스 항목을 반환하는 함수를 생성한다. itemgetter() 에 여러 개의 인덱스를 전달하면, 생성된 함수는 해당 인덱스의 값들로 구성된 튜플을 반환한다.
cc_name = itemgetter(1, 0)
for city in metro_data:
print(cc_name(city))
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')
itemgetter() 는 [] 연산자를 사용하므로 시퀀스 뿐만 아니라 매핑 및 __getitem__() 을 구현한 모든 클래스를 지원한다.
attrgetter() 는 이름으로 객체 속성을 추출하는 함수를 생성한다.
attrgetter() 에 여러 속성명을 인수로 전달하면, 역시 해당 속성값으로 구성된 튜플을 반환한다. 또한 속성명에 점(.)이 포함되어 있으면 attrgetter() 는 내포된 객체를 찾아서 해당 속성을 가져온다.
from collections import namedtuple
LatLong = namedtuple('LatLong', 'lat long')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for name, cc, pop, (lat, long) in metro_data]
print(metro_areas[0])
> Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))
print(metro_areas[0].coord.lat)
> 35.689722
from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')
for city in sorted(metro_areas, key=attrgetter('coord.lat')):
print(name_lat(city))
('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
operator 모듈의 methodcaller() 메서드는 인수로 전달받은 객체의 메서드를 호출한다.
from operator import methodcaller
s = 'The time has come'
upcase = methodcaller('upper')
print(upcase(s))
> THE TIME HAS COME
hiphenate = methodcaller('replace', ' ', '-')
print(hiphenate(s))
> The-time-has-come
functools.partial() 는 함수를 부분적으로 실행할 수 있게 해주는 고위 함수다. 어떤 함수가 있을 때 partial() 을 적용하면 원래 함수의 일부 인수를 고정한 콜러블을 생성한다. 이 기법은 하나 이상의 인수를 받는 함수를 그보다 적은 인수를 받는 콜백 함수를 사용하는 API에 사용하고자 할 때 유용하다.
from operator import mul
from functools import partial
triple = partial(mul, 3)
print(triple(7))
> 21
print(list(map(triple, range(1, 10))))
> [3, 6, 9, 12, 15, 18, 21, 24, 27]
mul() 함수의 첫 번째 위치 인수를 3으로 바인딩해서 triple() 함수를 새로 만들었다.
partial() 의 첫 번째 인수는 콜러블이며, 그 뒤에 바인딩할 위치 인수와 키워드 인수가 원하는 만큼 나온다.
picture = partial(tag, 'img', cls='pic-frame')
print(picture(src='wumpus.jpg'))
> <img class="pic-frame" src="wumpus.jpg" />
print(picture)
> functools.partial(<function tag at 0x10e98d700>, 'img', cls='pic-frame')
print(picture.func)
> <function tag at 0x10e98d700>
print(picture.args)
> ('img',)
print(picture.keywords)
> {'cls': 'pic-frame'}
앞서 만들었던 tag 함수의 위치 인수와 키워드 인수를 고정했을 때의 결과이다.
functools.partialmethod() 함수는 partial() 과 동일하지만 메서드에 대해 작동하도록 설계되었다.
이러한 함수들을 통해 기능이 떨어지는 람다 구문을 대체하여 함수형 프로그래밍을 할 수 있게 해준다.
'python' 카테고리의 다른 글
Fluent Python (챕터 6) (0) | 2021.10.27 |
---|---|
Architecture Patterns with Python(3장) (0) | 2021.10.23 |
Architecture Patterns with Python(2장) (0) | 2021.10.15 |
Fluent Python (챕터 4) (0) | 2021.10.11 |
Fluent Python (챕터 3) (0) | 2021.10.09 |