흔한말 (collection)/Python

python filter 로 게으르게 커피한잔 하고 가세요! 향이 좋아요!

hnanmal 2022. 3. 10. 02:51
300x250

filter라는 함수가 있습니다.
파이썬에만 있는 함수는 아닌데요.
사용법은 다들 유사합니다.

 

커피필터는 갈아낸 커피가루를 받아들여서 물에 녹는 성분들만 아래로 통과시킵니다.

 

이를 함수로 나타내면

커피한잔 = 커피필터(수용성성분만_골라냅시다!, 커피가루)

정도가 되겠죠?

 

원본리스트에서 원하는 부분집합을 추출해주는 함수가 바로 filter() 함수입니다.

추출리스트 = filter(조건, 원본리스트)

아까 커피의 예와 비슷하게 써보면 이렇게 되겠죠?

 

원본리스트의 원소 중에서 조건을 만족하는 원소만 반환한다.
이렇게 독해하시면 됩니다.

 

그러면 코드예제를 통해 어떻게 쓰는지 알아봅시다.

 

커피로 배워보는 filter 함수()

아까 커피의 예를 들어서 설명했으니 컨셉을 계속 끌고 가보도록 하죠.
일단은 커피가 어떤 성분으로 이루어 져 있는지, 간단한 데이터를 한번 준비해 봤습니다.

 

커피가루 준비 하기

커피가루 = [
    {'이름': '카페인', '용해성': '수용성', '로스팅후변화': False},
    {'이름': '광물질', '용해성': '불용성', '로스팅후변화': False},
    {'이름': '지방', '용해성': '지용성', '로스팅후변화': False},
    {'이름': '탄닌산', '용해성': '수용성', '로스팅후변화': True},
    {'이름': '클로로겐산', '용해성': '수용성', '로스팅후변화': True},
    {'이름': '트리고넬린', '용해성': '수용성', '로스팅후변화': False},
    {'이름': '탄산가스', '용해성': '불용성', '로스팅후변화': True},
    {'이름': '당분', '용해성': '수용성', '로스팅후변화': True},
    {'이름': '섬유소', '용해성': '불용성', '로스팅후변화': True},
        ]

위의 데이터를 보면 총 9가지의 성분으로 커피가루가 이루어져있다는 것을 알 수 있습니다.
(물론 예제를 위해 단순화, 변형 시킨 데이터이기 때문에 실제 커피와 완전히 동일하다고 이해하시면 안됩니다..)

 

우리가 커피를 끓여 먹게 되면 이중에서 수용성 성분만 추출된다고 보면 되겠죠?
커피한잔 내려 봅시다!

 

아까 filter는 조건과 원본리스트를 인자로 받는다고 했었죠?
그렇다면 가장 먼저 조건에 해당하는 함수를 만들어 줄 필요가 있습니다.

def 수용성인가(성분):
    return 성분['용해성'] == '수용성'

 

수용성인가()함수를 만들어서, 어떠한 성분을 투입했을 때,
해당성분이 수용성이면 True를, 수용성이 아니라면 False를 반환하도록 했습니다.

성분1 = {'이름': '광물질', '용해성': '불용성', '로스팅후변화': False}
성분검사결과 = 수용성인가(성분1)
print(성분검사결과)
False

광물질로 예를 들어봤는데 이해 되시죠?
이렇게 필터는 어떤 객체를 검사하여 True, False를 반환해주는 형식의 함수이면 됩니다.

 

커피한잔 = filter(수용성인가, 커피가루)
print(커피한잔)
<filter object at 0x7f25c370ead0>

커피한잔을 filter로 추출해보니, 코드실행은 잘 되는 데 결과값이 이상하죠?
<filter object at ~> 이라는 문구는 뭘까요?

 

filter 함수는 원본리스트의 항목중에 조건에 부합하는 항목을 반환해주는데, 한가지 특징이 있습니다.

 

바로 lazy evaluation을 지원하는 filter 객체로 결과를 반환한다는 것이죠.
이 특성은 map()함수도 마찬가지인데, 지연 평가에 대해서 밑에서 좀 더 설명하겠습니다.

 

