[코드잇] 쉽게 배우는 파이썬 문법 - 프로퍼티(Property) 4편

코드잇

안녕하세요, 온라인 코딩 스쿨 코드잇입니다.

저희는 지금 파이썬의 프로퍼티(property)가 어떻게 동작하는지 알기 위해 먼저 디스크립터(descriptor)가 무엇인지를 배우고 있는데요.

지난 시간에 디스크립터가 무엇인지 배워보았죠?

쉽게 배우는 파이썬 문법 - 프로퍼티(Property) 3편

이번엔 디스크립터를 조금 더 깊게 알아볼게요. 일단 클래스 내에

__get__ 메소드 또는 __set__ 메소드 또는 __delete__ 메소드 중 하나라도 정의되어 있으면 디스크립터라고 했었죠? 디스크립터는 인스턴스의 속성이 아닌 클래스의 속성으로 정의되어 있어야 합니다. (the descriptor must be in either the owner’s class dictionary or in the class dictionary for one of its parents, 공식문서 링크) 이건 파이썬이 그냥 그렇게 디자인/구현된 것입니다. 그러니까 앞으로 디스크립터를 사용하려면 클래스의 속성, 그러니까 클래스 변수로 두어야겠구나라고 생각하시면 됩니다.

그런데 이 디스크립터도 크게 두 가지 종류로 나눌 수 있습니다.

크게

1. 데이터 디스크립터(data descriptor)

2. 비데이터 디스크립터(non-data descriptor)

로 나눌 수 있는데요. 먼저 __set__ 메소드 또는 __delete__ 메소드 중 하나라도 정의되어 있는 객체를 데이터 디스크립터라고 합니다.

하지만 그냥 __get__ 메소드만 정의되어 있는 객체는 비데이터 디스크립터라고 합니다.

이 둘의 차이점은 해당 객체에 같은 이름의 인스턴스 속성을 설정하려고 할 때 드러납니다.

이전에 사용한 코드를 그대로 다시 가져와볼게요. 지금 CharacterInfo 에는 __set__ 메소드 또는 __delete__ 메소드 둘다 정의되어 있기 때문에 CharacterInfo 클래스로 만든 객체는 데이터 디스크립터입니다. 이 코드를 실행해보면

class CharacterInfo:
    def __init__(self, power, speed):
        self.power = power
        self.speed = speed

    def __get__(self, obj, objtype):
        print('(GET)정보 조회됨')
        return ('공격력 : '+str(self.power) + ' / 스피드 : ' + self.speed)

    def __set__(self, obj, val):
        print('(UPDATE)정보 갱신 시작')
        self.power = val.power
        self.speed = val.speed

    def __delete__(self, obj):
        print('(DELETE)정보 삭제하기')
        self.power =''
        self.speed = ''


class Guardian:
    info = CharacterInfo(10, '50km/h')


g1 = Guardian()   # g1 이라는 수호천사 인스턴스 하나 생성
print(g1.info)   # 인스턴스 g1의 초기 정보 출력 
info_after_upgrade = CharacterInfo(15, '70km/h')   # 업그레이드 아이템 적용 후 캐릭터 정보
g1.info = info_after_upgrade   # 새 캐릭터 정보를 인스턴스 g1 에 설정
print(g1.info)   # 인스턴스 g1의 정보 출력
del g1.info   # 인스턴스 g1의 정보 삭제
print(g1.info)   # 인스턴스 g1의 정보 출력

정상적으로 실행되어 아래와 같은 실행결과가 나옵니다.

(GET)정보 조회됨
공격력 : 10 / 스피드 : 50km/h
(UPDATE)정보 갱신 시작
(GET)정보 조회됨
공격력 : 15 / 스피드 : 70km/h
(DELETE)정보 삭제하기
(GET)정보 조회됨
공격력 :  / 스피드 :

실행결과를 보면 디스크립터의 각 메소드인 __get__ 메소드, __set__ 메소드, __delete__ 메소드가 잘 실행됨을 알 수 있습니다. 그런데 코드 중에서

g1.info = info_after_upgrade

새로운 캐릭터 정보를 설정하는 부분을 아래 코드처럼 바꿔봅시다.

