함수형 파이썬 라이브러리 Partial.py

Partial.py는 함수형 파이썬을 더 많은 영역에서 사용하고자, 몇 가지 기능을 확장한 라이브러리입니다. Partial.py는 부분 적용, 파이프라인, 비동기 제어 등의 기능을 제공하고 있습니다.

설치하기

Partial.py 설치

git으로 설치하기:
1
2
3
$ git clone https://github.com/marpple/partial.py.git
$ cd partial.py
$ sudo python setup.py install

혹은

pypi로 설치하기:
1
$ pip install partial.py

Partial.py 사용

네임스페이스로 사용할 _, __, ___를 불러옵니다.

1
from partial import _, __, ___

더 나은 부분 적용 (Partial application)

Partial.py는 이름처럼 부분 적용(Partial application)을 중요하게 생각합니다. 기존의 _.partial 함수는 왼쪽에서부터만 인자를 적용해둘 수 있습니다. Partial.py의 _.partial 함수는 맨 오른쪽 인자나 맨 오른쪽에서 두 번째에만 인자를 적용해두는 것도 가능하며, 새로운 구분자인 ___를 활용하여 중간 지점에 인자가 가변적으로 적용되도록 비워둘 수 있습니다.

_.partial의 일반적 사용

_.partial을 실행하면서 인자 자리에 _를 넘기면 부분 적용할 인자를 건너띌 수 있습니다. _를 이용하면 원하는 곳에만 인자를 부분 적용해둘 수 있습니다. _가 있는 자리는 이후 실행시 채워집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pc = _.partial(print, 1)
pc(2)
# 결과: 1 2
# 2 가 오른쪽으로 들어갑니다.
pc(2, 3)
# 결과: 1 2 3
# 2, 3이 오른쪽으로 들어갑니다.
pc = _.partial(print, _, 2)
pc(1)
# 결과: 1 2
# 1이 왼쪽의 _ 자리에 들어갑니다.
pc(1, 3)
# 결과: 1 2 3
# 1이 왼쪽의 _ 자리에 들어가고 3이 오른쪽으로 들어갑니다.
pc = _.partial(print, _, _, 3)
pc(1)
# 결과: 1 undefined 3
# 1이 왼쪽의 _ 자리에 들어가고 두 번째 _는 들어오지 않아 undefined가 됩니다.
pc(1, 2)
# 결과: 1 2 3
# 1과 2가 순서대로 _, _를 채웁니다.
pc(1, 2, 4)
# 결과: 1 2 3 4
# 1과 2가 순서대로 _, _를 채우고 3의 오른쪽으로 4가 들어갑니다.
pc = _.partial(print, _, 2, _, 4)
pc(1, 3, 5)
# 결과: 1 2 3 4 5
# 1을 _ 자리에 채우고 2를 넘겨서 _에 3을 채우고 4의 오른쪽에 5가 들어갑니다.
pc = _.partial(print, _, 2, _, _, 5)
pc(1, 3, 4, 6)
# 결과: 1 2 3 4 5 6
# 1을 _ 자리에 채우고 2를 넘겨서 _에 3을 채우고 다음 _에 4를 채우고 5의 오른쪽에 6이 들어갑니다.

오른쪽에서부터 인자 적용해두기