일단 우리가 이해할 수 있는 list 객체로 바꾸려면 아래와 같이 코드 입력하면 됩니다.

list(커피한잔)
[{'이름': '카페인', '용해성': '수용성', '로스팅후변화': False},
 {'이름': '탄닌산', '용해성': '수용성', '로스팅후변화': True},
 {'이름': '클로로겐산', '용해성': '수용성', '로스팅후변화': True},
 {'이름': '트리고넬린', '용해성': '수용성', '로스팅후변화': False},
 {'이름': '당분', '용해성': '수용성', '로스팅후변화': True}]

수용성인 성분들만 잘 골라져서 나왔죠?

 

지연평가? Lazy Evaluation? 게을러?

지연평가라는 개념은요. 평가를 늦추고 필요할 때 값을 계산한다는 개념입니다.
파이썬에 국한된 개념은 아니고요. 일단은 함수형 프로그래밍이라는 패러다임과 연관되어 있다고 알고 계셔요.


이걸 이해하려면, lazy(게으른) 하지 않은 케이스를 먼저 봐야 합니다.

대표적인 것이 바로 list입니다.
list는 Container 중 순서를 갖는 Sequence 객체 입니다.
여러가지 원소들을 갖는 다는 것은 직관적으로 이해가 되죠?
리스트가 갖고 있는 모든 객체들은 전부다 이미 평가가 완료된 상태로 저장됩니다.

일단 예시로 0부터 10000까지의 수를 만들어볼까요?

import time
원본=list(range(1000))

이 각각의 숫자들을 "a의a제곱" 으로 연산해서 새로운 리스트를 만들어본다고 합시다.

결과 = []
for a in 원본:
    결과.append(a**a)
print(결과)
len(결과)
[1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489, 10000000000, 285311670611, 8916100448256, 302875106592253, 11112006825558016, 437893890380859375, 18446744073709551616, 827240261886336764177, 39346408075296537575424, 1978419655660313589123979, 104857600000000000000000000, 5842587018385982521381124421, 341427877364219557396646723584, 20880467999847912034355032910567, 1333735776850284124449081472843776, 88817841970012523233890533447265625, 6156119580207157310796674288400203776, ...

368063488259223267894700840060521865838338232037353204655959621437025609300472231530103873614505175218691345257589896391130393189447969771645832382192366076536631132001776175977932178658703660778465765811830827876982014124022948671975678131724958064427949902810498973271030787716781467419524180040734398996952930832508934116945966120176735120823151959779536852290090377452502236990839453416790640456116471139751546750048602189291028640970574762600185950226138244530187489211615864021135312077912018844630780307462205252807737757672094320692373101032517459518497524015120165166724189816766397247824175394802028228160027100623998873667435799073054618906855460488351426611310634023489044291860510352301912426608488807462312126590206830413782664554260411266378866626653755763627796569082931785645600816236891168141774993267488171702172191072731069216881668294625679492696148976999868715671440874206427212056717373099639711168901197440416590226524192782842896415414611688187391232048327738965820265934093108172054875188246591760877131657895633586576611857277011782497943522945011248430439201297015119468730712364007639373910811953430309476832453230123996750235710787086641070310288725389595138936784715274150426495416196669832679980253436807864187160054589045664027158817958549374490512399055448819148487049363674611664609890030088549591992466360050042566270348330911795487647045949301286614658650071299695652245266080672989921799342509291635330827874264789587306974472327718704306352445925996155619153783913237212716010410294999877569745287353422903443387562746452522860420416689019732913798073773281533570910205207767157128174184873357050830752777900041943256738499067821488421053870869022738698816059810579221002560882999884763252161747566893835178558961142349304466506402373556318707175710866983035313122068321102457824112014969387225476259342872866363550383840720010832906695360553556647545295849966279980830561242960013654529514995113584909050813015198928283202189194615501403435553060147713139766323195743324848047347575473228198492343231496580885057330510949058490527738662697480293583612233134502078182014347192522391449087738579081585795613547198599661273567662441490401862839817822686573112998663038868314974259766039340894024308383451039874674061160538242392803580758232755749310843694194787991556647907091849600704712003371103926967137408125713631396699343733288014254084819379380555174777020843568689927348949484201042595271932630685747613835385434424807024615161848223715989797178155169951121052285149157137697718850449708843330475301440373094611119631361702936342263219382793996895988331701890693689862459020775599439506870005130750427949747071390095256759203426671803377068109744629909769176319526837824364926844730545524646494321826241925107158040561607706364484910978348669388142016838792902926158979355432483611517588605967745393958061959024834251565197963477521095821435651996730128376734574843289089682710350244222290017891280419782767803785277960834729869249991658417000499998999]





