상위 목록: 하위 목록: 작성 날짜: 읽는 데 9 분 소요

필터(filter) 함수

필터(filter) 함수는 반복 가능한 데이터(목록, 사전 등)의 요소에서 특정 조건을 만족하는 값들만 추출합니다.

필터 함수는 결괏값 = filter(조건 함수, 범위)의 형태로 사용합니다.

조건 함수는 논리값(True or False)을 반환하는 함수를 사용하며, 결과가 참(True)이 되는 원소만 반환합니다.

Python 3에서 필터 함수는 List가 아닌 반복자(iterator)를 반환하므로, lambda 함수와 list 함수 등을 같이 사용하여 결과를 확인합니다.


데이터에서 조건에 맞는 것만 골라내는 작업은 프로그래밍에서 가장 흔한 작업 중 하나입니다. 양수만 고르기, 빈 값 제거하기, 점수가 70점 이상인 학생만 고르기 등이 모두 여기에 해당합니다.

이 작업은 for 문과 if 문으로도 작성할 수 있지만, 필터 함수를 사용하면 “무엇을 고를지”(조건 함수)만 작성하고 “어떻게 반복할지”는 필터 함수에 맡길 수 있습니다.

필터 함수처럼 함수를 인수로 받는 함수를 고차 함수(Higher-order function)라 하며, 제 20강의 일급 함수와 제 21강의 람다 함수를 실제로 활용하는 대표적인 예입니다.

이번 강좌에서는 필터 함수의 사용법과 함께, 필터 함수가 반환하는 반복자가 값을 필요할 때마다 계산하는 지연 평가(lazy evaluation) 방식으로 동작한다는 점과 그 장단점, 그리고 리스트 간소화와 비교해 언제 무엇을 쓰는지 알아봅니다.



함수 생성

f = lambda x: x > 0

print(list(filter(f, range(-5, 5))))
결과
[1, 2, 3, 4]

filter(함수, 범위)를 사용하여 참(True)의 결과를 반환합니다.

범위의 값을 함수에 대입해 참이 되는 값만을 묶어 반환합니다.

range(-5, 5)는 -5부터 4까지의 정수이며, 각 값이 f에 전달되어 -5 > 0, -4 > 0, … 4 > 0이 차례로 계산됩니다. 그중 True가 된 1, 2, 3, 4만 남습니다.

결과에 남는 것은 조건 함수의 반환값(True)이 아니라 원래 원소의 값이라는 점에 주의합니다.

list() 또는 set() 등의 구문을 추가하지 않으면 <filter object at 0x00000194188A1450> 등의 iterator 형태로 출력됩니다. (메모리 주소는 실행할 때마다 달라집니다.)


result = []

for x in range(-5, 5):
    if x > 0:
        result.append(x)

print(result)
결과
[1, 2, 3, 4]

위의 필터 함수는 이 반복문과 같은 일을 합니다.

반복문으로 작성하면 결과를 담을 빈 리스트 만들기, 반복하기, 조건 검사하기, 추가하기의 네 단계를 매번 작성해야 하지만, 필터 함수는 이 중 조건 검사 부분만 함수로 전달하면 됩니다.



함수를 인수로 전달

필터 함수의 첫 번째 인수는 함수 객체 그 자체입니다. 필터 함수는 전달받은 함수를 저장해 두었다가, 원소를 하나 꺼낼 때마다 조건 함수(원소) 형태로 대신 호출합니다.

def is_positive(x):
    return x > 0


print(list(filter(is_positive, [3, -1, 0, 7])))

try:
    filter(is_positive(), [3, -1, 0, 7])
except TypeError as e:
    print(e)
결과
[3, 7]
is_positive() missing 1 required positional argument: ‘x’

filter(is_positive, ...)는 괄호 없이 함수 이름만 전달합니다. 필터 함수 안에서 is_positive(3), is_positive(-1), is_positive(0), is_positive(7)이 차례로 호출되어 True인 3, 7만 남습니다.