_.partial을 실행하면 ___를 기준으로 왼편의 인자들을 왼쪽부터 적용하고 오른편의 인자들을 오른쪽부터 적용할 준비를 해둔 함수를 리턴합니다. 부분 적용된 함수를 나중에 실행하면 그때 받은 인자들로 왼쪽과 오른쪽을 먼저 채운 후, 남은 인자들로 가운데 ___ 자리를 채웁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pc = _.partial(print, ___, 2, 3)
pc(1)
# 결과: 1 2 3
# ___ 자리에 1이 들어가고 2, 3은 맨 오른쪽에 들어갑니다.
pc(1, 4, 5, 6)
# 결과: 1 4 5 6 2 3
# ___ 자리에 1, 4, 5, 6이 들어가고 2, 3은 맨 오른쪽에 들어갑니다.
pc = _.partial(print, _, 2, ___, 6)
pc(1, 3, 4, 5)
# 결과: 1 2 3 4 5 6
# _에 1이 들어가고 2를 넘어가고 ___ 자리에 3, 4, 5가 채워지고 6이 맨 오른쪽에 들어갑니다.
pc(1, 3, 4, 5, 7, 8, 9)
# 결과: 1 2 3 4 5 7 8 9 6
# _에 1이 들어가고 2를 넘어가고 ___ 자리에 3, 4, 5, 7, 8, 9가 채워지고 6이 맨 오른쪽에 들어갑니다.
pc = _.partial(print, _, 2, ___, 5, _, 7)
pc(1)
# 결과: 1 2 5 undefined 7
# _ 자리에 1이 들어가고 2와 5사이는 유동적이므로 모이고 5가 들어간 후 _가 undefined로 대체 되고 7이 들어갑니다.
pc(1, 3, 4)
# 결과: 1 2 3 5 4 7
# _ 자리에 1이 들어가고 2와 5사이에 3이 들어가고 _ 를 4로 채운 후 7이 들어갑니다.
# 왼쪽의 _ 들이 우선 순위가 제일 높고 ___ 보다 오른쪽의 _ 들이 우선순위가 높습니다.
pc(1, 3, 4, 6, 8)
# 결과: 1 2 3 4 6 5 8 7
# _ 자리에 1이 들어가고 2와 5사이에 3, 4, 6이 들어가고 _ 를 8로 채운 후 7이 들어갑니다.

간결하게 사용하기

_ == _.partial입니다. _.partial_로 간결하게 표현할 수 있습니다.

1
2
3
4
5
6
def add (a, b):
return a + b
add10 = _(add, 10)
print( add10(5) )
# 15

파이프라인

파이프라인 함수인 _.pipe, _.go 등은 작은 함수들을 모아 큰 함수를 만드는 함수입니다. 파이프라인으로 함수를 조합하면 왼쪽에서부터 오른쪽, 위에서부터 아래로 표현되어 읽기 쉬운 코드가 됩니다. 체인 방식과 다르게 아무 함수나 사용할 수 있어 자유도가 높습니다. 작은 함수들을 인자와 결과만을 생각하면서 조합하면 됩니다.

즉시 실행과 Multiple Results

_.go는 파이프라인의 즉시 실행 버전입니다. 첫 번째 인자로 받은 값을 두 번째 인자로 받은 함수에게 넘겨주고 두 번째 인자로 받은 함수의 결과는 세 번째 함수에게 넘겨주는 것을 반복하다가 마지막 함수의 결과를 리턴해줍니다.

1
2
3
4
5
6
7
_.go(10, # 첫 번째 함수에서 사용할 인자
lambda a: a * 10, # 연속 실행할 함수 1
# 100
lambda a: a - 50, # 연속 실행할 함수 2
# 50
lambda a: a + 10) # 연속 실행할 함수 3
# 60

_.go는 Multiple Results를 지원합니다. _.mr 함수를 함께 사용하면 다음 함수에게 2개 이상의 인자들을 전달할 수 있습니다.

1
2
3
4
5
_.go(10, # 첫 번째 함수에서 사용할 인자
lambda a: _.mr(a * 10, 50), # 두 개의 값을 리턴
lambda a, b: a - b, # 두 개의 인자 받기
lambda a: a + 10)
# 60

_.go의 첫 번째 인자는 두 번째 인자인 함수가 사용할 인자고 두 번째 부터는 파이프라인에서 사용할 함수들입니다. _.go의 두 번째 인자인 함수, 즉 최초 실행될 함수에게 2개 이상의 인자를 넘기고자 한다면 _.mr을 사용하면 됩니다. _.mr로 인자들을 감싸서 넘겨주면, 다음 함수는 인자를 여러 개로 펼쳐서 받게 됩니다.

