파이썬에서 요런 범위를 판단하는 코드를 작성해본적이 있을거에용.
a = 10
b = 20
c = 15
if a < c < b:
print("범위내 포함!")
이 코드를 좀더 Pythonic 하게 작성하는 방법에 대해 알아봐요.
먼저 파이썬은 객체 지향 언어입니다.
여기서 우리는 판단할 범위를 클래스로 만들어 줄거에용
class MyRange:
def __init__(self, min, max):
self.min = min
self.max = max
ma_range = MyRange(10,15)
print(ma_range.min) # 10
print(ma_range.max) # 15
자 ! 마레인지가 완성되었습니다.!
min과 max가 잘 출력 되고있습니다.
그러면 우리가 원하는 범위내에 포함되는 판단을 할수 있을까용?
print( 1 in ma_range)
print( 15 in ma_range)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 1
----> 1 print( 1 in ma_range)
2 print( 15 in ma_range)
TypeError: argument of type 'MyRange' is not iterable
엥 ? 왜 안대용?
우리는 여기서 에러메세지만 보면 되용
TypeError: argument of type 'MyRange' is not iterable
MyRange는 iterable이 아니다.
맞아용 우리는 Myrange 클래스를 iterable하게 선언하지 않았습니다!
고쳐줍시다!
class MyRange:
def __init__(self, min, max):
self.min = min
self.max = max
def __iter__(self):
return iter(range(self.min, self.max+1))
짠 이렇게하고 다시한번 해보죵
print( 1 in ma_range)
print( 15 in ma_range)
False
False
잘나와용~~ 근데 이렇게만하면 안되겠죠?
현재의 작은 단위에선 괜찮은데 우리 마렌지가 1부터10만이 된다면 어떻게 될까요? 속도로 확인해보죵
ma_range = MyRange(1, 100_000) # 화폐단위 구분에 쓰면 좋~습니다
start = time.time()
for i in range(10_000):
(i+999_990) in ma_range
end = time.time()
print("Range :", f"{end - start:.6f}초")
Range : 15.263454초
세상에 15초나 걸렸어요 ㄷㄷㄷㄷ
15초면 우사인 볼트가 100m결승선을 들어오고도 약 5~6초가 남는 시간인데 , 컴퓨터가 그러면 안되겠죵?
우리는 __contains__를 선언해서 확인하는방법을 써볼거에요.
class MyRangeContains:
def __init__(self, min, max):
self.min = min
self.max = max
def __contains__(self, value):
return self.min <= value <= self.max
ma_con_range = MyRangeContains(1, 100_000)
start = time.time()
for i in range(10_000):
(i+999_990) in ma_con_range
end = time.time()
print("Contains:", f"{end - start:.6f}초")
Contains: 0.001277초
헉? 어째서 이렇게 차이가 많이나죠?
이유는 in 구문의 작동원리에 있습니다.
파이썬에서 in 구문은 __contains__를 먼저 찾고 없으면 __iter__를 찾아서 비교합니다.
두개의 시간복잡도를 비교하자면 이런 차이입니다.
__contains__
위 MyRangeContains 클래스에서 __contains__를 min <= x <= max로 선언했기때문에
x값이 범위 안에 있는지만 멤버십 개념으로 확인합니다.
이 경우에 시간복잡도는 O(1) 이 됩니다.
__iter__
위 MyRange 클래스 에는 선언해준 __contains__ 이 없기때문에
x in MyRange(min, max)를 하면 파이썬은 내부적으로 __iter__ 즉, min~max 까지의 모든값으로 반복을 통해 찾습니다.
이 경우에 시간복잡도는 O(n) 이 되어버립니다.
! 참고 !
Python의 내장 range 객체는 O(1) 멤버십 체크를 제공하기 때문에 O(1) 입니다.
하지만, 우리가 `iter(range(min, max + 1))`을 사용하면 Python이 range 객체를 직접 탐색하는 것이 아니라 이터레이터를 순차적으로 순회하면서 값을 찾게 되므로 O(n)이 됩니다.
오! 그러면 __contains__ 를 쓰면 만능이네요?
아니에용 __contains__ 만 가진 클래스는 iterable 객체가 아니기 때문에 iterable로 동작하는 기능을 사용하려고했을때 오류가 납니당.
예시로 sum()을 사용할수없어요.
이 경우 해당 클래스에서 합산을 하려면 아래와 같이 나올거에용
###################################################
# MyRange를 사용한 합산
start = time.time()
total_range = sum(my_range)
end = time.time()
print(f"MyRange를 사용한 sum() 시간: {end - start:.6f}초")
MyRange를 사용한 sum() 시간: 0.008538초
###################################################
# sum()을 `__contains__` 기반으로 구현하면?
start = time.time()
total_con_range = 0
sum_contains = sum(x for x in range(1, 100_001) if x in my_range_contains)
end = time.time()
print(f"MyRangeContains를 사용한 합산 시간: {end - start:.6f}초")
MyRangeContains를 사용한 합산 시간: 0.132346초
contains만 선언된 클래스는 iterable한 객체가 아니기 때문에 `sum(my_range)` 같은 연산을 사용할 수 없습니다.
이 경우 불필요한 시간이 소요 됩니다.
이것을 보면, __contains__만 선언된 경우 전체 순회 작업에서 성능이 크게 불리하다는 사실을 알 수 있습니다.
어? 저건 너무 비효율적인 코드 같은데 range를 쓰면 되지 않나요?
맞습니다! 아래와 같이 하면 똑같은 동작을 합니다.
sum(range(my_con_range.min, my_con_range.max + 1))
하지만, 위쪽에서 보여드린 예시는 range 없이 __iter__가 없는 경우 어떻게 동작하는지 보여주기 위한 구현입니다.
즉, sum(range(...)) 을 사용하면 직접 연산할 수 있지만, 객체가 이터러블이 아니라는 근본적인 문제를 해결하지 못합니다.
따라서 매번 sum(range(...))을 사용하는 것은 Pythonic하지 않으며,
대신 __iter__를 구현하는 것이 가장 이상적인 해결책입니다.
그러면 iterable한것도 필요하고 in 구문에서도 유리한 pythonic한 방법은 어떤것일까요?
class MyRange:
def __init__(self, min, max):
self.min = min
self.max = max
def __iter__(self):
return iter(range(self.min,self.max+1))
def __contains__(self, value):
return self.min <= value <= self.max
final_range = MyRange(1, 100_000)
start = time.time()
print(sum(final_range))
end = time.time()
print(f"{end-start:.6f}초")
#0.001018초
start = time.time()
for i in range(10_000):
(i+999_990) in final_range
end = time.time()
print(f"{end-start:.6f}초")
#0.001067초
이것을 보면, `__contains__`만 선언된 경우 전체 순회 작업에서 성능이 크게 불리하다는 사실을 알 수 있습니다.
'Python > core' 카테고리의 다른 글
Pydantic, Dataclass, TypedDict는 언제 쓰는 게 좋을까? (1) | 2025.03.27 |
---|