1000

1000개의 결과가 자릿수까지 많으니 어지럽죠? 그런데 "결과"리스트 중에 163으로 나눈 나머지가 2인 숫자들만 필요하다고 하면 코드를 어떻게 짜면 될까요?

결과2 = []
for a in 결과:
    if a % 163 == 2:
        결과2.append(a)
print(결과2)
len(결과2)
[392581291184964384564039223372917857421740212596015828566681613036618488882450872057604984637483765699874590821980738332389799126728898050788336090899262412273890602247858026483485575985748925544028430405887599256647786606973246934407644393724485438842429470288142175516772682374441072511631091069708913415221677600277883009095628773136828378164303837845187322297297270913195618848128874392316821630952607378203102883074130758795897335549020179685586351932385665313412441291070180406758530926100178325593566883654430193268221950235941005987238201731113, 22532672065650436145387580246143384790918279632484561042949284216106822897997064973550548074693270449120018248341308258961791495170092792587191921677240052427834208980517773535784201833978482326749190038048787545982380684587376986006664875950334234293108866090378103207918160687295797346487190089331069989394003463154730218439174846943999850619798553576347110619473849135221107942168039780583981991084703371334613718596104815343874642280740078381451653642290040314891679910630650243481342910827181928084002315231813794967910091143613795541058640843777969822371421619699740314807678650559616535360671527870947760395059457, ...146448084678649019603485365058677719950705269981025716164074598793341444146935168690584120287067957604886597039461303518413576985744367317284745084232489728438268622984075455852070726868567322484515491021217739792225835961904808601876711073769966393208850540329484083070659543801959144869738941301241637538856948812850011287370216009335153668610247491888652798119664387239806270036293874608567004560458004603274510910591137568966671392950408310739658517385157469882554138972770744752587028773014681643399049675846708881324286987796420188521254778832990199885599743354668105225039612031629861604469680359922839168536971471457339163746195120350679377250811095328245741879297732810530701240926230344891076390136696398588834072665335195242783803489025598480894072269211021485828299826126629115261089314609433040582539758191694093062051503602500256024970453179525166379500340850758468401553403115072875087020282482055858596034559226339217902527917979097170792655635171612868903099935396439622391833437353391058178432789836855875445124678299647716913123845286444589674674628530921575962240378135393827577645667579938644362971344317476283557454172934095840068858355270955104351516203473712356694499886400872936832211635643169127421255482895250515544325256735781190932716352429067090982222859088332650657726859568991489737090439771433650381902210664390177343770335642859951385492811037609550963603799691148598138038185917069848142180024789886128136120230662702859203930164336612834240666857194420695549641206646852628877343120805187465294730795654164501338911534684293391945264258230788367399870077465995095184701679365363611357917588545906713573537894521352335678982983055840124214396282010151040914158107299732113463546804522047919647204790154334084437590327943338709300926419856155244737137656438550614663036142819473954057150880932686955574724155680704928979714054115822760948086057183620984869693664416113104467145816104544277922169263122620011212354411452421734735538616080729554026577765097811155384120233698833777161253430082280076079624414060676106890792359369743834222737352460664923720427329172642129487558973419264602957639610145678897853816091224032476446318687039455180348811971287808445782597298401770634871895733153017038634277276423605361677956245503799693685159137465668340807528799475960464004931083081828546710312366485595703125]





