[Python] 21. 정보은닉과 dict, __slots__의 효과
본 포스팅은 “윤성우의 열혈 파이썬 중급편” 책 내용을 기반으로 작성되었습니다. 잘못된 내용이 있을 경우 지적해 주시면 감사드리겠습니다.
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