3 minute read

본 포스팅은 “윤성우의 열혈 파이썬 중급편” 책 내용을 기반으로 작성되었습니다. 잘못된 내용이 있을 경우 지적해 주시면 감사드리겠습니다.

21-1. 속성 감추기

class Person:
  def __init__(self, n, a):
    self.__name = n
    self.__age = a
  def add_age(self, a):
    if(a < 0):
      print('나이 정보 오류')
    else:
      self.__age += a
  
  def __str__(self):
    return '{0}: {1}'.format(self.__name, self.__age)

def main():
  p = Person('James', 22)
  p.add_age(1)
  # p.__age -= 1 # 이 문장 실행하면 오류 발생된다!
  print(p)

main()
(결과) James: 23

위 예제의 __(변수명)에 주목하자! 평소 보는 변수명과는 많이 다르다.

  • 객체 내 변수(속성) 이름 앞에 언더바(__)를 붙이면 이 변수에 직접 접근할 수 없다!

왜 이런게 필요할까?
만약 실수로 p.__age에 접근해서 += 연산을 하려 했는데 -=연산을 했다고 하자. 이런 사소한 문법적 오류를 발견하기는 쉽지 않다. 그래서 객체 외부에서 객체 내 변수에 직접 접근하지 못하도록 막을 수 있게 만들었다. 그리고 메소드를 이용하여 += 연산에 해당하는 기능을 만들고 여기에 접근하도록 구현한 것이다. 그러면 덜 헷갈릴 것이니까!

참고로 파이썬에는 _(변수명), 즉 언더바를 하나만 사용하면 실제 객체 내 해당 변수에 직접 접근이 가능하지만, 해당 변수에 직접 접근하지 말아야 한다! 라는 암묵적인 규칙이 있다고 한다.

21-2. dict

class Person:
  def __init__(self, n, a):
    self._name = n
    self._age = a
  
def main():
  p = Person('James', 22)
  print(p.__dict__)
  p.len = 178
  p.adr = 'Korea'
  print(p.__dict__)

main()
(결과) {'_name': 'James', '_age': 22}
       {'_name': 'James', '_age': 22, 'len': 178, 'adr': 'Korea'}

보다시피, 객채는 __dict__라는 스페셜 메소드를 갖고 있으며, 여기에는 해당 객체의 변수 정보가 담긴다.

class Simple:
  def __init__(self, n, s):
    self._n = n
    self._s = s
  def __str__(self):
    return '{0}: {1}'.format(self._n, self._s)

def main():
  sp = Simple(10, 'my')
  print(sp)
  sp.__dict__['_n'] += 10
  sp.__dict__['_s'] = 'your'
  print(sp)

main()
(결과) 10: my
       20: your

21-1에서 _(변수명)은 직접 접근이 안된다고 배웠다. 하지만 __dict__에 접근하면, 객체 내 _(변수명) 형태의 변수 값을 수정할 수 있게 된다!
즉, 객체 내 변수의 값은 __dict__를 통해서 관리가 되고 있는 것이다!

class Person:
  def __init__(self, n, a):
    self.__name = n
    self.__age = a

def main():
  p = Person('James', 22)
  print(p.__dict__)

main()
(결과) {'_Person__name': 'James', '_Person__age': 22}

객체 내 변수 이름을 __(변수명), 즉 언더바를 2개 붙여서 변수를 생성하면 __dict__에는 다음과 같은 패턴으로 키가 생성된다.

  • __AttrName_ClassName__AttrName

이제 왜 언더바 2개를 붙인 객체 내 변수에는 접근이 안되는지 이해가 됐능가?!?

21-3. dict 단점과 그 해결책

딕셔너리는 키와 값을 갖다보니 리스트나 튜플에 비해 메모리 사용량이 많다. 이에 따라, 많은 수의 객체를 생성하는 경우 객체 하나 당 존재하는 __dict__의 존재는 부담이 된다.

이 때 __slots__ 이란 녀석을 이용해보자.

class Point3D:
  __slots__ = ('x', 'y', 'z')

  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z
  def __str__(self):
    return '({0}, {1}, {2})'.format(self.x, self.y, self.z)

def main():
  p1 = Point3D(1, 2, 3)
  p2 = Point3D(7, 7, 7)
  print(p1)
  print(p2)
  # p2.w = 7   # 이거 넣으면 AttributeError: 'Point3D' object has no attribute 'w' 에러 발생!
  # print(p1.__dict__)  # 이거 넣으면 AttributeError: 'Point3D' object has no attribute '__dict__' 에러 발생!
  print(p1.__slots__)

main()
(결과) (1, 2, 3)
       (7, 7, 7)
       ('x', 'y', 'z')

이처럼, 객체 내 변수를 x, y, z만 제한하는 효과를 줄 수 있으며, 객체별로 __dict__가 생성되지 않게되어 메모리를 효과적으로 관리할 수 있다. 또한, 클래스당 __slots__ 하나만 생성된다는 것도 메모리 측면에서 개이득이다.

21-4. dict 있을 때와 slots 있을 때 속도차이

import timeit

class Point3D:
  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z
  def __str__(self):
    return '({0}, {1}, {2})'.format(self.x, self.y, self.z)

def main():
  start = timeit.default_timer()
  p = Point3D(1, 1, 1)

  for i in range(3000):
    for i in range(3000):
      p.x += 1  # == p.__dict__['x'] = p.__dict__['x'] + 1 방식으로 접근함!
      p.y += 1
      p.z += 1
    
  print(p)

  stop = timeit.default_timer()
  print(stop - start)

main()
(결과) (9000001, 9000001, 9000001)
       2.5983912000010605
import timeit

__slots__ = ('x', 'y', 'z')

class Point3D:
  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z
  def __str__(self):
    return '({0}, {1}, {2})'.format(self.x, self.y, self.z)

def main():
  start = timeit.default_timer()
  p = Point3D(1, 1, 1)

  for i in range(3000):
    for i in range(3000):
      p.x += 1  # == p.__dict__['x'] = p.__dict__['x'] + 1 방식으로 접근함!
      p.y += 1
      p.z += 1
    
  print(p) # == print(p.__dict__['x'], p.__dict__['y'], p.__dict__['z']) 와 같음
  stop = timeit.default_timer()
  print(stop - start)

main()
(결과) (9000001, 9000001, 9000001)
       2.40482350000093 

보다시피, __slots__이 있을 때 조금 더 빨리 실행된다!

Leave a comment