1
2
3
4
_.go(_.mr(2, 3),
lambda a, b: a + b, # 2 + 3
lambda a: a * a)
# 25

_.go를 이미 정의되어 있는 함수와 조합하면 더욱 읽기 좋아집니다.

1
2
3
4
5
6
7
8
def add(a, b):
return a + b
def square(a):
return a * a
_.go(_.mr(2, 3), add, square)
# 25

파이프라인 함수를 리턴하는 _.pipe

_.go가 즉시 실행하는 파이프라인이라면 _.pipe는 실행할 준비가 된 함수를 리턴하는 파이프라인 함수입니다. 그외 모든 기능은 _.go와 동일합니다.

1
2
3
f1 = _.pipe(add, square)
f1(2, 3)
# 25

부분 커링

커링이 부분적으로 동작하는 함수

Partial.py의 주요 함수들은 커링이 부분적으로 동작하도록 지원하고 있습니다. 아래는 일반적인 사용 모습입니다.

일반적인 방식:
1
2
3
4
5
6
7
values = lambda data: _.map(data, lambda v, *r: v)
print(values({ 'a': 1, 'b': 2, 'c': 4 }))
# [1, 2, 4]
take3 = lambda data: _.take(data, 3)
take3([1, 2, 3, 4, 5])
# [1, 2, 3]

Partial.py의 주요 함수들은 부분 커링이 적용되어 위와 동일한 동작을 아래와 같이 간결하게 표현할 수 있습니다.

부분 커링이 지원될 경우:
1
2
3
4
5
6
7
values = _.map(lambda v, *r: v)
print(values({ 'a': 1, 'b': 2, 'c': 4 }))
# [1, 2, 4]
take3 = _.take(3)
take3([1, 3, 5, 7, 9])
# [1, 3, 5]

파이프라인과 함께

부분 커링이 지원되면 파이프라인과 함께 사용할 때, 체인처럼 간결한 표현이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
users = [
{ 'id': 1, 'name': 'ID', 'age': 32 },
{ 'id': 2, 'name': 'HA', 'age': 25 },
{ 'id': 3, 'name': 'BJ', 'age': 32 },
{ 'id': 4, 'name': 'PJ', 'age': 28 },
{ 'id': 5, 'name': 'JE', 'age': 27 },
{ 'id': 6, 'name': 'JM', 'age': 32 },
{ 'id': 7, 'name': 'JI', 'age': 31 }
]
## 일반적인 사용
_.go(users,
lambda users: _.filter(users, lambda u, *r: u['age'] < 30),
lambda users: _.pluck(users, 'name'),
print)
# ["HA", "PJ", "JE"]
## 부분 커링이 된다면
_.go(users,
_.filter(lambda u, *r: u['age'] < 30),
_.pluck('name'),
print)
# ["HA", "PJ", "JE"]
## Underscore.py 체인
underscore\
.chain(users)\
.filter(lambda u: u['age'] < 30)\
.pluck('name')\
.tap(print)
# ["HA", "PJ", "JE"]

_.go, _.pipe 등의 파이프라인이 받는 재료는 함수이기 때문에 아무 함수나 조합할 수 있습니다. 체인처럼 메서드 등으로 준비되어있지 않아도 되며 Partial.py의 함수만 사용할 필요도 없습니다. Partial.py의 파이프라인은 결과를 여러 개로 리턴할 수 있고, 여러 개의 인자를 받을 수 있고, 다른 라이브러리에 있는 함수든, 직접 만든 함수든, 익명 함수든 모두 쉽게 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
products = [
{ 'id': 1, 'name': '후드 집업', 'discounted_price': 6000, 'price': 10000 },
{ 'id': 2, 'name': '코잼 후드티', 'discounted_price': 8000, 'price': 8000 },
{ 'id': 3, 'name': 'A1 반팔티', 'discounted_price': 6000, 'price': 6000 },
{ 'id': 4, 'name': '코잼 반팔티', 'discounted_price': 5000, 'price': 6000 }
]
# 할인 상품들을 가격이 낮은 순으로 정렬한 상품 이름들
_.go(products,
_.filter(lambda p, *r: p['price'] > p['discounted_price']),
_.sortBy('discounted_price'),
_.pluck('name'),
print)
# ["코잼 반팔티", "후드 집업"]
# 할인이 없는 상품들의 id들
_.go(products,
_.reject(lambda p, *r: p['price'] > p['discounted_price']),
_.pluck('id'),
print)
# [2, 3]
# 할인 상품 중 할인액이 가장 낮은 상품의 이름
_.go(products,
_.filter(lambda p, *r: p['price'] > p['discounted_price']),
_.min(lambda p, *r: p['price'] - p['discounted_price']),
_.val('name'),
print)
# 코잼 반팔티
# 할인액이 가장 높은 상품의 이름
_.go(products,
_.max(lambda p, *r: p['price'] - p['discounted_price']),
_.val('name'),
print)
# 후드 집업

