본문 바로가기
Language/Python

[Python] 캡슐화(encapsulation) - @property 와 getter와 setter의 개념

by 며루치꽃 2021. 12. 14.

@property의 개념

class Robot:

    __population = 0

    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        Robot.__population += 1

    def __say_hi(self):
        print(f"Greetings, my masters call me {self.__name}.")

droid = Robot("R2-D2", 2)
print(droid.__age)	#에러
>>> AttributeError: 'Robot' object has no attribute '__age'

클래스 변수와 인스턴스 변수, 인스턴스 메서드를 위와 같이 private 형태로 만들었다고 하자.

이 경우에 droid 인스턴스에 __age로 접근을 해도 private하기 때문에 AttributeError 가 발생한다.

이 때, age에 read는 하고, update나 write을 불가능하게 하게 할 수는 없을까? 정답은 존재한다. 

바로 '@property' 라는 데코레이터를 사용하는 것이다. 

사용하는 방법은 아래와 같이 선언하고 싶은 메서드 위에 return을 해주는 형태로 이뤄진다.

class Robot:

    __population = 0

    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        Robot.__population += 1

    @property
    def age(self):
        return self.__age

    def __say_hi(self):
        print(f"Greetings, my masters call me {self.__name}.")

droid = Robot("R2-D2", 2)
print(droid.age)
>>> 2

선언한 메서드 위에 @property 데코레이터를 붙여주고 인스턴스에 해당 메서드로 접근하면 print가 되는 것을 볼 수 있다. 

 

Setter의 개념

droid.age = 50	#에러
>>> AttributeError: can't set attribute

그리고 age를 바꾸기 위해서 다음과 같이 값을 할당하게 되면 'AttributeError: can't set attribute' 가 난다. getter 라는 property를 통해서 age는 가져올 수 있으나 이 것을 바꿀 setter라는 attribute가 없기 때문이다. 

age를 바꾸고 싶을 경우 어떻게 해야할까? 정답을 settet를 사용하면 된다. 

setter를 사용하는 방법은 해당하는 property를 적어준후 setter를 적어준다. 그 이후 함수 이름은 동일하게 적고 인자로 할당할 새로운 값을 적는다. setter를 하는 이유는 직접적으로 set을 하지 말고 setter 안으로 들어와서 처리하는 방식이다. 

class Robot:

    __population = 0

    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        Robot.__population += 1

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, new_age):
        print(new_age)
        self.__age = new_age
        
droid = Robot("R2-D2", 2)
print(droid.age)
>>> 2
droid.age = 50
print(droid.age)
>>> 50

setter를 이용했을 때 동작방식은 다음과 같다.

1. droid.age = 50 이라고 한 순간 age 프로퍼티는 set이라는 attribute를 찾기 시작한다

2. set을 찾았을 경우 오른쪽에 어떤것을 대입했는지 찾는다. 이 경우에는 50이다.

3. 50이 바로 new_age 인자로 넘어가게 된다(setter 안의 new_age를 출력할 경우 50이 된다)

4. 안에서는 private을 사용할 수 있기 때문에 new_age를 할당해준다.

 

그럼 여기서 의문이 들 수 있다.

droid.age = -100

결국 접근할 수 있는데 은닉이 안되는 것이 아니냐?

바로 답은 setter에 값에 조건문을 넣어서 원하지 않는 값이 할당되는 것을 막을 수 있다. 

class Robot:

    __population = 0

    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        Robot.__population += 1

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, new_age):
        if new_age < 0:
            raise TypeError("invaild range")
        print(new_age)
        self.__age = new_age
        
droid = Robot("R2-D2", 2)
droid.age = -100	# 에러
>>> TypeError: invaild range

의도하지 않은 값을 할당할 경우 setter의 if문에 걸리게 되어 Error를 뱉어내줌으로써 원하지 않는 값을 할당하는 것을 막을 수 있다.

 

은닉의 이점

1. 위와같이, 인스턴스 변수 값에 대한 유효성 검사 및 수정을 막을 수 있다

2. 인스턴스 변수 값을 사용해서 적절한 값으로 보내고 싶을 때


인스턴스 변수 값을 사용해서 적절한 값으로 보내고 싶을 때라는 의미는 다음과 같다.

예를 들어 클래스 밖 외부에서 속성을 출력하고 싶을 때 특정 단어를 붙이고 싶을 때 다음과 같이 정의할 수 있다

 

class Robot:

    __population = 0

    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        Robot.__population += 1

    @property
    def name(self):
        return f"min {self.__name}"
        
droid = Robot("R2-D2", 2)
print(droid.name)
>>> min R2-D2

이렇게 되면 간접적으로 property를 사용해서 접근할 수 있게 되고 다음과 같이 원하는 출력의 형태로 변경할 수 있다.

댓글