on
[Python - OOP] 클래스와 캡슐화
[Python - OOP] 클래스와 캡슐화
들어가며
Python은 기본적으로 객체 지향 언어이다. 따라서 클래스를 작성할 때 객체 지향의 특징 중 하나인 캡슐화를 하는 것이 보통이다. 이 포스팅에서는 프라이빗 변수와 프로퍼티(property)를 포함해, 파이썬 프로그래밍의 몇 가지 관례를 소개한다.
Public VS Private
클래스의 주역할은 객체의 데이터 와 내부 구현 세부사항을 캡슐화하는 것이다. 그러나 외부 세계가 객체를 조작하는 데 사용할 public 인터페이스도 클래스에 정의해야 한다.
문제점
파이썬은 자바와 다르게 클래스와 객체의 거의 모든 것이 오픈되어 있다. 따라서 다음과 같은 문제점들이 발생한다.
객체의 내부를 쉽게 조사할 수 있고 원하는 대로 바꿀 수 있다. 액세스 제어에 대한 강력한 개념이 없다 (ex: 프라이빗 클래스 멤버)
이는 내부 구현을 격리하고자 할 때 이슈가 된다.
파이썬의 캡슐화(Encapsulation)
파이썬은 기본적으로 의도된 사용법을 지시하는 프로그래밍 관례를 따른다. 이러한 관례는 명명(naming)에 기반을 둔다. 언어에서 규칙을 강요하기보다는 프로그래머가 자발적으로 준수하는 것이 일반적이다.
프라이빗 어트리뷰트
이름이 _ 로 시작하는 어트리뷰트는 프라이빗 (Private)로 간주한다.
class Person(Object): def __init__(self, name): self.name = 0
위에서 언급한 것과 같이, 이것은 프로그래밍 스타일일 뿐이다. 따라서 여전히 액세스하고 변경할 수 있다.
p = Person('Blue') p._name p._name = 'Dave' >>>
_로 시작하는 이름은, 그것이 변수든 함수든 모듈명이든, 모두 내부 구현으로 간주하는 것이 일반적인 규칙이다. 그런 이름을 직접 사용하고 있다면 뭔가 잘못하고 있는 것이다.
용어 정리 어트리뷰트란
클래스 내부에 포함되어 있는 변수나, 메서드를 의미한다
단순 어트리뷰트
다음 클래스를 생각해보자
class Stock: def __init__ (self.name, shares, price): self.name = name self.shares = shares self.price = price
놀랍게도, 어떤 값에든지 어트리뷰트를 설정할 수 있다.
s = Stock('IBM', 50, 91,1) s.shares = 100 s.shares = "hundred" s.shares = [1,0,0]
이것을 보고 뭔가 추가적인 검사가 필요하다고 생각이 들었을지 모른다
s.shares = '50' # 문자열의 경우 TypeError를 일으키고 싶다
이것을 어떻게 할 수 있을까?
관리(Managed) 어트리뷰트
위 문제를 해결하기 위해선 접근자(accessor) 메서드를 도입하는 방법이 있긴 하다.
class Stock: def __init__(self, name, shares, price): self.name = name self.set_shares(shares) self.price = price def get_shares(self): return self._shares def set_shares(self, value): if not isinstance(value, int): raise TypeError('Expected an int') self._shares = value
이것 때문에 기존의 코드가 깨지는 것이 너무 나쁘다
s.shares = 50 이 s.set_shares(50)이 된다.
프로퍼티
더 좋은 방법이 있다.
class Stock: def __init__ (self, name, shares, price): self.name = name self.shares = shares self.price = price @property def shares(self): return self._shares @shares.setter def shares(self, value): if not ininstance(value, int): raise TypeError ('Expected int') self._shares = value
이제 평범한 프로퍼티 액세스는 @property와 @shares. setter 하에서 getter와 setter 메서드를 트리거한다.
s = Stock('IBM', 50, 91.1) s.shares # @property를 트리거 >>> 50 s.shares = 75 # @shares.setter를 트리거 >>>
이 패턴을 적용하면 소스 코드를 바꿀 필요가 없다. 새로운 setter도 클래스 내에 할당되면 내부 init() 메서드를 포함해 호출된다.
class Stock: def __init__(self, name, shares, price): ... # 이 할당은 아래 setter를 호출한다 self.shares = shares ... ... @shares.setter def shares(self, value): if not isinstance(value, int): raise TypeError('Expected int') self._shares = value
프로퍼티와 프라이빗 이름이 혼동되곤 한다. 프러퍼티는 프라이븟 이름을 _shares 같이 내부적으로 사용하지만, 클래스의 나머지(프로퍼티가 아닌 것)는 shares와 같은 이름을 계속 사용할 수 있다.
통일된 액세스
@property를 활용하면 좀 더 통일된 인터페이스를 넣는 방법을 보여준다. 예를 들어보자
# @property def cost(self): return self.shares * self.price s = Stock('GOOG', 100, 490.1) print(s.shares) print(s.cost())
cost()라는 메서드를 만들고 @property를 붙이지 않는다면 s.cost()라고 메서드를 호출해야 한다. 하지만 shares를 호출할 때는 ()를 넣지 않기 때문에 통일된 방법이 아니다. 따라서 @property를 붙여서 일관된 인터페이스를 제공하는 것이 바람직하다.
수정
@property def cost(self): return self.shares * self.price s = Stock('GOOG', 100, 490.1) print(s.shares) print(s.cost())
참고
from http://bloowhale.tistory.com/114 by ccl(A) rewrite - 2021-10-23 18:27:57