비동기

async, await 키워드와 Partial.py를 함께 사용하면 다양한 비동기 상황을 간단히 제어할 수 있습니다.

파이프라인으로 비동기 제어 하기

_.asy를 네임스페이스로 갖는 _.asy.go, _.asy.pipe 등의 파이프라인 함수들은 비동기 제어를 지원합니다.

await 키워드와 함께 사용:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async def asy_add(val, *r):
await asyncio.sleep(1)
return val + 10
## await 키워드를 사용해서 바로 실행하는 _.go.asy
await _.asy.go(10,
asy_add,
print)
# 20
## _.asy.pipe로 비동기 함수와 동기 함수 조합해 만든 새로운 함수
asy_pipe = _.asy.pipe(asy_add, print)
await asy_pipe(10)
# 20
## _.pipe는 비동기 함수를 만나면 자동으로 _.asy.pipe가 됩니다.
asy_pipe = _.pipe(asy_add, print)
await asy_pipe(10)
# 20

컬렉션을 다루는 비동기 제어 함수

Partial.py의 _.each, _.map, _.reduce 등의 주요 함수들은 _.asy.go_.asy.pipe처럼 동기와 비동기 상황이 모두 대응되도록 되어 있습니다. Partial.py의 함수를 이용하면 비동기 상황에서도 동기 상황과 동일한 코드를 작성할 수 있고, 비동기 함수와 동기 함수의 조합도 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async def asyncDate(*r):
await asyncio.sleep(1)
return datetime.datetime.now()
def syncDate(*r):
return datetime.datetime.now()
_.go(
[1, 2, 3],
_.map(syncDate),
_.map(lambda now, *r: now.strftime('%Y-%m-%d %H:%M:%S')),
print)
# ['2017-06-16 12:34:39', '2017-06-16 12:34:39', '2017-06-16 12:34:39']
await _.asy.go(
[1, 2, 3],
_.map(asyncDate),
_.map(lambda now, *r: now.strftime('%Y-%m-%d %H:%M:%S')),
print)
# ['2017-06-16 12:34:39', '2017-06-16 12:34:39', '2017-06-16 12:34:39']

위 사례처럼 Partial.py는 _.pipe, _.each, _.map, _.find, _.filter, _.reject, _.reduce, _.some, _.every 등의 함수들에서 자동 비동기 제어 로직을 지원합니다.

지연 평가 L

Partial.py의 L을 이용하면, 파이프라인 내부에서 함수들의 실행 순서를 재배치하여 적절하게 평가를 지연합니다. 사용법은 간단합니다. Partial.py에서 Limport하시면 됩니다. L을 통해 지연 평가할 영역을 명시적으로 선택할 수 있습니다. _.go, _.pipe등의 파이프라인이 L로 시작하여 L로 끝날 때까지의 함수들을 재배치하여 성능을 개선합니다.

비교