5

최종적으로 원하는 결과2는 원소의 개수가 5개인 리스트가 되었군요.

이렇게 놓고 보니 뭔가 좀 아쉽지 않나요?
최종적으로 산출되는 목록의 갯수는 고작 5개인데, 그 중간과정에 있는 리스트의 갯수는 1000개라서 꼼짝없이 1000번의 연산을 해야하는 것이니까요.
2단계로 진행되는 코드를 하나로 합친다음 코드 실행시간을 계산해볼까요?

import time
start = time.time()  # 시작 시간 저장

for a in 원본:
    if a**a % 163 == 2:
        결과.append(a**a)
print(결과)
len(결과)

print("소요시간 :", time.time() - start)  # 현재시각 - 시작시간 = 실행 시간
[1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489, 10000000000, 285311670611, 8916100448256, 302875106592253, 11112006825558016, 437893890380859375, 18446744073709551616, 827240261886336764177, 39346408075296537575424, 1978419655660313589123979, 104857600000000000000000000, 5842587018385982521381124421, 341427877364219557396646723584, 20880467999847912034355032910567, 1333735776850284124449081472843776, 88817841970012523233890533447265625, 6156119580207157310796674288400203776, 443426488243037769948249630619149892803, 33145523113253374862572728253364605812736, 2567686153161211134561828214731016126483469, 205891132094649000000000000000000000000000000, 17069174130723235958610643029059314756044734431, 1461501637330902918203684832716283019655932542976, 129110040087761027839616029934664535539337183380513, 11756638905368616011414050501310355554617941909569536, 1102507499354148695951786433413508348166942596435546875, 106387358923716524807713475752456393740167855629859291136, 10555134955777783414078330085995832946127396083370199442517, ...146448084678649019603485365058677719950705269981025716164074598793341444146935168690584120287067957604886597039461303518413576985744367317284745084232489728438268622984075455852070726868567322484515491021217739792225835961904808601876711073769966393208850540329484083070659543801959144869738941301241637538856948812850011287370216009335153668610247491888652798119664387239806270036293874608567004560458004603274510910591137568966671392950408310739658517385157469882554138972770744752587028773014681643399049675846708881324286987796420188521254778832990199885599743354668105225039612031629861604469680359922839168536971471457339163746195120350679377250811095328245741879297732810530701240926230344891076390136696398588834072665335195242783803489025598480894072269211021485828299826126629115261089314609433040582539758191694093062051503602500256024970453179525166379500340850758468401553403115072875087020282482055858596034559226339217902527917979097170792655635171612868903099935396439622391833437353391058178432789836855875445124678299647716913123845286444589674674628530921575962240378135393827577645667579938644362971344317476283557454172934095840068858355270955104351516203473712356694499886400872936832211635643169127421255482895250515544325256735781190932716352429067090982222859088332650657726859568991489737090439771433650381902210664390177343770335642859951385492811037609550963603799691148598138038185917069848142180024789886128136120230662702859203930164336612834240666857194420695549641206646852628877343120805187465294730795654164501338911534684293391945264258230788367399870077465995095184701679365363611357917588545906713573537894521352335678982983055840124214396282010151040914158107299732113463546804522047919647204790154334084437590327943338709300926419856155244737137656438550614663036142819473954057150880932686955574724155680704928979714054115822760948086057183620984869693664416113104467145816104544277922169263122620011212354411452421734735538616080729554026577765097811155384120233698833777161253430082280076079624414060676106890792359369743834222737352460664923720427329172642129487558973419264602957639610145678897853816091224032476446318687039455180348811971287808445782597298401770634871895733153017038634277276423605361677956245503799693685159137465668340807528799475960464004931083081828546710312366485595703125]
소요시간 : 0.25603342056274414

time 모듈을 로드한 뒤에, 간단하게 코드 실행시간을 계산하는 코드도 추가해보았습니다.
대략 0.22초 정도 걸리는 군요...

start = time.time()  # 시작 시간 저장

게으른결과 = list(filter(lambda b: b% 163 == 2, map(lambda a: a**a, 원본)))
게으른결과