반면, filter(is_positive(), ...)처럼 괄호를 붙이면 필터 함수에 전달되기 전에 함수가 인수 없이 먼저 호출되므로 TypeError가 발생합니다.

즉, 함수를 인수로 넘긴다는 것은 “이 규칙으로 판단해 달라”는 판단 방법 자체를 넘기는 것이고, 언제 몇 번 호출할지는 받는 쪽(필터 함수)이 결정합니다.

  • Tip : 조건 함수는 원소 하나를 받는 함수여야 합니다. 매개변수가 두 개 이상인 함수를 전달하면 원소를 꺼낼 때 TypeError가 발생합니다.



반복자(iterator) 특성

f = filter(lambda x: x > 0, range(-2, 3))

print(f)
print(list(f))
print(list(f))
결과
<filter object at 0x0000020913444280>
[1, 2]
[]

필터 함수가 반환하는 filter 객체는 반복자(iterator)이므로, 값을 미리 계산해 두지 않고 꺼낼 때마다 하나씩 계산합니다.

그러므로, 한 번 끝까지 꺼낸 반복자는 비어있게 되어 다시 list()로 변환하면 빈 리스트가 반환됩니다.

결과를 여러 번 사용해야 한다면 list() 등으로 변환해 변수에 저장한 뒤 사용합니다.


반복자는 다음 값을 하나씩 꺼내 주는 객체입니다. next(반복자)를 호출할 때마다 다음 값을 하나 반환하고, 더 이상 꺼낼 값이 없으면 StopIteration 예외를 발생시켜 끝났음을 알립니다.

for 문과 list()는 내부적으로 next()를 반복 호출하다가 StopIteration을 만나면 멈추는 방식으로 동작합니다.

f = filter(lambda x: x > 0, [-1, 5])

print(next(f))
print(next(f, "끝"))

try:
    next(f)
except StopIteration:
    print("StopIteration")
결과
5
끝
StopIteration

첫 번째 next(f)는 -1을 검사해 건너뛰고 5를 검사해 반환합니다.

두 번째 next(f, "끝")은 더 이상 원소가 없으므로, 두 번째 인수로 전달한 기본값 "끝"을 반환합니다.

세 번째 next(f)는 기본값이 없으므로 StopIteration이 발생합니다. 반복자는 앞으로만 진행하며, 처음으로 되돌리는 기능이 없습니다.


f = filter(lambda x: x > 0, [-1, 5, -3, 8])

for value in f:
    print(value)
결과
5
8

반복자는 for 문에 바로 사용할 수 있습니다. 결과를 한 번만 순서대로 사용한다면 list()로 변환할 필요 없이 이렇게 반복하는 것이 가장 자연스럽습니다.



지연 평가(lazy evaluation)

지연 평가(lazy evaluation)란 계산을 결과가 실제로 필요한 시점까지 미루는 방식입니다.

필터 함수를 호출하는 순간에는 아무 원소도 검사하지 않고, 값을 꺼내 달라는 요청(next())이 올 때마다 필요한 만큼만 조건 함수를 호출합니다.

def is_even(x):
    print("검사 :", x)
    return x % 2 == 0


f = filter(is_even, [1, 2, 3, 4])
print("filter 생성 완료")

print(next(f))
print(next(f))
결과
filter 생성 완료
검사 : 1
검사 : 2
2
검사 : 3
검사 : 4
4

결과를 한 줄씩 읽으면 동작 순서를 확인할 수 있습니다.

  1. filter(is_even, ...)를 호출해도 검사 메시지가 출력되지 않습니다. 이 시점에는 조건 함수가 한 번도 호출되지 않았습니다.
  2. 첫 번째 next(f)가 호출되자 1을 검사(거짓이므로 건너뜀)하고, 2를 검사(참)한 뒤 2를 반환하고 멈춥니다.
  3. 두 번째 next(f)에서 멈췄던 위치부터 다시 진행하여 3, 4를 검사하고 4를 반환합니다.