엄격한 평가:
1
2
3
4
5
6
7
8
list = [1, 2, 3, 4, 5, 6]
_.go(list,
_.map(lambda v, *r: v * v), # 6번
_.filter(lambda v, *r: v < 20), # 6번
_.take(2),
print)
# [1, 4]
# 총 12번
지연 평가:
1
2
3
4
5
6
7
8
list = [1, 2, 3, 4, 5, 6]
_.go(list,
L.map(lambda v, *r: v * v), # 2번
L.filter(lambda v, *r: v < 20), # 2번
L.take(2),
print)
# [1, 4]
# 총 4번

지원 함수들

Partial.py의 지연 평가 지원 함수로는 L.map, L.filter, L.reject, L.find, L.some, L.every, L.take가 있습니다. 이 함수들을 순서대로 나열하면 파이프라인이 평가 시점을 변경하여 성능을 개선합니다.

다음과 같은 상황 등에서 동작합니다.

  • map->map->map
  • map->take
  • filter->take
  • map->filter->take
  • map->filter->map->map
  • map->filter->map->take
  • map->reject->map->map->filter->map
  • map->some
  • map->every
  • map->find
  • map->filter->some
  • map->filter->every
  • map->filter->find
  • filter->map->some
  • filter->map->every
  • filter->map->reject->find

지연 평가를 시작시키고 유지 시키는 함수는 map, filter, reject이고 끝을 내는 함수는 take, some, every, find, 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
users = [
{ 'id': 1, 'name': 'ID', 'age': 12 },
{ 'id': 2, 'name': 'BJ', 'age': 28 },
{ 'id': 3, 'name': 'HA', 'age': 13 },
{ 'id': 4, 'name': 'PJ', 'age': 23 },
{ 'id': 5, 'name': 'JE', 'age': 29 },
{ 'id': 6, 'name': 'JM', 'age': 32 },
{ 'id': 7, 'name': 'JE', 'age': 31 },
{ 'id': 8, 'name': 'HI', 'age': 15 },
{ 'id': 9, 'name': 'HO', 'age': 28 },
{ 'id': 10, 'name': 'KO', 'age': 34 }
]
# 10대 2명까지만 찾아내기
_.go(users,
L.filter(lambda user, *r : user['age'] < 20),
L.take(2),
print)
# [{ 'id': 1, 'name': "ID", 'age': 12 }, { 'id': 3, 'name': "HA", 'age': 13 }]
# 3번만 반복
# 10대 2명까지만 찾아내서 이름 수집하기
_.go(users,
L.filter(lambda user, *r : user['age'] < 20),
L.map(lambda v, *r : v['name']),
L.take(2),
print)
# ["ID", "HA"]
# 3번만 반복

L.strict

L.strict를 이용하여 지연 평가를 동작시킬 것인가를 동적으로 변경할 수 있습니다.

숫자로 하기:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
strict_or_lazy1 = __(
_.range,
L.strict(100),
L.map(lambda v, *r: v * v),
L.filter(lambda v, *r: _.bool(v % 2)),
L.take(10),
print)
strict_or_lazy1(50)
# [1, 9, 25, 49, 81, 121, 169, 225, 289, 361]
# 50 번 반복 (염격)
strict_or_lazy1(100)
# [1, 9, 25, 49, 81, 121, 169, 225, 289, 361]
# 20 번 반복 (지연)
strict_or_lazy1(15)
# [1, 9, 25, 49, 81, 121, 169]
# 15 번 반복 (엄격)
함수로 하기:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
strict_or_lazy2 = __(
_.range,
L.strict(lambda list, *r : len(list) < 100),
L.map(lambda v, *r : v * v),
L.filter(lambda v, *r : bool(v % 2)),
L.take(10),
print)
strict_or_lazy2(50)
# [1, 9, 25, 49, 81, 121, 169, 225, 289, 361]
# 50 번 반복 (염격)
strict_or_lazy2(100)
# [1, 9, 25, 49, 81, 121, 169, 225, 289, 361]
# 20 번 반복 (지연)
strict_or_lazy2(15)
# [1, 9, 25, 49, 81, 121, 169]
# 15 번 반복 (엄격)