python 객체지향과 @property 데코레이터

taker·2020년 2월 25일
3

python

목록 보기
1/1
post-thumbnail

@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하게 선언하는 것이다.

동전 마술사 @property

파이썬에서도 위의 방식으로 데이터를 보호할 수 있다. 하지만 굳이 자바를 들먹인 이유는 파이썬은 좀 다른 방식을 제공하고 권장하기 때문이다. 일단 아래의 실행결과를 보자.

>>> 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

There should be one-- and preferably only one --obvious way to do it.

그렇다면 @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,


프로그래밍을 공부하는 학생입니다. 저도 공부하는 내용이기 때문에 잘못된 정보가 있을 수 있습니다. 틀린 내용과 오타, 맞춤법 실수 등 지적이라면 무엇이든, 언제든 환영입니다.
또 이해가 안되는 내용이 있으면 댓글 부탁드립니다.

profile
hello

0개의 댓글