Partial.py는 함수형 파이썬을 더 많은 영역에서 사용하고자, 몇 가지 기능을 확장한 라이브러리입니다. Partial.py는 부분 적용, 파이프라인, 비동기 제어 등의 기능을 제공하고 있습니다.
$ git clone https://github.com/marpple/partial.py.git
$ cd partial.py
$ sudo python setup.py install
혹은
$ pip install partial.py
네임스페이스로 사용할 _
, __
, ___
를 불러옵니다.
from partial import _, __, ___
Partial.py는 이름처럼 부분 적용(Partial application)을 중요하게 생각합니다. 기존의 _.partial
함수는 왼쪽에서부터만 인자를 적용해둘 수 있습니다. Partial.py의 _.partial
함수는 맨 오른쪽 인자나 맨 오른쪽에서 두 번째에만 인자를 적용해두는 것도 가능하며, 새로운 구분자인 ___
를 활용하여 중간 지점에 인자가 가변적으로 적용되도록 비워둘 수 있습니다.
_.partial
을 실행하면서 인자 자리에 _
를 넘기면 부분 적용할 인자를 건너띌 수 있습니다. _
를 이용하면 원하는 곳에만 인자를 부분 적용해둘 수 있습니다. _
가 있는 자리는 이후 실행시 채워집니다.
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
을 실행하면 ___
를 기준으로 왼편의 인자들을 왼쪽부터 적용하고 오른편의 인자들을 오른쪽부터 적용할 준비를 해둔 함수를 리턴합니다. 부분 적용된 함수를 나중에 실행하면 그때 받은 인자들로 왼쪽과 오른쪽을 먼저 채운 후, 남은 인자들로 가운데 ___
자리를 채웁니다.
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
을 _
로 간결하게 표현할 수 있습니다.
def add (a, b):
return a + b
add10 = _(add, 10)
print( add10(5) )
# 15
파이프라인 함수인 _.pipe
, _.go
등은 작은 함수들을 모아 큰 함수를 만드는 함수입니다. 파이프라인으로 함수를 조합하면 왼쪽에서부터 오른쪽, 위에서부터 아래로 표현되어 읽기 쉬운 코드가 됩니다. 체인 방식과 다르게 아무 함수나 사용할 수 있어 자유도가 높습니다. 작은 함수들을 인자와 결과만을 생각하면서 조합하면 됩니다.
_.go
는 파이프라인의 즉시 실행 버전입니다. 첫 번째 인자로 받은 값을 두 번째 인자로 받은 함수에게 넘겨주고 두 번째 인자로 받은 함수의 결과는 세 번째 함수에게 넘겨주는 것을 반복하다가 마지막 함수의 결과를 리턴해줍니다.
_.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개 이상의 인자들을 전달할 수 있습니다.
_.go(10, # 첫 번째 함수에서 사용할 인자
lambda a: _.mr(a * 10, 50), # 두 개의 값을 리턴
lambda a, b: a - b, # 두 개의 인자 받기
lambda a: a + 10)
# 60
_.go
의 첫 번째 인자는 두 번째 인자인 함수가 사용할 인자고 두 번째 부터는 파이프라인에서 사용할 함수들입니다. _.go
의 두 번째 인자인 함수, 즉 최초 실행될 함수에게 2개 이상의 인자를 넘기고자 한다면 _.mr
을 사용하면 됩니다. _.mr
로 인자들을 감싸서 넘겨주면, 다음 함수는 인자를 여러 개로 펼쳐서 받게 됩니다.
_.go(_.mr(2, 3),
lambda a, b: a + b, # 2 + 3
lambda a: a * a)
# 25
_.go
를 이미 정의되어 있는 함수와 조합하면 더욱 읽기 좋아집니다.
def add(a, b):
return a + b
def square(a):
return a * a
_.go(_.mr(2, 3), add, square)
# 25
_.go
가 즉시 실행하는 파이프라인이라면 _.pipe
는 실행할 준비가 된 함수를 리턴하는 파이프라인 함수입니다. 그외 모든 기능은 _.go
와 동일합니다.
f1 = _.pipe(add, square)
f1(2, 3)
# 25
Partial.py의 주요 함수들은 커링이 부분적으로 동작하도록 지원하고 있습니다. 아래는 일반적인 사용 모습입니다.
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의 주요 함수들은 부분 커링이 적용되어 위와 동일한 동작을 아래와 같이 간결하게 표현할 수 있습니다.
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]
부분 커링이 지원되면 파이프라인과 함께 사용할 때, 체인처럼 간결한 표현이 가능합니다.
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의 파이프라인은 결과를 여러 개로 리턴할 수 있고, 여러 개의 인자를 받을 수 있고, 다른 라이브러리에 있는 함수든, 직접 만든 함수든, 익명 함수든 모두 쉽게 사용할 수 있습니다.
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
등의 파이프라인 함수들은 비동기 제어를 지원합니다.
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의 함수를 이용하면 비동기 상황에서도 동기 상황과 동일한 코드를 작성할 수 있고, 비동기 함수와 동기 함수의 조합도 가능합니다.
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
등의 함수들에서 자동 비동기 제어 로직을 지원합니다.
Partial.py의 L
을 이용하면, 파이프라인 내부에서 함수들의 실행 순서를 재배치하여 적절하게 평가를 지연합니다. 사용법은 간단합니다. Partial.py에서 L
을 import
하면 됩니다. L
을 통해 지연 평가할 영역을 명시적으로 선택할 수 있습니다. _.go, _.pipe
등의 파이프라인이 L
로 시작하여 L
로 끝날 때까지의 함수들을 재배치하여 성능을 개선합니다.
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번
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.js의 지연 평가 지원 함수로는 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
, 입니다.
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
를 이용하여 지연 평가를 동작시킬 것인가를 동적으로 변경할 수 있습니다.
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 번 반복 (엄격)
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 번 반복 (엄격)