Search

Python 클래스 다루기

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
복사

참고