Python __slots__ 활용해보기

미리 정하고 강제하기

Featured image

Python __slots__

Python 에는 __slots__ 라는 재미있는 녀석이 있습니다.

양 옆 더블언더바를 보고 어느정도 예상하셨겠지만, 이 친구는 class 와 관련된 친구입니다.

일단 바로 샘플 코드부터 볼게요!

# python2 버전과의 호환을 위해 object를 기입합니다!

class Bird(object):
	__slots__ = ["name", "length"]

	def __init__(self):
		self.name: str = "바보새"
		self.length: int = 100

항상 익숙하게 보던 __init__ 위에 처음 보는 친구가 있을겁니다.

저 친구가 가진 기능은 뭘까요?

__slots__ 는 클래스 속성(attribute)을 사전에 제한합니다.

Python은 기본적으로 클래스의 속성값이 dict 형태로 이루어져 있습니다. 이는, 인스턴스가 어느 곳에서든 새로운 속성값을 취하고, 버릴 수 있게 해주는 장점이 있습니다.

아래와 같은 코드가 실행될 수 있는 이유죠.


class Bird(object):
	def __init__(self):
		self.name: str = "짱바보새"
		self.length: int = 100

fat_bird: Bird = Bird()
fat_bird.weight: int = 700

print(fat_bird.weight)

# 700

하지만, 이와 같은 특성은 이미 생성된 인스턴스의 속성값을 변경한다는 점에서, 어찌 보면 필요 이상의 램을 잡아먹게 됩니다.

그럼 사전에 속성값을 제한하는 __slots__를 사용하게 되면 어떨까요?

https://wiki.python.org/moin/UsingSlots

https://book.pythontips.com/en/latest/__slots__magic.html

이는 위 두 링크에 나와있듯이, 클래스 속성이 더 이상 dict 형태를 취하지 않게 됨으로써 허용된 속성 이외의 값을 받아들이지 않게 되고, 이렇게 고정된 속성값에만 용량을 할당하게 함으로써 메모리 사용량을 줄일 수 있게 됩니다.

그래서 아래와 같은 코드가 더 이상 작동하지 않습니다.


class Bird(object):
	__slots__ = ["name", "length"]

	def __init__(self):
		self.name: str = "짱바보새"
		self.length: int = 100

fat_bird: Bird = Bird()
fat_bird.weight: int = 700

# Attribute Error

상당히 괜찮아보이죠?

하지만, 한가지 의문이 들게 됩니다.

좋아보이는 친구인데, 다른 python 코드를 볼 때 저 친구를 본 적이 거의 없다싶이 합니다. 메모리 사용량을 줄여주는데, 왜 많은 사람들이 사용하지 않고 있을까요?

일부 Python 라이브러리들, 메소드들은 __dict__를 필요로한다.

일단, 가장 큰 이슈는 호환성 이슈입니다.

간단한 하나의 예시를 들자면, django에 있는 cached_property 의 경우, class 의 __dict__ 를 필요로 하기 때문에 __slots__ 와 충돌합니다.


class Man(object):  
    __slots__ = ["name"]  
  
    def __init__(self):  
        self.name: str = "Kim"  
  
  @cached_property  
  def call_name(self) -> str:  
        return self.name  
  
  
some_man: Man = Man()  
some_man.call_name()

# Attribute Error

이 경우에는 __slots__를 사용할 수 없을겁니다.

확장성이라는 장점을 버릴 만큼 유의미한가?

두 번째 이슈는, 속성이 __dict__ 를 버림으로써 잃게 되는 확장성이라는 요소가 과연 __slots__ 를 사용하는 것에 비해 유의미한가? 입니다.

이 부분은 trade_off 적인 부분이라서, 사실 사용하기 나름이고, 판단하기 나름인 것 같습니다.


그런데 저라면, 이 친구를 다른 의미로 차용할 것 같습니다.

강제함으로써 명확해지기

이전 글에서, 제가 생각하는 강제의 미학에 대해서 이야기했었는데요, 저는 그 강제함을 위해 이 녀석을 차용하는건 어떨까 싶습니다.

Python overrides 활용해보기

설계자가 만든 의도대로 클래스를 활용하기 위해서는, 주어진 클래스의 속성값만 이용할 필요가 있다고 생각합니다.

제가 만든 강아지라는 클래스에는 분명 무는 힘이라는 속성이 없었는데, 제 클래스를 쓰는 다른 동료가 코드 상에서 특정 인스턴스에 임의적으로 무는 힘을 추가해서 예상하지 못한 메소드 또는 불필요한 과한 인스턴스 속성 만들어낼 수도 있으니까요.

그리고 그 특별한 속성을 지닌 인스턴스를 누군가가 다른 곳에 계속 이용하기 시작한다면, 작은 균열이 시작되는 것이라고 생각합니다.

하지만 그렇다고 속성값을 제한하는 것이 마냥 옳다고는 볼 수 없습니다.

Python을 사용하며 항상 고민할 수 밖에 없는, Pythonic 한 코드와 부합한지는 더 깊은 생각을 거쳐야만 합니다.

각 언어에는 제각기 특장점이 있으니, 언어에 구애받지 않는 개발자가 되더라도, 각 언어의 특징을 잘 활용해야만 한다고 생각합니다.

하지만 저는 제 생각이 과연 옳은 것인지, Pythonic한 것인지 섣불리 판단할 수 없는 작은 주니어입니다 :>

얼른 더 넓은 시야의 사람이 되고 싶습니다.

오늘도 저의 작은 의견이 누군가에게 도움이 되었기를 바라며!

샘플 코드와 인사드립니다.


from abc import ABCMeta, abstractmethod
from overrides import overrides

class Bird(metaclass=ABCMeta):
	@abstractmethod
	def flying(self) -> bool:
		pass


class Parrot(Bird):
	__slots__ = ["name", "weight"]

	def __init__(self, weight: int):
		self.name: str = "포도"
		self.weight: int = weight

	@overrides
	def flying(self) -> bool:
		if self.weight > 10:
			return True
		else:
			return False


class Penguin(Bird):
	__slots__ = ["name"]

	def __init__(self, name: str):
		self.name: str = name

	@overrides
	def flying(self) -> bool:
		return False


class BirdChecker(object):
	__slots__ = ["species"]

	def __init__(self, species: Bird):
		self.species: Bird = species

	def check_can_fly(self) -> str:
		if self.species.flying() is True:
			return f"{self.species.name} 는 날아!"
		else:
			return f"{self.species.name} 는 못날아!"

myimage