Logo

파이썬의 property 사용법 (함수/데코레이터)

객체지향 프로그래밍 언어에서 프로퍼티(property)는 객체의 속성을 제어할 때 유용하게 사용되는 기능이며 데이터 캡슐화에 도움이 됩니다.

이번 포스팅에서는 파이썬에서 프로퍼티를 구현하기 위해서 property() 함수와 @property 데코레이터를 어떻게 사용하는지 알아보겠습니다.

필드명을 통한 접근

사람의 이름, 성, 나이 데이터를 담기 위한 간단한 클래스를 하나 작성해보겠습니다. 아래 Person 클래스는 이름 first_name, last_name, age 이렇게 3개의 필드로 이루어져 있습니다.

class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

Person 클래스의 인스턴스를 생성 후에, 현재 필드 값을 읽거나 새로운 필드 값 쓰는 것은 매우 자유롭습니다.

>>> person = Person("John", "Doe", 20)
>>> person.age
20
>>> person.age = person.age + 1
>>> person.age
21

이렇게 필드명을 사용해서 객체의 내부 데이터에 접근하는 것은 편리하지만, 해당 데이터는 외부로 부터 무방비 상태에 놓이게 됩니다.

Getter/Setter 메서드 구현

클래스 인스턴스의 내부 데이터를 보호하기 위해서 데이터의 접근용 메서드를 작성하는 것은 객체 지향 프로그래밍에서 흔히 볼 수 있는 패턴입니다. 일반적으로 데이터를 읽어주는 메서드를 getter(게터), 데이터를 변경해주는 메서드를 setter(세터)라고 합니다.

Person 클래스에 age 필드에 대한 get_age()set_age() 메서드를 추가해보겠습니다. 나이는 음수가 될 수 없으므로 set_age() 메서드에 음수가 인자로 넘어오면 예외가 발생하도록 하였습니다.

class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self._age = age

    def get_age(self):
        return self._age

    def set_age(self, age):
        if age < 0:
            raise ValueError("Invalid age")
        self._age = age

여기서 놓치기 쉬운 부분은 _로 시작하는 이름을 가진 변수는 외부에서 직접 접근하지 않는 파이썬의 관행에 따라, 인스턴스 변수명을 age 대신에 _age로 변경하였다는 것입니다.

>>> person = Person("John", "Doe", 20)
>>> person.get_age()
20
>>> person.set_age(-1)
ValueError: Invalid age
>>> person.set_age(person.get_age() + 1)
>>> person.get_age()
21

이제 Person 클래스의 인스턴스에 저장되어 있는 나이 데이터에 접근하거나 변경하려면 메서드를 이용해야 합니다.

이렇게 getter/setter 메서드를 사용하면 객체의 내부 데이터에 대한 접근을 좀 더 체계적으로 통제할 수 있고, 속성을 읽거나 쓸 때 유효성 검사와 같은 추가적인 로직을 실행할 수 있습니다. 하지만 클래스의 프로그래밍 인터페이스가 변경됨에 따라 기존에 해당 클래스가 사용되는 코드를 모두 수정해줘야 한다는 단점이 있습니다.

property() 함수

파이썬의 내장 함수인 property()를 사용하면 마치 필드명을 직접 사용하는 것처럼 깔끔하게 getter/setter 메서드가 호출되게 할 수 있습니다.

class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self._age = age

    def get_age(self):
        return self._age

    def set_age(self, age):
        if age < 0:
            raise ValueError("Invalid age")
        self._age = age

    age = property(get_age, set_age)

property() 함수의 첫 번째 인자로 getter 메서드를 두 번째 인자로 setter 메서드를 넘겨주면 age라는 필드명을 이용해서 다시 나이 데이터에 접근할 수 있게 됩니다.

>>> person = Person("John", "Doe", 20)
>>> person.age
20
>>> person.age = -1
ValueError: Invalid age
>>> person.age = person.age + 1
>>> person.age
21

나이를 음수로 변경하려고 하면 set_age() 메서드를 직접 호출하는 것과 동일하게 예외가 발생하는 것을 볼 수 잇습니다.

이렇게 property() 함수를 사용하면 클래스를 사용하는 측면에서는 일반 필드에 접근하는 것처럼 보이지만 내부적으로 getter와 setter 메서드가 호출이 됩니다. 즉, 두 마리의 토끼를 다 잡는다고 볼 수 있는데요. 프로그래밍 인터페이스가 바뀌지 않아서 기존 코드를 수정할 필요가 없으면서도, 여전히 추가적인 로직을 속성값에 접근할 때 실행할 수 있습니다.

@property 데코레이터

파이썬에 내장된 데코레이터인 @property를 사용하면 위와 동일하게 작동하는 코드를 좀 더 간결하고 읽기 편하게 작성할 수 있습니다.

class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self._age = age

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

    @age.setter
    def age(self, age):
        if age < 0:
            raise ValueError("Invalid age")
        self._age = age

기존의 getter 메서드 위에 @property 데코레이터를 선언하고, 메서드 이름으로부터 get_을 삭제합니다. setter 메서드의 경우에는 @<필드명>.setter 데코레이터를 선언하고, 메서드 이름으로부터 set_을 삭제합니다.

>>> person = Person("John", "Doe", 20)
>>> person.age
20
>>> person.age = -1
ValueError: Invalid age
>>> person.age = person.age + 1
>>> person.age
21

property 함수나 @property 데코레이터를 사용했을 때 가장 큰 이점은 외부에 티 내지 않고 내부적으로 클래스의 필드 접근 방법을 바꿀 수 있다는 것인데요. Person 클래스를 사용하는 관점에서 봤을 때 나이 데이터는 항상 age라는 필드명으로 접근하고 변경할 수 있다는 사실은 변하지 않기 때문입니다.

읽기 전용 프로퍼티

클래스를 작성하다보면 다른 필드로 부터 값이 유추되거나 계산되는 읽기 전용 필드가 필요할 때가 있습니다. 예를 들어, Person 클래스에서 전체 이름을 얻고 싶다면 다음과 같이 @property 데코레이터를 이용해서 full_name 필드를 추가해줄 수 있습니다.

class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self._age = age

    @property
    def full_name(self):
        return self.first_name + " " + self.last_name

이렇게 @property 데코레이터를 활용하면, 마치 해당 객체의 일반 속성을 읽듯이 사용할 수 있게 됩니다.

>>> person = Person("John", "Doe", 20)
>>> person.full_name
'John Doe'

어떤가요? 일기 전용 속성에 접근을 할 때 굳이 함수를 호출하지 않아되 되서 코드 가독성이 좀 더 좋아지겠죠?

전체 코드

본 포스팅에서 제가 작성한 전체 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

https://dales.link/mvr

마치면서

지금까지 파이썬에 내장되어 있는 property() 함수와 @property 데코레이터를 어떻게 사용하는지에 대해서 살펴보았습니다. 본 포스팅에서는 다루지는 않았지만 이 방법을 통해 setter와 getter 메서드 뿐만 아니라 deleter 메서드도 함께 선언할 수 있습니다. property() 함수와 @property 데코레이터에 대한 좀 더 자세한 내용은 파이썬 공식 문서를 참고 바랍니다.