책 '파이썬 코딩의 기술'의 6장인 메타클래스와 애트리뷰트에 대해서 정리해보고자 한다.
- 애트리뷰트
- @property
- 동적 기능을 위한 애트리뷰트 메서드
0. 정의
- 메타클래스 :
- 정의 : 클래스를 넘어서는 개념, class문을 이용해 클래스가 정의될 때마다 특별한 동작 제공
- 예시 : 동적으로 애트리뷰트 접근을 커스텀화해주는 내장 기능
- 주의 : '최소 놀람의 법칙' 에 따라 뜻하지 않은 부작용을 피하기
1. 애트리뷰트
다른 언어를 사용할 때는 class 내부에 getter, setter를 명시해준다. 하지만 파이썬은 그럴 필요가 없다. 공개 애트리뷰트(self로 시작하는, 클래스에서 명시한 변수)를 이용하면 된다.
# 1. setter, getter 사용 에시
class OldOne:
def __init__(self, x):
self._x = x #애트리뷰트에 밑줄 하나는 보호 애트리뷰트!
def get_x(self):
return self._x
def set_x(self, x):
self._x = x
# 2. 공개 애트리뷰트만 사용할 때
class NewOne:
def __init__(self, x):
self.x = x
self.y = 0
self.z = 0
oo = OldOne(2)
oo.set_x(3)
print(oo.get_x()) # 3
no = NewOne(2)
no.x = 3
print(no.x) # 3
공개, 비공개 애트리뷰트가 헷갈린다면 이전 포스트의 PEP8 스타일을 참고하자 : https://hi-lu.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%BD%94%EB%94%A9%EC%9D%98-%EA%B8%B0%EC%88%A0-%EB%A6%AC%EB%B7%B0-self%EC%99%80-cls-bytes%EC%99%80-str-f-%EB%AC%B8%EC%9E%90%EC%97%B4
파이썬 코딩의 기술 리뷰 - self와 cls, bytes와 str, f-문자열, 왈러스 연산자
오늘은 책 '파이썬 코딩의 기술'을 읽고 파이썬으로 코딩하는 데에 유용한 내용들을 정리해보고자 한다. 이 책의 챕터는 다음과 같이 구성되어 있다. 파이썬답게 생각하기 리스트와 딕셔너리 함
hi-lu.tistory.com
2. @property
property
애트리뷰트에 접근할 때 특별한 동작을 위해서 setter, getter 메서드를 정의해야 할 일(ex. 계산)이 있을 것이다. 이렇게 특별한 기능을 수행할 때는 데코레이터 @property를 사용하면 된다. 다만 너무 과하게 프로퍼티를 사용할 때는 차라리 코드를 리펙토링 하는 것을 권장하고 있다.
위 OldOne, NewOne 클래스 예시를 사용해서 다음과 같이 빌드업할 수 있다. 1번은 @property를 사용했을 때 클래스로, 공개애트리뷰트 z 값을 setter로 하여금 계산할 수 있게 되었다.
# proptery 사용
class PropertyOne(NewOne):
def __init__(self, x):
super().__init__(x)
self._y = 0
@property
def y(self):
return self._y
@y.setter
def y(self, y):
self._y = y
self.z = self._y + self.x
# property 미사용. 잘못된 예. npo.y를 하면 NewOne의 self.y가 호출될 것
class NoPropertyOne(NewOne):
def __init__(self, x):
super().__init__(x)
self._y = 0
def y(self, y):
self._y = y
self.z = self._y + self.x
return self._y
po = PropertyOne(3)
print(po.z) # 0
po.y = 10
print(po.z) #13
npo =NoPropertyOne(3)
print(npo.z) # 0
npo.y = 10
print(npo.z) # 0
디스크립터
디스크립터 프로토콜 : 파이썬에서 애트리뷰트 접근을 해석하는 방법을 정의하는데, @property는 다른 클래스 간 공유하지 못하기 때문에 이럴 때 사용할 수 있는 방법이다.
__get__, __set__ 메서드를 제공하는데, 여기서 주의해야 할 점은 한 클래스를 애트리뷰트로 가지는 모든 인스턴스들이 공유한다는 것이다.
class EXAttribute:
def __init__(self):
self._value = 0
def __get__(self, instance):
return self._value
def __set__(self, instance, value):
self._value = value
class EXClass:
exa1 = EXAttribute()
exa2 = EXAttribute()
exc = EXClass
exc.exa1 = 10
print(exc.exa1) # 10
exc2 = EXClass
exc2.exa1 = 20
print(exc.exa1) # 20. 10이 나올거라 생각했지만 틀림. EXAttribute() 인스턴스가 EXClass 내에서 한번만 생성되었기 떄문
이를 옳게 표현하기 위해서는 다음과 같이 디스크립터를 사용하면 된다. (솔직히 def__init__해서 self로 클래스를 받아주면 해결되긴 하지만 디스크립터 기능을 사용할 수는 없었다. 만약 def __init__(self)로 해주면 __get__과 __set__이 호출이 안 되는 걸 확인할 수 있었다.)
from weakref import WeakKeyDictionary
class RightEXAttribute:
def __init__(self):
self._values = WeakKeyDictionary()#메모리 누수를 막기 위해 {} 대신 사용. _values가 __set__호출에 전달된 모든 인스턴스에 대해 참조를 저장하기 떄문.
def __get__(self, instance, instance_type):
print(instance) # -> ExClass2의 object 반환
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
self._values[instance] = value
class EXClass2:
exa1 = RightEXAttribute()
exa2 = RightEXAttribute()
exc = EXClass2()
exc.exa1 = 10
print(exc.exa1) # 10
exc2 = EXClass2()
exc2.exa1 = 20
print(exc.exa1, exc2.exa1) # 10, 20
3. 동적 기능을 위한 애트리뷰트 메서드
위에서 살펴본 @property, 디스크립터를 사용하기 적합(사전 정의로 애트리뷰트를 가져오기) 하지 않는 경우, 즉 지연 계산 애트리뷰트가 필요하다면 사용할 수 있는 메서드는 다음과 같다.
__getattr__
인스턴스 딕셔너리에서 존재하지 않는 애트리뷰트를 접근할 때 호출되는 매서드이다. 아래 예시처럼 1) 존재하지 않은 애트리뷰트를 가 호출될 때 __getattr__가 호출되고, 2) 그 안에서 setattr가 호출되어 인스턴스에 추가해준다.
class LazyRecord:
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = f'{name}!'
setattr(self, name, value)
return value
data =LazyRecord()
print(data.__dict__) #{'exists': 5}
print(data.no) #no!
print(data.__dict__) #{'exists': 5, 'no': 'no!'}
print(hasattr(data, '1')) # True를 출력함과 동시에 __dict__에 {'1' : '1!'}이 추가됨
__getattribute__
__getattr__처럼 애트리뷰트가 존재하지 않을 때만 호출되지 않고, 객체 애트리뷰트에 접근할 때 항상 호출된다. AttributeError가 발생할 때 setattr를 호출하면 __getattr__의 기능도 수행할 수 있다.
이때 재귀를 피하기 위해서는 super().__getattribute__를 사용하면 된다. 재귀가 나타나는 이유는 self._data를 호출할 때 __getattribute__를 다시 호출하기 때문이다.(WrongGetAttributeRecord -> __getattribute__ -> self._data -> __getattribute__ ->......)
# 잘못된 예
class WrongGetAttributeRecord:
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print(f'{name!r}')
return self._data[name]
class RightGetAttributeRecord:
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print(f'{name!r}')
data_dict = super().__getattribute__('_data')
return data_dict[name]
data = WrongGetAttributeRecord({'name' : 1})
data.name # '_data'를 출력하며 무한재귀
rdata = RightGetAttributeRecord({'name' : 1})
rdata.name
다음 글에서는 아래 순서대로 이어서 정리를 해볼까 한다.
- __init__subclass__
- __set_name__
- 클래스 데코레이터
애트리뷰트에 대한 근본적인 이해를 할 수 있는 챕터라고 생각한다. 올해도 꼬물꼬물 차근차근 힘 내보자.
'python > 파이썬 코딩의 기술' 카테고리의 다른 글
python3 애트리뷰트, 메타클래스 - 2 (0) | 2022.02.20 |
---|---|
파이썬 리스트 append 연산 시 arr와 arr[:] 차이점 (0) | 2022.02.12 |
파이썬 코딩의 기술 리뷰 - 클래스, 인터페이스 (0) | 2022.01.16 |
파이썬 코딩의 기술 리뷰- 동시성과 병렬성 1 (2) | 2022.01.10 |
파이썬 코딩의 기술 - 컴프리헨션, 제너레이터 (0) | 2022.01.09 |