g1.info = 3

실행해보면 이런 에러가 생깁니다.

AttributeError: 'int' object has no attribute 'power'

왜 그럴까요?

지금 CharacterInfo 클래스의 __set__ 메소드를 살펴보면

def __set__(self, obj, val):
    print('(UPDATE)정보 갱신 시작')
    self.power = val.power
    self.speed = val.speed

디스크립터인 CharacterInfo 클래스의 객체 g1power 라는 인스턴스 변수를 설정합니다. 그런데 제가 써준 3은 파이썬에서 int 객체입니다. 파이썬의 기본 객체에 power라는 인스턴스 변수가 있을리 없겠죠? 그래서 val.power 부분이 실행될 때 에러가 난겁니다.

자, 이제 CharacterInfo 클래스에서 __set__ 메소드와 __delete__ 메소드를 둘다 주석 처리해볼게요.

class CharacterInfo:
    def __init__(self, power, speed):
        self.power = power
        self.speed = speed

    def __get__(self, obj, objtype):
        print('(GET)정보 조회됨')
        return ('공격력 : '+str(self.power) + ' / 스피드 : ' + self.speed)
'''
    def __set__(self, obj, val):
        print('(UPDATE)정보 갱신 시작')
        self.power = val.power
        self.speed = val.speed

    def __delete__(self, obj):
        print('(DELETE)정보 삭제하기')
        self.power =''
        self.speed = ''
'''

class Guardian:
    info = CharacterInfo(10, '50km/h')


class Guardian:
    info = CharacterInfo(10, '50km/h')


g1 = Guardian()   # g1 이라는 수호천사 인스턴스 하나 생성
print(g1.info)   # 인스턴스 g1의 초기 정보 출력 
info_after_upgrade = CharacterInfo(15, '70km/h')   # 업그레이드 아이템 적용 후 캐릭터 정보
g1.info = 3   # 새 캐릭터 정보를 인스턴스 g1 에 설정
print(g1.info)   # 인스턴스 g1의 정보 출력
del g1.info   # 인스턴스 g1의 정보 삭제
print(g1.info)   # 인스턴스 g1의 정보 출력

이제 CharacterInfo 클래스로 만든 객체는 __get__ 메소드만 있기 때문에 비데이터 디스크립터입니다. 이 코드를 실행해보면 이런 결과가 나옵니다.

(GET)정보 조회됨
공격력 : 10 / 스피드 : 50km/h
3     
(GET)정보 조회됨
공격력 : 10 / 스피드 : 50km/h 

지금 실행결과 중 3번째 줄에 3이라고 잘 출력되었습니다. 이해가 되시나요? 그러니까 지금 g1 객체의 클래스 변수 infoCharacterInfo 클래스의 객체를 가리키고 있었고, 우리는 g1 객체에 또 info 라는 같은 이름의 인스턴스 속성을 추가했습니다. 그런데 아까와 달리 그냥 실제로 3이 인스턴스 속성으로 잘 추가되었네요.

아까 CharacterInfo 클래스에 __set__ , __delete__ 메소드가 있을 때는 그 메소드들이 실행되었는데 그 메소드들을 없애고 나니까 그냥 같은 이름의 인스턴스 속성이 추가된 겁니다.

그리고 3이 출력된 것을 보면 print(g1.info)가 실행될 때 CharacterInfo 클래스의 __get__ 메소드가 실행된 게 아니라 그냥 인스턴스 변수 3이 출력된 것임을 알 수 있습니다. 지금 디스크립터는 인스턴스 변수에 가려 아예 무시가 된 상태인 겁니다.

그리고 del g1.info가 실행될 때는 새로 추가된 인스턴스 변수 infog1의 속성 중에서 삭제됩니다. 마지막 줄 print(g1.info) 를 보면 원래의 info의 내용이 잘 출력되고 있죠?

클래스의 속성(클래스 변수)과 같은 이름으로 인스턴스의 속성(인스턴스 변수)을 설정하면 클래스의 속성이 수정되는 것이 아니라 새로운 인스턴스의 속성이 추가될 뿐입니다. 이 부분은 코드잇의 객체지향 프로그래밍에서 설명하니까 혹시 궁금하시면 참고하세요.(코드잇 'Python으로 배우는 객체지향 프로그래밍' 링크)

