Intro
파이썬의 클래스에 구현되어 있는 몇 가지 유용한 기능을 알아보자.
__dict__
다음과 같은 클래스를 정의한다.
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def bark(self):
print("bow-wow")
Python
복사
Dog 클래스로 생성한 인스턴스의 모든 attribute는 인스턴스 내부의 __dict__에 저장된다.
>>> dog = Dog(name="milo", age=3)
>>> dog
<__main__.Dog object at 0x10196a560>
>>> dog.__dict__
{'name': 'milo', 'age': 3}
>>> type(dog.__dict__)
<class 'dict'>
Python
복사
vars() 함수를 이용하면 객체 안의 __dict__ 를 출력할 수 있다.
>>> vars(dog)
{'name': 'milo', 'age': 3}
Python
복사
다음과 같이 객체에 새로운 attribute를 추가하면 __dict__에 추가된다.
>>> dog.color = "black"
>>> vars(dog)
{'name': 'milo', 'age': 3, 'color': 'black'}
Python
복사
__slots__
객체마다 내부에 mutable한 dict를 갖게되면 많은 객체를 생성하는 경우, 불필요한 메모리가 낭비된다.
객체에 __dict__가 필요하지 않다면 __slots__를 이용하여 메모리를 절약할 수 있다.
다음과 같이 __slots__를 지정해주면 객체에 __dict__와 __weakref__가 추가되지 않는다.
class Dog:
__slots__ = ("name", "age")
def __init__(self, name: str, age: int):
self.name = name
self.age = age
Python
복사
대신 이전처럼 동적으로 새로운 attribute를 추가할 수 없다.
>>> dog = Dog(name="milo", age=3)
>>> dog.__slots__
('name', 'age')
>>> dog.color = "black"
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'Dog' object has no attribute 'color'
>>> vars(dog)
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute
Python
복사
파이썬 3.10부터는 dataclass에 slots 옵션이 추가되어 보다 메모리 효율적으로 사용할 수 있다.
from dataclasses import dataclass
@dataclass(slots=True)
class Dog:
name: str
age: int
Python
복사
>>> dog = Dog(name="milo", age=3)
>>> dog.__slots__
('name', 'age')
>>> vars(dog)
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute
Python
복사
functools.cached_property
파이썬 3.8부터는 __dict__를 활용한 cached_property라는 기능이 있다.
특정 property를 캐싱할 수 있는 기능이다.
주로 연산이 오래 걸리는 property를 다시 계산하지 않고 재사용하기 위해 사용한다.
import time
from functools import cached_property
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@cached_property
def in_human_age(self) -> int:
print("calculating...")
time.sleep(0.5)
if self.age == 1:
return 15
elif self.age == 2:
return 24
else:
return 24 + (self.age - 2) * 4
Python
복사
다음과 같이 cached_property가 추가된 프로퍼티를 반복적으로 호출해도 첫 호출에서만 연산이 수행된다.
>>> dog = Dog(name="milo", age=3)
>>> dog.in_human_age
calculating...
28
>>> dog.in_human_age
28
>>> dog.in_human_age
28
Python
복사
이런 기능이 가능한 이유는 첫 호출 이후 객체의 __dict__에 결과값이 저장되기 때문이다.
>>> vars(dog)
{'name': 'milo', 'age': 3, 'in_human_age': 28}
Python
복사
따라서 다음과 같이 __dict__의 값을 지우게 되면 캐싱된 값이 무효화되고 다시 연산을 수행한다.
# 1
>>> dog.__dict__.pop("in_human_age")
28
>>> dog.in_human_age
calculating...
28
# 2
>>> del dog.in_human_age
>>> dog.in_human_age
calculating...
28
Python
복사
functools.lru_cache
functools.lru_cache를 이용하면 인자 별로 함수를 캐싱할 수 있다.
import time
from functools import lru_cache
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@lru_cache(maxsize=3)
def learn_trick(self, trick: str) -> str:
print(f"learning {trick}...")
time.sleep(0.5)
return f"I can {trick}."
Python
복사
lru_cache에는 maxsize 값을 통해 최대 캐싱 가능한 size를 지정할 수 있다.
milo에게 다음과 같이 세 가지 기술을 배우게 해보자.
>>> dog = Dog(name="milo", age=3)
>>> dog.learn_trick("jump")
learning jump...
'I can jump.'
>>> dog.learn_trick("bark")
learning bark...
'I can bark.'
>>> dog.learn_trick("sit")
learning sit...
'I can sit.'
Python
복사
세 개의 기술 이름(argument)은 이미 캐싱되었기 때문에 다시 호출해도 캐싱된 값을 반환한다.
>>> dog.learn_trick("jump")
'I can jump.'
>>> dog.learn_trick("bark")
'I can bark.'
>>> dog.learn_trick("sit")
'I can sit.'
Python
복사
하지만 새로운 기술(sing)을 익히게 하면 가장 마지막으로 사용된 jump는 캐시에서 사라진다.
따라서 다시 jump()를 호출하면 이전처럼 캐싱된 값을 사용하지 않고 새롭게 연산을 수행한다.
>>> dog.learn_trick("sing")
learning sing...
'I can sing.'
>>> dog.learn_trick("jump")
learning jump...
'I can jump.'
Python
복사
다음과 같이 캐시에 대한 정보를 출력할 수 있다.
hits 3회는 캐시가 재사용된 횟수이고, misses 5회는 캐시가 사용되지 않고 새롭게 연산이 수행된 횟수다.
>>> dog.learn_trick.cache_info()
CacheInfo(hits=3, misses=5, maxsize=3, currsize=3)
Python
복사
캐시는 다음과 같이 일괄적으로 삭제할 수 있다.
>>> dog.learn_trick.cache_clear()
Python
복사
functools.cache
파이썬 3.9부터는 memoization를 쉽게 구현할 수 있게 함수를 캐싱할 수 있는 functools.cache를 지원한다.
cache는 maxsize=None인 lru_cache와 같지만 LRU 알고리즘의 캐시 퇴출(eviction)을 수행하지 않는다.
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@cache
def learn_trick(self, trick: str) -> str:
print(f"learning {trick}...")
time.sleep(0.5)
return f"I can {trick}."
Python
복사
주의사항
하지만 functools.lru_cache와 functools.cache를 사용할 때 주의할 점이 있다.
위의 예시처럼 인스턴스 메소드에 데코레이터를 통해 cache를 지정하는 경우 아래와 같이 동작한다.
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def learn_trick(self, trick: str) -> str:
print(f"learning {trick}...")
time.sleep(0.5)
return f"I can {trick}."
learn_trick = cache(learn_trick)
Python
복사
캐시 함수가 클래스 변수로 추가된다.
이를 확인하기 위해 다음과 같이 __del__ 메소드를 오버라이드 해보자.
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __del__(self):
print("deleting dog")
def learn_trick(self, trick: str) -> str:
print(f"learning {trick}...")
time.sleep(0.5)
return f"I can {trick}."
learn_trick = cache(learn_trick)
Python
복사
dog 인스턴스를 생성한 뒤 메소드를 호출하여 캐싱한다.
>>> dog = Dog(name="milo", age=3)
>>> dog.learn_trick("jump")
learning jump...
'I can jump.'
Python
복사
dog의 reference count를 없애고 가비지 콜렉팅을 수행해도 인스턴스의 __del__ 메소드가 호출되지 않는다.
다시 말해 dog 인스턴스가 삭제되지 않는다.
>>> dog.learn_trick.cache_info()
CacheInfo(hits=0, misses=1, maxsize=None, currsize=1)
>>> dog = None
>>> gc.collect()
Python
복사
클래스 변수의 캐시 정보를 확인해보면 여전히 캐시가 남아있다.
>>> Dog.learn_trick.cache_info()
CacheInfo(hits=0, misses=1, maxsize=None, currsize=1)
Python
복사
dog 인스턴스를 삭제하기 위해선 다음과 같이 클래스 변수의 캐시를 지워야한다.
>>> Dog.learn_trick.cache_clear()
deleting dog
Python
복사
따라서 인스턴스 메소드에 데코레이터를 붙이면 클래스 레벨에 캐시가 생성된다.
이로 인해 인스턴스가 정상적으로 삭제되지 않아 심각한 메모리 누수(memory leak)를 유발할 수 있다.
해결책
이를 방지하기 위해선 다음과 같은 방법으로 메소드에 cache를 적용해야 한다.
class Dog:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
self.learn_trick = cache(self._learn_trick)
def __del__(self):
print("deleting dog")
def _learn_trick(self, trick: str) -> str:
print(f"learning {trick}...")
time.sleep(0.5)
return f"I can {trick}."
Python
복사
이제 인스턴스 메소드를 호출해서 캐싱해도 가비지 컬렉팅에 의해 정상적으로 인스턴스가 삭제된다.
>>> dog = Dog(name="milo", age=3)
>>> dog.learn_trick("jump")
learning jump...
'I can jump.'
>>> dog = None
>>> gc.collect()
deleting dog
Python
복사