즉, 필터 객체는 어디까지 검사했는지만 기억하고 있다가, 요청이 오면 다음 참인 값을 찾을 때까지만 진행합니다.


import sys

nums = range(1_000_000)

lazy = filter(lambda x: x % 3 == 0, nums)
eager = [x for x in nums if x % 3 == 0]

print(sys.getsizeof(lazy))
print(sys.getsizeof(eager))
print(len(eager))
결과
48
2926776
333334

sys.getsizeof()는 객체가 차지하는 메모리 크기를 바이트 단위로 반환합니다. (값은 Python 버전과 운영체제에 따라 다를 수 있으며, 위 결과는 64비트 Windows의 Python 3.14.6 기준입니다.)

리스트 간소화로 만든 eager는 조건을 만족하는 333334개의 값을 모두 미리 만들어 저장하므로 약 2.9MB를 차지합니다. 이마저도 리스트가 원소를 가리키는 공간만 계산한 값이며, 정수 객체 자체의 크기는 포함되지 않습니다.

반면, 필터 객체 lazy는 원소 개수와 관계없이 48 바이트입니다. 필터 객체가 저장하는 것은 조건 함수, 원본 반복자, 현재 위치 정도이기 때문입니다.

데이터가 크거나, 결과를 한 번만 순서대로 사용한다면 반복자를 그대로 사용하는 것이 메모리에 유리합니다.


from itertools import count

multiples = filter(lambda x: x % 7 == 0, count(1))

print(next(multiples), next(multiples), next(multiples))
결과
7 14 21

itertools.count(1)은 1, 2, 3, ...을 끝없이 만들어 내는 반복자입니다.

리스트는 끝이 없는 데이터를 저장할 수 없지만, 필터 함수는 필요한 만큼만 계산하므로 무한한 데이터에도 적용할 수 있습니다. 예제는 7의 배수를 처음부터 세 개만 꺼냅니다.

단, 무한 반복자에 list()를 사용하면 끝나지 않으므로 반드시 next()나 중단 조건이 있는 반복문으로 필요한 만큼만 꺼냅니다.



반복자 사용 시 주의점

f = filter(lambda x: x > 100, [1, 2, 3])

print(bool(f))
print(list(f))
결과
True
[]

필터 객체는 결과가 비어 있어도 bool()이 항상 True입니다.

필터 객체는 아직 아무것도 계산하지 않았으므로 결과가 비어 있는지 미리 알 수 없습니다. 그러므로, if filter(...):처럼 결과가 있는지 검사하면 항상 참으로 판단되는 실수가 발생합니다.

결과가 있는지 확인해야 한다면 list()로 변환한 뒤 검사하거나, any() 함수를 사용합니다.


data = [3, -1, 2]
f = filter(lambda x: x > 0, data)
print(list(f))
print(data)
결과
[3, 2]
[3, -1, 2]

필터 함수는 원본 데이터를 변경하지 않습니다. 원본에서 값을 읽기만 하고, 조건을 통과한 원소를 하나씩 내보낼 뿐입니다.

원본 자체에서 원소를 제거하고 싶다면 data = list(filter(...))처럼 결과를 다시 할당합니다.

  • Tip : 필터 객체를 만든 뒤 결과를 꺼내기 전에 원본 리스트를 수정하면, 수정된 내용이 결과에 반영됩니다. 필터 객체는 원본을 복사하지 않고 꺼낼 때 원본을 읽기 때문입니다.



사용자 정의 함수 사용

def func(x):
    if x > 0:
        return x
    else:
        return x - 100


print(list(filter(func, range(-5, 5))))
결과
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]

필터 함수는 함수의 반환값을 논리값으로 판단합니다.

그러므로, 0, None, 빈 문자열(""), 빈 리스트([]) 등을 제외한 모든 값은 참(True) 값으로 간주하게 됩니다.