print("소요시간 :", time.time() - start)  # 현재시각 - 시작시간 = 실행 시간
소요시간 : 0.015403032302856445

이번에는 게으른 연산이 적용되는 map과 filter 함수를 활용해 만든 동일한 로직을 가진 코드의 실행 시간입니다.
대략 0.02초 정도 걸립니다.
대략 11배 정도 빠른 속도네요.

 

왜 이런 차이가 날까요? 동일한 연산을 하는 코드일 텐데?

 

코드의 평가란??

이런현상의 이유를 알려면 앞에서 잠깐 언급한 지연평가라는 것의 의미를 알아야 합니다.

 

지연평가의 의미는 평가를 늦추고 필요할때만 그 결과를 계산한다는 의미입니다.
그렇다면 도대체 평가란 무엇일까요?
이 개념을 이해하려면 코드와 데이터의 차이를 이해해야 합니다.

start = time.time()
[1, 4, 16, 256, 65536, 4294967296, 18446744073709551616, 340282366920938463463374607431768211456, 115792089237316195423570985008687907853269984665640564039457584007913129639936, 13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096]
print("소요시간 :", time.time() - start)  # 현재시각 - 시작시간 = 실행 시간
소요시간 : 0.0001304149627685547

주피터노트북을 열어서 셀 하나에 하드코딩을 통해 위와 같은 리스트를 하나 써볼까요?
코드를 작성하고 shift + enter 키를 누르면 결과값이 입력한 코드와 동일한 리스트가 결과로 나오는 걸 볼 수 있습니다.
여기까지는 바로 이해가 되시죠? 쇼요시간은 약 0.0001초 정도 걸리네요. 하드코딩이라서 빠르게 평가됩니다.

start = time.time()
a = 2
b = [pow(a,0), pow(a,a**1), pow(a,a**2), pow(a,a**3), pow(a,a**4), pow(a,a**5), pow(a,a**6), pow(a,a**7), pow(a,a**8), pow(a,a**9)]
print(b)
print("소요시간 :", time.time() - start)  # 현재시각 - 시작시간 = 실행 시간
[1, 4, 16, 256, 65536, 4294967296, 18446744073709551616, 340282366920938463463374607431768211456, 115792089237316195423570985008687907853269984665640564039457584007913129639936, 13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096]
소요시간 : 0.0007984638214111328

그럼 이번에는 위와 같이 a 라는 변수에 2라는 값을 하나 선언한 뒤,
그 a를 활용해서 제곱 수를 늘려가는 리스트를 한번 작성해 보았습니다.


위쪽의 예제와 다른 점은 리스트의 원소를 사용자가 직접 작성해 준 것이 아니고,
a 변수의 값을 참조해서 규칙을 통해 각각의 원소를 계산하는 코드를 마지막에 컴퓨터가 "평가"해서 결과값을 돌려주게 됩니다.
그래서인지, 소요시간이 약 0.0007초 정도 걸립니다. 7배정도 걸리네요?

 

똑같은 리스트인데, 왜 첫번째는 결과가 빨리 나오고 두번째는 느리게 나올까요?

 

그 이유는 바로 작성된 코드를 평가하는데 걸리는 시간이 다르기 때문입니다.
첫번째 예에서는 리스트의 원소가 가져야 할 값을 사용자가 코드에 그대로 입력해버렸기 때문에,
딱히 시간이 필요한 연산과정이 없이 코드를 평가할 수 있습니다.

 

그런데 두번째 예에서는 a의 값을 가져와서, 계속 제곱수를 늘려가는 식으로 코드가 작성되었죠?
그렇기 때문에 코드에 작성된 규칙을 보고 값을 평가하는 것이 시간이 좀 걸리게 된 것입니다.

 

다시, 지연평가란?

앞의 예제에서는 print 명령어로 리스트를 출력할 때 10개의 원소를 미리 계산해서 보여주었습니다.
이게 일반적인 평가겠죠?

 

