들어가기 전
- 이 글은 이펙티브 파이썬을 공부하며 작성한 글입니다.
- 코드 : https://github.com/chickenchickenlove/effective-python/tree/master/item45
요약
- 특정 어트리뷰트에 요구사항이 추가되고, 이를 호출하는 외부 클라이언트에 영향을 주지 않고 확장하고 싶은 경우 @property는 좋은 선택이 될 수 있음.
- @property를 사용해 데이터 모델을 점진적으로 개선할 수 있음.
- @property 메서드를 너무 과하게 쓰고 있다면, 클래스와 클래스를 사용하는 모든 코드를 리팩토링 하는 것을 고려해야 함.
- @property를 너무 많이 사용한 경우, 클래스를 분리하는 방식을 고려해봐야 함.
- 너무 거대한 @property는 읽기 어려워지고, 클래스로 분화시켜 필요한 기능들만 모아놓는 것이 더 응집력 있고, 가독성이 좋기 때문임.
Item45. 어트리뷰트를 리팩토링하는 대신 @property를 사용하라.
Item44에서 이야기한 것처럼 @property를 이용해 어트리뷰트를 제공하는 경우, 어트리뷰트에 부가 기능을 손쉽게 추가할 수 있다. 심지어 해당 어트리뷰트에 대한 상태를 인스턴스가 가지지 않고, 필요할 때 마다 계산하는 형태도 가능해진다. 이런 이유 때문에 @property를 이용한 어트리뷰트는 '점진적인 인터페이스의 개선'에 적합한 방법이 될 수 있다.
# quota attribute를 제공하지만, quota attribute는 @propety로 제공됨.
# 필요한 값은 그 때마다 계산해서 제공됨.
class Bucket:
def __init__(self, period):
...
self.max_quota = 0
self.quota_consumed = 0
@property
def quota(self):
return self.max_quota - self.quota_consumed
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0:
self.quota_consumed = 0
self.max_quota = 0
...
예를 들어 위와 같은 형태로 @property 어트리뷰트를 활용할 수 있다.
- self.quota로 접근할 수 있지만, @property로 제공되기 때문에 인스턴스 내에 self.quota라는 값은 저장되지 않음.
- self.quota로 접근하면, Getter 메서드에서는 필요할 때 마다 관련된 필드를 계산해서 응답함.
이런 형태로 사용할 수 있다는 것을 알 수 있다. 그렇다면 어떤 경우에 이런 형식으로 활용할까? 아래에서 코드 예시를 살펴보겠지만 'self.quota'에 요구하는 기능이 추가되지만, self.quota를 사용하는 클라이언트 코드에는 영향을 주지 않으면서 확장하고 싶을 때 사용한다.
코드 : @property 사용 전
self.quota에 대한 부분만 중점적으로 보면 된다.
- self.quota는 현재 사용 가능한 용량을 의미한다.
- quota를 특정 주기마다 다시 리필된다.
- 사용자는 얼마만큼의 quota를 요청하고, self.quota가 요청 quota보다 많은 경우에는 실행하고, 그렇지 않으면 거절한다.
self.quota가 부족해서 요청을 거절하는 경우에 고려해야 할 기능이 추가된다고 하자. quota 부족 응답이 온다면, 다음 경우가 존재할 것이다.
- 주기 내에 내가 많은 요청을 보내서 이미 주어진 quota를 다 소모한 경우.
- 주기가 지났는데, 새로운 quota가 보충되지 않은 경우.
quota 부족 응답이 왔을 때, 어떤 경우인지 구분도 해줬으면 한다고 가정한다. 이럴 때는 일반적으로 사용하고 있던 public attribute인 quota를 @property 기반으로 리팩토링해서 외부 클라이언트가 인지하지 못하게 추가 기능을 구현할 수 있다.
from datetime import datetime, timedelta
# 이렇게 구현된 경우, Bucket으로부터 '불가능함' 응답을 받았을 때, 두 가지 경우가 있는데 무엇인지 알 수 없음.
# 1. 주기 내에 버킷에 할당된 용량을 다 쓴 경우
# 2. 주기가 지났는데, 버킷에 용량이 다시 리필되지 않아서 할 수 없는 경우.
# 이런 부분을 구별하는 부가기능을 추가하고 싶다면, quota를 @property를 이용해 확장할 수 있음.
# 이 때 사용자의 코드는 전혀 변하지 않음.
class Bucket:
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.quota = 0
def __repr__(self):
return f'Bucket(quota={self.quota})'
def fill(bucket, amount):
now = datetime.now()
if (now - bucket.reset_time) > bucket.period_delta:
bucket.quota = 0
bucket.reset_time = now
bucket.quota += amount
def deduct(bucket, amount):
now = datetime.now()
if (now - bucket.reset_time) > bucket.period_delta:
return False # 새 주기가 시작되었는데, 아직 버킷 할당량이 재설정되지 않음.
if bucket.quota - amount < 0:
return False # 버킷의 가용 용량이 충분하지 못하다.
else:
bucket.quota -= amount
return True # 버킷의 가용 용량이 충분하므로 필요한 분량을 사용한다.
bucket = Bucket(60)
fill(bucket, 100)
print(bucket)
if deduct(bucket, 99):
print('99 용량 사용')
else:
print('가용 용량이 작아서 99 용량을 처리할 수 없음.')
print(bucket)
if deduct(bucket, 3):
print('3 용량 사용')
else:
print('가용 용량이 작아서 3 용량을 처리할 수 없음.')
print(bucket)
코드 : @property 사용 후
self.quota 대신에 @property로 quota Attribute를 제공하도록 바궜다. 그리고 요구사항 구현을 위해 내부적으로 사용하는 max_quota / quota_consumed가 추가되었다.
- self.quota를 요청하면 @property를 통해 필요한 값을 그 때 마다 계산해서 반환
- self.quota의 Setter를 요청하면 @quota.setter를 통해 상태에 따라 max_quota / quota_consume에 적절한 값을 추가.
이런 형태로 구현을 하게 되었다. 여기서 가장 중요한 부분은 quota를 저장하고, 접근하는 방법이 완전히 변했지만 클라이언트의 코드는 전혀 변하지 않았다는 점이다. 이런 의미에서 처음에는 public attribute로 시작하고, 데이터 모델에 기능이 추가될 때 @property를 코드를 점차 발전시켜나가는 것이다.
from datetime import datetime, timedelta
# 1. 주기 내에 버킷에 할당된 용량을 다 쓴 경우
# 2. 주기가 지났는데, 버킷에 용량이 다시 리필되지 않아서 할 수 없는 경우.
# 이 두 경우를 구분할 수 있도록 self.quota는 @property로 리팩토링하고, 요청할 때 마다 계산해서 응답한다.
class Bucket:
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
# self.quota = 0
def __repr__(self):
return (f'Bucket(max_quota={self.max_quota}, '
f'quota_consumed={self.quota_consumed})')
@property
def quota(self):
return self.max_quota - self.quota_consumed
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0:
# 새로운 주기 and 가용 용량을 재설정하는 경우
self.quota_consumed = 0
self.max_quota = 0
elif delta < 0:
# 새로운 주기가 되고 가용 용량을 추가하는 경우
assert self.quota_consumed == 0
self.max_quota = amount
else:
# 어떤 주기 안에서 가용 용량을 소비하는 경우
assert self.max_quota >= self.quota_consumed
self.quota_consumed += delta
def fill(bucket, amount):
now = datetime.now()
if (now - bucket.reset_time) > bucket.period_delta:
bucket.quota = 0
bucket.reset_time = now
bucket.quota += amount
def deduct(bucket, amount):
now = datetime.now()
if (now - bucket.reset_time) > bucket.period_delta:
return False # 새 주기가 시작되었는데, 아직 버킷 할당량이 재설정되지 않음.
if bucket.quota - amount < 0:
return False # 버킷의 가용 용량이 충분하지 못하다.
else:
bucket.quota -= amount
return True # 버킷의 가용 용량이 충분하므로 필요한 분량을 사용한다.
bucket = Bucket(60)
fill(bucket, 100)
print(bucket)
if deduct(bucket, 99):
print('99 용량 사용')
else:
print('가용 용량이 작아서 99 용량을 처리할 수 없음.')
print(bucket)
if deduct(bucket, 3):
print('3 용량 사용')
else:
print('가용 용량이 작아서 3 용량을 처리할 수 없음.')
print(bucket)
@property의 남용은 금물
@property를 사용하면 클라이언트의 코드 수정없이 내부 기능을 손쉽게 확장해 나갈 수 있어 좋은 선택지가 될 수 있다. 그러나 내부적으로 @proeprty가 너무 많이 사용되기 시작한다면, @property로 이루어진 부분을 클래스로 분리할 것을 고려해봐야 한다.
@property는 그 자체로 추상화 되는 부분이 있는데, 추상화는 디버깅 관점에서 비싼 비용을 지불해야한다. 한 클래스에서 수십 개의 @property가 있고, 각 @property를 넘나드는 상황이라면 디버깅이 아주 어려울 것이다. 이런 상황이 되기 전에 @property 들을 클래스로 분화시키는 것이 좋은 선택지가 될 수도 있다.
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
Effective Python Item 10. 왈러스 연산자를 이용해 반복을 피하라 (0) | 2024.01.14 |
---|---|
Effective Python Item 28. 컴프리헨션 내부에는 수식을 2개까지만 써라 (0) | 2024.01.13 |
Effective Python Item 44. Setter, Getter 대신 Attribute를 사용하라. (0) | 2024.01.13 |
Effective Python Item 36. Iterator / Generator를 다룰 때는 itertools를 사용하라 (0) | 2024.01.13 |
이펙티브 파이썬 Item 27. map과 filter 대신 컴프리헨션을 사용하라 (0) | 2023.10.21 |