예제의 함수는 x가 0일 때도 -100을 반환하는 등 어떤 경우에도 0을 반환하지 않으므로 모두다 참 값이 되어, 범위를 그대로 반환하게 됩니다.

조건 함수가 True/False가 아닌 값을 반환해도 오류가 발생하지 않으므로, 의도하지 않은 결과가 나왔다면 조건 함수가 무엇을 반환하는지부터 확인합니다.


def func(x):
    if (x / 2) - 1 > 0:
        return True
    elif x % 2 == 0:
        return True 
    else:
        return False


print(list(filter(func, range(-5, 5))))
결과
[-4, -2, 0, 2, 3, 4]

사용자 정의 함수를 이용한다면, 복잡한 형태의 필터 함수를 쉽게 구현할 수 있습니다.

예제는 x / 2 - 1 > 0, 즉 x가 2보다 큰 값이거나, 짝수인 값을 남깁니다. -4, -2, 0, 2는 짝수 조건으로, 3, 4는 첫 번째 조건으로 통과합니다.

반환값이 참인 경우에만 해당 값을 반환하므로, 특정한 형태의 데이터만 추출할 수 있습니다.

조건이 여러 개이거나 여러 줄에 걸친 판단이 필요하다면, 람다 함수 대신 이렇게 이름 있는 함수로 작성하는 것이 읽기 쉽고 따로 시험하기도 쉽습니다.

  • Tip : 반환값이 없는 경우 None이 반환되므로, 거짓(False) 값으로 간주해 필터 함수에서 값이 반환되지 않습니다.



None 필터

data = [0, 1, "", "a", None, [], [0]]

print(list(filter(None, data)))
결과
[1, ‘a’, [0]]

조건 함수 자리에 None을 전달하면, 원소 자체를 논리값으로 판단하여 참으로 간주되는 원소만 반환합니다.

[0]은 0을 포함하고 있지만 비어있지 않은 리스트이므로 참으로 간주됩니다.

입력 데이터에서 빈 문자열이나 None 같은 빈 값을 한 번에 제거할 때 자주 사용합니다. 단, 숫자 0도 거짓이므로 함께 제거된다는 점에 주의합니다.


data = ["10", "abc", "7", "", "3.5"]

print(list(filter(str.isdigit, data)))
print(list(filter(len, data)))
결과
[‘10’, ‘7’]
[‘10’, ‘abc’, ‘7’, ‘3.5’]

조건 함수로 이미 존재하는 함수나 메서드를 그대로 전달할 수도 있습니다.

str.isdigit은 문자열이 모두 숫자인지 판단하므로 "10", "7"만 남습니다. "3.5"는 .이 포함되어 있어 거짓입니다.

len은 길이를 반환하므로, 길이가 0인 빈 문자열만 거짓으로 판단되어 제거됩니다.



사전 필터링

scores = {"A": 90, "B": 60, "C": 75}

print(list(filter(lambda k: scores[k] >= 70, scores)))
print(dict(filter(lambda kv: kv[1] >= 70, scores.items())))
print({k: v for k, v in scores.items() if v >= 70})
결과
[‘A’, ‘C’]
{‘A’: 90, ‘C’: 75}
{‘A’: 90, ‘C’: 75}

사전을 필터 함수에 그대로 전달하면 키만 반복되므로, 조건 함수는 키를 받습니다. 첫 번째 줄은 키로 값을 찾아 비교한 뒤, 조건을 만족하는 키 목록을 반환합니다.

키와 값이 함께 필요하다면 scores.items()를 전달합니다. 조건 함수는 ("A", 90) 같은 Tuple을 받으므로 kv[1]로 값을 비교하고, 남은 쌍을 dict()로 다시 사전으로 만듭니다.

세 번째 줄의 사전 간소화(Dict Comprehension)는 같은 결과를 kv[1] 같은 색인 없이 이름으로 작성할 수 있어 더 읽기 쉽습니다.