지연평가는, 이와 반대로, 리스트에 있는 모든 원소를 미리 평가해 두는 것이 아닙니다.
다른 코드에서 해당 리스트를 참조로 호출하기 전까지는 평가를 미뤄두다가,
필요한 시점에 원소 하나씩만 평가하는 방식을 지연평가라고 하는 것입니다.

 

그래서, 10만개의 원소가 있는 리스트에서 10개의 원소만 뽑아내서 처리하고자 한다면,
일반적인 평가를 하는 list 객체보다는
지연평가를 지원하는 map, filter 객체를 사용하는 것이 속도가 더 빠른 것이죠.

 

대신에 필요할때마다 원소를 꺼내 평가하기 때문에 (ex. pow(a,$a**2$)를 16 으로 평가)
print 와 같은 명령어로는 전체 리스트의 원소들의 계산값을 볼 수 없고,
그저 filter object 라는 이름의 객체명과 해당 객체가 들어가 있는 메모리 주소만 볼 수 있는 것입니다.

 

지연평가라는 이름에 겁먹지 말고, 그저 여러개의 객체를 불러들이는 다른 방법이 있고,
사람이 볼수 있게 하려면 list() 를 맨 바깥에 붙여주면 된다고 일단 이해하세요.

 

다시 커피로

filter 의 지연평가에 대해 어느 정도 이해가 되었다면, 다시 커피로 돌아갑시다.

이번에는 물 말고 기름으로 커피를 내려볼까요?

def 지용성인가(성분):
    return 성분['용해성'] == '지용성'

커피한잔 = filter(지용성인가, 커피가루)
print(list(커피한잔))
[{'이름': '지방', '용해성': '지용성', '로스팅후변화': False}]

위와 같이 지방 성분만 녹아나온 것을 확인할 수 있습니다!

 

lambda를 활용한 filter

filter에 들어갈 조건 함수를 매번 이름을 붙여가며 만들기는 조금 귀찮습니다. 그런 번거로움을 해소하기 위해 lambda 문법이 있는데요.
설명할 내용이 조금 있기 때문에 다음 포스팅으로 넘기고, 익명 함수를 만드는 방법으로 lambda를 기억하시면 됩니다.
lambda 문법 설명은 나중에 보시고, 지금은 일단 따라서 코드를 작성해보세요.

lambda a: a['용해성'] == '지용성'
<function __main__.<lambda>(a)>

상기의 코드는 그 자체로 함수 object를 나타냅니다.
그러니 앞에서 우리가 작성한 코드는 다음과 같이 작성해도 정확히 동일하게 작동합니다.

커피한잔 = filter(lambda a: a['용해성'] == '지용성', 커피가루)
print(list(커피한잔))
[{'이름': '지방', '용해성': '지용성', '로스팅후변화': False}]

"지용성인가" 부분이 람다식으로 대체 되었는데도 결과가 동일하죠?

 

이것의 의미는, 간단한 조건의 경우에는 매번 filter 문 바깥에서 조건 함수를 선언하고,
filter 문에서 조건함수의 이름을 적어 사용할 필요없이,

 

"filter 문 내부에 람다식을 쓰기만 하면 간단히 filter 를 완성할 수 있다" 라는 것입니다.

 

결론

filter, map, reduce 와 같은 함수형프로그래밍 패러다임에서 기인한 함수들은 모두 지연평가를 지원하는 객체를 반환하고,
우리가 리스트의 객체를 확인하려면, list()라는 코드를 최외각에 덧붙여야 합니다.

 

그런데, 우리가 코딩을 하면서 중간단계에 불과한 리스트들을 계속 생산하면서 논리를 전개해가는 알고리즘을 짠다면,
그 부분을 filter 와 map을 사용해서 성능을 높일 수 있다는 것은 알아두셔야 할 점입니다.

 

느긋하게 커피한잔 하시니 좀 기분이 나아지셨죠?
filter 와 map 함수의 활용법은 무궁무진한데, 개념을 설명하는 포스트에서는 여기까지만 설명하는 것이 좋아보입니다.

 

다음에는 reduce 함수 포스트에서 뵙겠습니다.

반응형