비데이터 디스크립터의 경우 이런 기본적인 원리가 그대로 적용됩니다. 하지만 __set__ , __delete__ 메소드 중 하나가 있는 데이터 디스크립터의 경우 새로운 인스턴스의 속성이 추가되는 것이 아니라 기존의 클래스 변수였던 데이터 디스크립터의 메소드들이 호출되는 것이구요.

디스크립터이더라도 __set__ 메소드 또는 __delete__ 메소드가 정의되었느냐에 따라서 이런 차이가 생기는 겁니다. g1.info = 3 이라고 쓴 부분은 원래 코드 g1.info = CharacterInfo(15, '70km/h')로 되돌려놓읍시다.

마지막으로 디스크립터로 할 수 있는 꿀팁, 하나 알려드릴게요! 디스크립터를 잘 활용하면 읽기 전용 클래스 변수를 만들 수 있습니다. 읽기 전용이란 말 그대로 초기 설정된 값을 읽을 수만 있고 새로운 값을 설정할 수는 없다는 뜻인데요. CharacterInfo 클래스의 __set__ 메소드를 아래처럼 바꿔봅시다.

class CharacterInfo:
    def __init__(self, power, speed):
        self.power = power
        self.speed = speed

    def __get__(self, obj, objtype):
        print('(GET)정보 조회됨')
        return ('공격력 : '+str(self.power) + ' / 스피드 : ' + self.speed)

    def __set__(self, obj, val):
        raise AttributeError


    def __delete__(self, obj):
        print('(DELETE)정보 삭제하기')
        self.power =''
        self.speed = ''


class Guardian:
    info = CharacterInfo(10, '50km/h')


g1 = Guardian()   # g1 이라는 수호천사 인스턴스 하나 생성
print(g1.info)   # 인스턴스 g1의 초기 정보 출력
info_after_upgrade = CharacterInfo(15, '70km/h')   # 업그레이드 아이템 적용 후 캐릭터 정보
g1.info = CharacterInfo(15, '70km/h')   # 새 캐릭터 정보를 인스턴스 g1 에 설정
print(g1.info)   # 인스턴스 g1의 정보 출력
del g1.info   # 인스턴스 g1의 정보 삭제
print(g1.info)   # 인스턴스 g1의 정보 출력

지금 __set__ 메소드를 보면

def __set__(self, obj, val):
    raise AttributeError

AttributeError 라는 에러를 일으키도록(raise) 되어있습니다. 파이썬에서 에러를 일으키는 방법은 나중에 설명해드릴게요! 일단 이렇게 쓰면 지금 __set__ 메소드가 실행될 때 AttributeError라는 에러가 발생한다고 이해하시면 됩니다. 이 말은 곧 이제 CharacterInfo 클래스의 객체를 새로 설정하려고 할 때 에러가 발생한다는 뜻입니다. 전체 코드를 실행해보면

(GET)정보 조회됨
공격력 : 10 / 스피드 : 50km/h
File 'C:/Users/user/PycharmProjects/sample/test.py', line 27, in 
    g1.info = CharacterInfo(15, '70km/h')
File 'C:/Users/user/PycharmProjects/sample/test.py', line 11, in __set__
    raise AttributeError
AttributeError

말씀드린 대로 에러가 납니다. 에러 내용을 자세히 보면 g1.info = CharacterInfo(15, '70km/h')에서 에러가 났습니다. 이제 info는 맨 처음 클래스에서 디스크립터로 설정된 이후에는 새로운 값을 지정할 수 없는 겁니다. 계속 같은 객체를 유지하고 싶은 경우에 이런 식으로 읽기 전용 변수를 유지하면 유용하겠죠?

이번 시간에는 디스크립터에 대해 좀 깊게 배워보았는데요. 다음 시간부터는 프로퍼티가 디스크립터를 어떻게 사용해서 만들어지는 건지 설명하겠습니다. 파이썬에 대해서 자세히 배우고 싶으시다면 3일 무료로 체험해보세요!

기업문화 엿볼 때, 더팀스

로그인

/