조건의 반대(filterfalse)

from itertools import filterfalse

data = range(-2, 3)

print(list(filter(lambda x: x > 0, data)))
print(list(filterfalse(lambda x: x > 0, data)))
결과
[1, 2]
[-2, -1, 0]

itertools 모듈의 filterfalse() 함수는 필터 함수와 반대로 조건 함수의 결과가 거짓(False)인 원소만 반환합니다.

조건을 만족하는 값과 만족하지 않는 값을 둘 다 따로 사용해야 할 때, 같은 조건 함수를 두 함수에 전달하여 나눌 수 있습니다.

filterfalse()도 필터 함수처럼 반복자를 반환합니다.



간소화와 비교

print(list(filter(lambda x: x > 0, range(-5, 5))))
print([x for x in range(-5, 5) if x > 0])
결과
[1, 2, 3, 4]
[1, 2, 3, 4]

필터 함수는 [값 for 변수 in 범위 if 조건] 형태의 리스트 간소화(List Comprehension)로도 동일하게 구현할 수 있습니다.

조건에 lambda 함수를 새로 작성해야 한다면 간소화 구문이 더 읽기 쉬운 경우가 많으며, 이미 정의된 함수를 그대로 전달할 때는 필터 함수가 간결합니다.

간소화 구문은 즉시 List를 생성하고, 필터 함수는 반복자를 반환하므로 값을 필요할 때마다 계산합니다. 반복자처럼 동작하는 간소화가 필요하다면 (x for x in 범위 if 조건) 형태의 제너레이터 표현식을 사용합니다.


gen = (x for x in range(-5, 5) if x > 0)

print(gen)
print(sum(gen))
print(sum(filter(lambda x: x > 0, range(-5, 5))))
결과
<generator object <genexpr> at 0x0000025F308C0E10>
10
10

제너레이터 표현식은 소괄호로 감싼 간소화 구문이며, 필터 객체처럼 지연 평가되는 반복자를 만듭니다. 출력하면 값 대신 generator 객체가 표시됩니다.

sum()처럼 반복 가능한 객체를 받는 함수에 전달하면, 중간 리스트를 만들지 않고 값을 하나씩 꺼내 더하므로 메모리를 절약할 수 있습니다.


세 가지 방식은 다음 기준으로 선택합니다.

방식 결과 사용하기 좋은 경우
filter(함수, 범위) 반복자 (지연 평가) str.isdigit, None처럼 이미 있는 함수로 거를 때
[x for x in 범위 if 조건] 리스트 (즉시 계산) 조건을 새로 작성하고, 결과를 여러 번 사용할 때
(x for x in 범위 if 조건) 반복자 (지연 평가) 조건을 새로 작성하고, 결과를 한 번만 순서대로 사용할 때




정리

이름 설명 반환값/특징
filter(함수, 범위) 함수의 결과가 참인 원소만 선택 filter 반복자, 원본 유지
filter(None, 범위) 원소 자체가 참인 원소만 선택 빈 값과 0 제거
itertools.filterfalse(함수, 범위) 함수의 결과가 거짓인 원소만 선택 반복자
next(반복자, 기본값) 반복자에서 다음 값을 하나 꺼냄 끝이면 기본값 또는 StopIteration
list(반복자) 반복자의 남은 값을 모두 꺼내 리스트로 만듦 이후 반복자는 비어 있음


  • 필터 함수에는 함수를 호출하지 않고 함수 객체 그대로 전달합니다.
  • 필터 객체는 꺼낼 때마다 계산하므로 메모리를 적게 사용하고 무한한 데이터에도 쓸 수 있지만, 한 번만 사용할 수 있고 비어 있는지 미리 알 수 없습니다.
  • 결과를 여러 번 사용하거나 길이를 알아야 한다면 list()로 변환해 저장합니다.

댓글 남기기