@property
는 파이썬에서 객체지향 프로그래밍을 지원하는 데코레이터 이다. @property
가 뭔지 알아보기 전에 파이썬의 class와 getter, setter 대해 알아보자.
많은 class기반 객체지향 언어에서 private를 지원한다.
private로 선언된 변수는 class내부 메소드로만 제어할 수 있고 외부에서 접근할 수 없다.
파이썬은 attribute앞에 언더바 "_"를 두개 붙여서 private한 attribute를 제공한다.
# test.py
class Ex:
def __init__(self):
self.pub = "I'm public"
self.__pri = "I'm private"
>>> from test import Ex
>>> ex = Ex()
>>> print(ex.pub)
Im public
>>> print(ex.__pri)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Ex' object has no attribute '__pri'
평소처럼 선언한 ex.pub
어트리부트는 print
함수를 통해서 불러올수 있지만 언더바를 2개 붙인 ex.__pri
어트리부트는 없는 변수라고 오류를 내뿜는다.
ex
인스턴트안에 정말로__pri
어트리부트가 없는지 확인하기 위해서 Ex
클리스를 수정해보자.
class Ex:
def __init__(self):
self.pub = "I'm public"
self.__pri = "I'm private"
def print_pub(self):
print(self.pub)
def print_pri(self):
print(self.__pri)
>>> from test import Ex
>>> ex = Ex()
>>>
>>> ex.print_pub()
I'm public
>>> ex.print_pri()
I'm private
>>>
위의 실행결과를 보면 Ex
클래스의 내부 메소드인 print_pri()
를 사용하면 __pri
를 사용할 수 있다.
언뜻보면 private 같은게 왜 필요한가 싶지만 경우에 따라서 유용할 수 있다. 예를들어 파이썬으로 냉장고를 만든다고 생각해보자.
#appliances.py
class Fridge:
def __init__(self):
self.fridge_temperature = 4
...
def doing_somthing(self):
#doing something
pass
...
냉장고에 대한 클래스로 온도 속성을 가지고 있다.
당신 혼자서 이런 프로그램을 만들고 사용한다면 크게 상관 없을 수 있다. 하지만 오래동안 유지보수를 해야하거나 협업을 하는 상황이라면 문제가 생길수 있다.
예를들어 다음과 같은 실수를 할 수 있다.
from appliances import Fridge
fridge = Fridge()
fridge.fridge_temperature = 39 # Celsius 4
누군가 화씨와 섭씨를 헷갈려서 4(°C)를 넣을 자리에 39(°F)를 넣은 것이다.
만약 아무런 보호장치가 없다면 냉장고는 사우나가 되고 모든 식재료가 상해버릴 것이다.
이런 실수를 방지하는 가장 멋있는 방법중 하나는 private하게 fridge_temperature
를 선언하고 setter와 getter를 통해서 변수를 제어하는 것이다.
#appliances.py
class Fridge:
def __init__(self, temp=4):
self.set_celsius_temp(temp)
def set_celsius_temp(self, temp):
if temp < 0:
self.__temperature = 0
elif temp > 10:
self.__temperature = 10
else:
self.__temperature = temp
def set_fahrenheit_temp(self, temp):
temp = (temp - 32) * 5/9
self.set_celsius_temp(temp)
def get_temp(self):
return self.__temperature
f = Fridge()
f.set_fahrenheit_temp(41)
print(f.get_temp()) # 5 <-> (41-32)*5/9
f.set_celsius_temp(4)
print(f.get_temp()) # 9
위의 방식은 java같은 언어에서 많이 쓰인다. 이런식으로 오브젝트의 데이터를 감추고 메소드를 통해서 접근하는 방법을 추상화, 캡슐화 등으로 부른다. java같은 언어는 모든 attribute를 private하게 선언하고 접근할 때 method를 사용하는 것을 권장한다. public하게 선언할 수 있는 attribute도 private하게 선언하는 것이다.
파이썬에서도 위의 방식으로 데이터를 보호할 수 있다. 하지만 굳이 자바를 들먹인 이유는 파이썬은 좀 다른 방식을 제공하고 권장하기 때문이다. 일단 아래의 실행결과를 보자.
>>> from fridge import Fridge
>>> f = Fridge()
>>>
>>> f.celsius = -4
>>> print(f.celsius)
0
>>> f.celsius = 100
>>> print(f.celsius)
10
>>> f.celsius = 4
>>> print(f.celsius)
4
위의 파이썬 쉘에서 사용한 Fridge
클래스는 @property
를 사용해서 만든 냉장고 클래스다.
여기서 주목할 점은 f.celsius
에 할당한 값이 자기 멋대로 변했다는 점이다. -4를 대입하면 0으로 변하고 100을 대입하면 10으로 변한다. 4를 대입할 때만 말을 듣는 것 같다.
사실 Fridge
클래스는 celsius
를 0°C에서 10°C사이에서 보호하고 있다. set_celsius_temp
같은 메소드와는 다른 방식으로 말이다.
이런 일이 가능하게 하는 것이 바로 @property
이다.
@property
는 python에서 기본으로 제공하는 데코레이터로 별도로 import하지 않고 바로 사용할 수 있다. 이를 사용하면 =
연산자를 통해서 private한 attribute를 다룰 수 있다.
위 쉘에서 사용한 Fridge클래스는 다음과 같다.
class Fridge:
def __init__(self, temp=4):
self.celsius = temp
@property
def celsius(self):
return self.__celsius
@celsius.setter
def celsius(self, temp):
if temp < 0:
self.__celsius = 0
elif temp > 10:
self.__celsius = 10
else:
self.__celsius = temp
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, temp):
self.celsius = (temp - 32) * 5/9
이제 어떻게 @property
를 사용하는지 차근 차근 살펴보자
class Fridge:
def __init__(self):
self.__celsius = 'I will be 4'
@property
def celsius(self):
return self.__celsius
>>>f = Fridge()
>>>print(f.celsius)
I will be 4
>>>print(type(f.celsius))
<class 'str'>
우리는celsius
메소드를 @property
로 감싸줬고 마법이 일어났다. f의 메소드 celsius가 str객체로 변한 것이다. 잘 살펴보면 값이 'I will be 4'이고 이건 celsius메소드가 반환하기로 된 self.__celsius
의 값과 같다는걸 알 수 있다.
이제 @property
의 역활이 무엇인지 알 수 있다. @property
로 감싸준 메소드는 외부에서 attribute로 취급되고 메소드의 반환값을 가진다. 좀 다르게 말하면 getter 역할을 한다. 아직 감이 안잡히더라도 계속 가보도록하자. 다음 코드가 도음이 될 수 있을 것이다.
class Fridge:
def __init__(self):
self.celsius = 4
@property
def celsius(self):
return self.__celsius
@celsius.setter
def celsius(self, temp):
if temp < 0:
self.__celsius = 0
elif temp > 10:
self.__celsius = 10
else:
self.__celsius = temp
f = Fridge()
print(f.celsius)
f.celsius = 11
print(f.celsius)
4
10
눈에 띄는 변화부터 보자면 또다른 celsius메소드가 추가됐다. @celsius.setter
라는 메소드를 달고 있는데 여기서 celsius
는 바로 위에서 정의한 메소드 celsius
이다. 그러니까 뒤에 나오는 celsius
가 바로 위 celsius
의 setter역할을 하도록 만들는 데코레이터이다. 여기까지는 완벽하다.
그런데 놓치기 쉬운 변화가 하나 더 있다. init를 보면 전에 private로 선언했던 attribute가 public모양으로 바뀌었음을 알 수 있다.
그런데 머리가 복잡하지 않은가? celsius메소드 안에서는 private한 __celsius
를 썼고 celsius는 private하게 보호되고 있는데 init를 보면 public으로 선언됐다. 그 와중에 메소드 이름은 왜 celsius
이고 또 어디로 가버린걸까
나는@property
를 처음 접했을 떄 많이 햇갈렸다. 조금 공부하고 내린 결론은 @property
는 마술사같은 존재라는 것이다. 작은 눈속임으로 진짜 변수를 숨기고 진짜같은 가짜를 보여준다. @property
는 진짜 attribute는 감추고 메소드를 attribute로 위장해 내놓는다. 언뜻보면 똑같은 celsius
지만 각자 다른 역할을 하고 있다. 다음 코드를 보면서 설명하자
class Fridge:
def __init__(self):
self.setter = 4
@property
def getter(self):
return self.__real_attr
@getter.setter
def setter(self, temp):
if temp < 0:
self.__real_attr = 0
elif temp > 10:
self.__real_attr = 10
else:
self.__real_attr = temp
f = Fridge()
print(f.getter)
f.setter = 11
print(f.getter)
4
10
위 코드는 서로 같은 celsius
들끼리만 묶어서 이름을 바꿔어 놓았다. 변수와 함수 이름만 바꿨기 떄문에 똑같이 동작한다. 첫번째로 볼 것은 __real_attr
다. __celsius
가 변한 것으로 추상화 과정에서 숨어버린 진짜 attribute다.
다음은 getter
이다. getter
는 @property
데코레이터를 달고있는 메소드다. 이 메소드는 attribute처럼 다뤄지고 내부 코드에 따라 다른 동작을 수행할 수 있지만 일반적으로 감춰놓은 attribute를 반환하는 역할을 수행한다.
마지막은 setter
다. 달고있는 데코레이터를 보면 알 수 있듯이getter
가 반환하는 attribute의 setter이다. 그리고 __init__
메소드안에 있던 celsius
의 정체가 바로 setter였다. setter가 정의되지 않았을 때는 __celsius
를 통해 직접 값을 할당했지만 setter가 정의되었기 때문에 더 안전하게 setter를 사용하는 것이었다.
그리고 아래 코드와 비교해 보면 사실 처음 소개했던 java스타일 코드와 거의 같은 모양이라는 걸 알 수 있다.
class Fridge:
def __init__(self, temp=4):
self.set_celsius(temp)
def get_celsius(self):
return self.__temperature
def set_celsius(self, temp):
if temp < 0:
self.__temperature = 0
elif temp > 10:
self.__temperature = 10
else:
self.__temperature = temp
그렇다면 @property
는 왜 사용하는 것일까?
첫째로 그동안 attribute를 public으로 선언했는데 private로 바꿔야 할 경우이다. 즉 데이터에 기존에는 필요없던 보호장치가 필요한 경우다. 이 경우 그동안 =
연산자를 사용해서 작성한 수많은 코드가 존재할 것이다. 그 모든 코드를 일일이 찾아가며 바꾸는 것은 지루하고 힘든 일이다. 그럴 때 @property
를 사용하면 그동안 작성한 코드를 유지하면서 데이터를 보호할 수 있다.
다음은 기존 프로젝트에서 @property
를 이미 사용하고 있는 경우이다. 접두어로 'get'이나 'set'을 붙인 메소드를 쓴 코드와 =
연산자를 사용한 코드는 모양이 정말 다르다. 같은 기능을 수행하는 동작이 2개이상인 코드는 읽는 사람을 혼란스럽게 한다. 일관된 코드를 유지하는 것이 협업과 유지보수를 편하게 한다.
마지막으로 property
함수를 소개하고 글을 마치겠다.
class Fridge:
def __init__(self, temp=4):
self.set_celsius(temp)
def get_celsius(self):
return self.__celsius
def set_celsius(self, temp):
if temp < 0:
self.__celsius = 0
elif temp > 10:
self.__celsius = 10
else:
self.__celsius = temp
celsius = property(get_celsius, set_celsius)
f = Fridge()
print(f.celsius == f.get_celsius())
# true
print(f.get_celsius())
# 4
f.celsius = 6
print(f.get_celsius())
# 6
위코드는 내가 자바스타일 객체지향이라고 소개했던 방식을 사용하고 있다. 딱 하나 Fridge
클래스 마지막 줄에 property라는 함수를 사용한 것만 빼고 말이다. 그 마지막 줄 property
의 마법으로 Fridge
의 인스턴트 f
에서 celsius
를 통해 __celsius
에 접근할 수 있는 것 같다. 하지만 위 코드는__celsius
에 접근하는 방법을 2가지나 만들기 때문에 피해야 할 방법이다. __get_celsius
와 __set_celsius
를 사용하면 데코레이터 @property
를 쓴 것과 똑같은 효과를 거둘 수 있지만 그러면 코드가 너무 지저분해 보이기 때문에 역시 피해야 할 방법이다.
참조
"Properties vs. Getters and Setters," python-course.eu,
"Bulit-in functions -- python 3.7", python.org,
프로그래밍을 공부하는 학생입니다. 저도 공부하는 내용이기 때문에 잘못된 정보가 있을 수 있습니다. 틀린 내용과 오타, 맞춤법 실수 등 지적이라면 무엇이든, 언제든 환영입니다.
또 이해가 안되는 내용이 있으면 댓글 부탁드립니다.