데코레이터 처리 기준을 함수에서 크래스로 바꿔서 처리하는 것이 클래스 데코레이터다.
class Decorator:
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs): # 객체 생성 후 받은 인자로 함수를 실행해 결과를 반환한다.
return self._func(*args, **kwargs)
@Decorator
def mul(x, y):
return x + y
mul(1, 2) # 2
# mul에는 Decorator 클래스의 객체가 들어있어서 __name__속성이 없다.
# 객체의 이름공간을 확인하면 내부에 함수가 저장된 것을 알 수 있다.
mul.__name__ # 예외 발생
mul.__dict__ # {'_func': <function __main__.mul(x, y)>}
클래스 데코레이터로 함수를 처리할 때도 실행 함수의 메타 정보를 전달해야 한다.
함수를 호출할 때 2번 실행하는 것은 함수의 정보를 처리하고 이를 객체에 세팅해야하기 때문이다.
실제 실행은 wraps(함수 이름)(객체 이름)으로 처리한다.
import functools as ft
class Decorator_: # 데코레이터 클래스에 함수의 메타 정보 전달
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
ft.wraps(self._func)(self) # 실행 함수의 정보를 객체 속성으로 생성
return self._func(*args, **kwargs)
@Decorator_
def add(x, y):
return x + y
add(1, 2) # 3
# 메타 정보를 세팅한 후에는 __name__속성을 조회할 수 있다.
# 객체의 이름공간을 확인하면 여러 개의 메타 정보가 들어간 것을 확인할 수 있다.
add.__name__ # 'add'
add.__dict__ # 함수의 메타 정보가 속성에 들어가 있는 것을 확인 가능하다.
객체도 데코레이터로 처리할 수 있다.
이 경우 객체를 실행할 때 내부에 함수를 정의해서 데코레이터를 처리하게 만든다.
class InsDecorator:
def __call__(self, func): # 객체가 실행될 때 함수의 매개변수를 받는다.
self._func = func # 객체 속성에 함수 저장
def inner(*args, **kwargs): # 내부 함수를 만들어 실행 함수의 인자를 받는다.
return self._func(*args, **kwargs) # 실행해서 결과를 반환
return inner # 내부 함수를 반환
ins = InsDecorator()
@ins
def mul(x, y):
return x * y
mul(1, 2) # 2
# 함수의 이름을 확인하면 __call__ 내부에 정의된 함수라는 것과 아무 속성이 없다는 것을 알 수 있다.
mul.__name__ # 'inner', 내부 함수
mul.__dict__ # {}, 속성이 없다.
실행 함수의 메타 정보를 처리하는 방식은 함수 데코레이터로 처리할 때와 같은 방식을 사용한다.
import functools as ft
class InsDecorator_:
def __call__(self, func):
self._func = func
@ft.wraps(self._func)
def inner(*args, **kwargs): # 내부 함수를 실행 함수의 메타 정보로 변경
return self._func(*args, **kwargs)
return inner
@InsDecorator_()
def add(x, y):
return x + y
add(1, 2) # 3
# 데코레이터 처리된 결과를 확인하면 실행 함수의 이름이 반환되었고 실제 함수도 객체 내부에 저장된 것을 확인할 수 있다.
add.__name__ # 'add'
add.__dict__ # {'__wrapped__': <function __main__.add(x, y)>}, 내부 함수 객체 이름공간에 함수가 별도로 저장
# __ wrapped__ 속성을 확인하면 같은 함수라는 것을 알 수 있따.
add.__wrapped__(1, 2) # 3
메소드 내에 데코레이터를 처리할 때는 내부에 함수를 정의하고 반환해서 다시 실행할 수 있는 환경을 구성한다.
메소드로 데코레이터를 ㅌ처리하려면 클래스를 정의할 때 초기화 함수에 함수를 저장하는 속성을 만들고 매개변수로 함수를 전달받게 지정한다.
import functools as ft
class MethDecorator: # 클래스에 정의된 함수 즉 메소드로 데코레이터를 처리하는 클래스 정의
def __init__(self, func=None):
self._func = func
def method(self, func): # 객체가 메소드를 호출할 때 실행 함수를 받아 데코레이터 처리
self._func = func
@ft.wraps(self._func) # 실행 함수의 메타 정보를 내부 함수의 메타 정보에 할당
def inner(*args, **kwargs): # 내부 함수는 실행 함수의 인자를 전달받게 정의
return self._func(*args, **kwargs) # 함수를 실행하고 결과 반환
return inner # 내부 함수 반환
meth = MethDecorator()
@meth.method
def mul(x, y):
return x * y
mul(1, 2) # 2
# 실행 함수의 메타 정보가 할당되어 실행 함수의 정보들이 이름공간과 속성에 전달된 것을 알 수 있다.
mul.__name__ # 'mul'
mul.__dict__ # {'__wrapped__': <function __main__.mul(x, y)>}
데코레이터 처리를 함수로 정의할 때, 인자로 클래스를 받고 내부에 초기화 함수를 정의해서 클래스에 추가한 후에 클래스를 반환한다.
특정 기능을 클래스에 추가하여 정의할 때도 데코레이터를 사용해 처리할 수 있다.
def decorator(cls):
def __init__(self, name): # 초기화 함수를 함수 내부에 정의
self.name = name
cls.__init__ = __init__ # 클래스에 초기화 함수 추가
return cls # 클래스를 반환
@decorator # 초기화 함수가 없는 클래스에 초기화 함수를 추가하는 데코레이터 처리
class DecoratedClass:
def method(self):
return self.name
dec = DecoratedClass('Decorator')
dec # <__main__.DecoratedClass at ~~~>
dec.method() # 'Decorator', 객체 속성을 읽고 반환
# 아무것도 없는 클래스에 위의 데코레이터를 처리하면 같은 초기화 함수가 삽입된다.
@decorator
class Klass:
pass
k = Klass('Class')
Klass.__dict__ # __init__이 들어있다.
k.__dict__ # {'name': 'Class'}
재귀 호출 방식은 일반적인 재귀 호출 방식과 꼬ㄱ리 재귀 호출 방식을 구분해서 처리해야 한다.
파이썬은 재귀 호출에 대한 제약이 있어 재귀 호출할 때 특정 개수 이상 사용할 수 없게 한다.
def sigma(n):
if n in [0]:
return 0
return n + sigma(n - 1)
sigma(10) # 55
def pie(n): # 연속된 숫자의 곱
if n in [0, 1]:
return 1
return n * pie(n - 1)
pie(3) # 6
꼬리 재귀 호출은 반환문에서 함수를 호출할 때 함수만 호출한다.
def sigma_(n, accum=0): # 꼬리 호출을 이용해서 결과 계산을 하지 않게 변경한 함수
if n in [0]:
return accum
return sigma_(n - 1, n + accum) # 계산된 상태를 보관하기 위해 매개변수 추가
sigma_(10) # 55
def pie_(n, accum=1):
if n in [0, 1]:
return accum
return pie(n - 1, n * accum)
pie_(3) # 6
재귀 호출 함수를 클래스로 변환하고 객체에 저장할 값은 표현식으로 바꿔서 특정 계산식도 들어올 수 있도록 변경
class Sigma: # 함수를 클래스로 변경
def __init__(self, expr):
self._expr = expr # 일반항을 문자열로 보관
def sigma(self): # 연속하는 값을 계산하는 함수 정의
def inner(self, n, accum=0):
if n in [0]:
accum = accum + eval(self._expr)
return accum # eval을 실행해서 값을 구한다.
else:
return inner(self, n - 1, (accum + eval(self._expr))) # 재귀 호출할 때 함수와 별도의 연산을 실행하지 않는다.
return inner
s = Sigma('2*n+1') # 일반항을 가진 객체 생성
s.sigma()(s, 10) # 121, 연속하는 숫자 계산 메소드를 실행한 후, 내부 함수를 반환받아 계산
# 클래스에 n을 전달해서 일반항에 있는 n에 해당 수가 들어간다.
개발자들이 속성 이름으로 이 속성을 사용하지 않게 관행화하는 것이 보호 속성이다.
class Klass: # 밑줄이 하나 있는 보호 속성을 정의한 클래스
_name = 'J' # 클래스 속성 이름에 밑줄을 하나 추가해서 보호 속성으로 만든다.
def __init__(self, name):
self._name = name # 객체의 속성 이름 앞에 밑줄을 하나 넣어 보호 속성으로 만든다.
def getName(self): # 보호 속성을 조회하는 함수 정의
return self._name
Klass.__dict__ # 보호 속성이라도 클래스에서 접근해서 조회할 수 있다.
Klass._name # 'J'
클래스로 객체를 만들어도 보호 속성이지만 직접 접근해서 조회할 수 있다.
k = Klass('K')
k.__dict__ # {'_name': 'K'}, 1개의 속성이 있다.
k.getName() # 'K'
k._name # 'K', 파이썬은 보호 속성을 명명규칙에 따라 표기만 해서, 직접 접근해서 조회하면 속성값을 알 수 있다.
클래스 속성과 객체 속성의 일므 앞에 밑줄을 2개 붙이면 이것을 맹글링(Mangling)이라고 한다.
클래스 내부에서는 밑줄이 2개 붙은 이름을 사용하지만, 외부에서 사용할 때는 밑줄이 붙은 클래스가 더 추가된다.
class Mangling:
__name = 'J' # 밑줄이 2개 붙은 클래스 속성 정의
def __init__(self, name):
self.__name = name # 밑줄이 2개 붙은 객체의 속성을 하나 정의
def getName(self): # 객체의 보호 속성을 조회하는 함수 정의
return self.__name
@classmethod
def getAttr(cls): # 클래스 속성을 조회하는 클래스 메소드 정의
return cls.__name # 클래스 메소드에 저장될 함수 정의
Mangling.__dict__ # 클래스 이름 앞에 밑줄이 하나 더 붙은 이름으로 바뀐 것을 확인할 수 있다.
# {'_Mangling__name': 'J' ~~~}
Mangling.__name # 클래스에서 클래스 속성을 접근하면 맹글링으로 처리되는데 이름이 매칭되는 것이 없어서 예외가 발생한다.
# 메소드로 조회할 수도 있고, 이름 작성 기준을 알고 조회하면 속성에 바로 접근할 수 있다.
Mangling.getAttr() # 'J'
Mangling._Mangling__name # 'J', 밑줄을 2개 붙이면 이름이 변경되는 것을 맹글링리라고 한다.
# 객체도 마찬가지로 이름이 변경된다.
m = Mangling('K')
m.__dict__ # {'_Mangling__name': 'K'}
m.getName() # 'K'
m.__name # 예외 발생
m._Mangling__name # 'K', 객체 속성도 맹글링 이름 체계로 작성해서 조회하면 값을 알 수 있다.
객체는 속성과 메소드로 처리하지만, 이름으로 접근하면 내부적으로 메소드가 실행되어 속성과 메소드를 같은 방식으로 사용할 수 있다.
파이썬 속성을 이름으로 접근해서 조회할 수 있는 property도 클래스다.
프로퍼티를 지정할 때 속성과 함수 이름이 같지 않게 속성의 이름을 보호 속성으로 만드는 것을 생각해야 한다.
class Data:
def __init__(self, data): # 초기화 함수를 정의할 때, 속성 이름을 보호 속성으로 생성
self._data = data
@property # 데코레이터로 함수를 프로퍼티 객체로 변환
def data(self): # 속성과 같은 이름으로 함수 정의
return self._data
Data.__dict__ # {'data': <property at ~~~>}, data가 프로퍼티 객체라는 것을 알 수 있다.
d = Data(10)
d.data # 10, 함수 이름에 접근해 객체의 속성값 조회
d.__dict__ # {'_data': 10}
d.data = 3 # 프로퍼티에 값을 할당하면 예외가 발생하는데, 이는 프로퍼티를 정의할 때 조회만 정의했기 때문이다.
클래스 하나, 속성 하나로 프로퍼티 등록
class DataInt: # 같은 이름으로 속성을 조회할 클래스 정의
@property # 조회하는 함수를 등록할 때는 property 클래스로 데코레이터 처리
def data(self):
return self._data
@data.setter # 이름으로 접근해서 속성을 갱신하는 함수를 정의할 때는 setter 메소드로 함수를 등록
def data(self, value):
if isinstance(value, int): # 갱신하는 값이 정수가 아니면 예외 처리
self._data = value
else:
raise AttributeError('Not Int')
@data.deleter # 이름으로 접근해서 속성을 삭제하는 함수를 정의할 때는 deleter 메소드로 함수를 등록
def data(self):
raise AttributeError("Can't delete")
t = DataInt() # 초기화 함수가 정의되지 않아서 속성이 없는 객체가 생성된다.
t.__dict__ # {}
t.data = 12 # 갱신할 수 있는 함수를 프로퍼티에 등록하고 값을 갱신할 때 객체에 속성을 추가
t.__dict__ # {'_data': 12}
t.data # 12
# 객체에 문자열 할당 했을 때
try:
t.data = '123'
except Exception as e:
print(e) # Not Int
# 객체를 삭제할 때
try: # 프로퍼티에 삭제를 정의했지만, 예외만 처리해서 삭제할 경우 내부에 정의된 예외 발생
del t.data
except Exception as e:
print(e) # Can't delete
프로퍼티 클래스
class my_property: # 프로퍼티 클래스 구조와 같은 클래스 정의
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget # 초기화 함수에 3개의 함수를 받아 3개의 속성에 저장하는 구조 생성
self.fset = fset
self.fdel = fdel
def setter(self, fset): # 변경하는 함수를 등록하는 메소드 정의
self.fset = fset
return self
def deleter(self, fdel): # 삭제하는 함수를 등록하는 메소드 정의
self.fdel = fdel
return self
def getter(self, fget): # 조회하는 함수를 등록하는 메소드 정의
self.fget = fget
return self
# 디스크립터 클래스 __get__, __set__, __delete__ 함수 정의한다.
# 이름으로 접근하면 자동 실행되어 결과를 반환한다.
def __get__(self, instance, owner): # 이름으로 조회하면 함수를 실행해 속성값 조회
return self.fget(instance)
def __set__(self, instance, value): # 이름으로 값을 갱신하면 속성을 변경하는 함수를 실행하여 결과를 변경
if not self.fset:
raise AttributeError
return self.fset(instance, value)
def __delete__(self, instance): # 이름으로 삭제하면 삭제하는 함수를 실행해 속성값 삭제
if not self.fdel:
raise AttributeError
return self.fdel(instance)
데코레이터 처리
class OnlyInt:
@my_property # 프로퍼티 클래스로 데코레이터를 처리해서 프로퍼티 객체 생성
def data(self): # 첫 번째로 조회 함수 등록
return self._data
@data.setter # 프로퍼티 객체의 setter 메소드로 등록
def data(self, value):
if isinstance(value, int):
self._data = value
else:
raise AttributeError('Only Int')
@data.deleter # 프로퍼티 객체의 deleter 메소드로 등록
def data(self):
raise AttributeError('No Delete')
c = OnlyInt()
c.__dict__ # {}
c.data = 999 # 속성 갱신
c.data # 999
try:
c.data = 'str'
except Exception as e:
print(e) # Only Int
계산을 처리하는 프로퍼티
class Double:
def __init__(self, value):
self.value = value
@property # 객체의 속성을 사용해 계산하는 함수를 프로퍼티로 처리
def double(self):
return self.value * 2
@double.setter # 변경하면 예외가 발생해야 하므로 변경 처리하는 함수 등록
def double(self, value):
raise AttributeError("Can't Change")
d = Double(3)
d.double # 6, 이름으로 접근하면 계산 결과를 반환
try:
d.double = 999
exception Exception as e:
print(e) # Can't Change
클래스를 캡슐화하려면 속성에 직접 접근해 사용하지 않게 구성해야 한다.
파이썬은 디스크립터라는 클래스를 만들고, 속성에 스페셜 메소드로 접근해서 조회, 갱신, 삭제한다.
class Descriptor:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner): # 이름으로 접근하면 값을 조회하는 스페셜 메소드 정의
print('get')
return self.value
class A: # 디스크립터 속성을 사용하는 클라이언트 클래스 정의
value = Descriptor(1) # 2개의 클래스 속성에 디스크립터 객체를 생성한다.
value1 = Descriptor(2) # 디스크립터 객체는 반드시 클래스 속성으로 정의한다.
# 객체를 만들고 객체에서 value 속성을 조회하면 __get__ 메소드가 실행된다.
a = A()
a.value # get이 출력되고 나서 1 출력
a.value1 # get이 출력되고 나서 2 출력
__get__과 __set__ 정의
class Descriptor_:
def __get__(self, instance, owner): # 디스크립터 객체, 클라이언트 객체, 클라이언트 클래스 매개변수를 정의
print('get')
if not hasattr(instance, '_value'): # 속성이 없으면 __set__으로 초깃값을 가진 속성 추가
self.__set__(instance,0)
return instance._value # 클라이언트 객체의 속성 조회
def __set__(self, instance, value): # 디스크립터 객체, 클라이언트 객체, 클라이언트 속성 갱신 매개변수 정의
print('set')
instance._value = value # 클라이언트 속성 갱신
class B:
value = Descriptor_()
value1 = Descriptor_()
B.__dict__ # 이름공간에 'value'와 'value1'이 클래스 속성으로 생성되어 있다.
b = B()
b.value # get, set, 0 순으로 출력된다. __get__을 호출하고, __set__을 호출해서 속성을 추가하기 때문이다.
b.value1 # get, 0 순으로 출력된다. 추가적인 속성이 만들어지지 않는다.
b.value1 = 999 # set 출력
b.__dict__ # {'_value': 999}, 클라이언트 객체를 확인하면 하나의 속성만 만들어져 있다.
b.value = 111 # 다른 속성으로 값을 갱신
b.__dict__ # {'_value': 111}, 클라이언트 객체 내에 하나의 속성만 만들어져서 값이 갱신된다.
디스크립터 클래스를 정의할 때 객체에 들어갈 속성 이름을 저장할 수 있는데, 이때는 __set_name__을 정의한다.
class Descriptor_c:
def __set_name__(self, owner, name): # 클라이언트의 속성 이름을 읽어와서 디스크립터 객체에 클라이언트 객체의 속성 이름을 관리
self._name = '_' + name
def __get__(self, instance, owner):
print('get')
if not (self._name in instance.__dict__):
self.__set__(instance, 0) # 클라이언트 객체의 이름공간에 속성 이름이 없으면 초기화
return instance.__dict__[self._name] # 클라이언트 이름공간을 이요해서 속성 조회
def __set__(self, instance, value):
print('set')
instance.__dict__[self._name] = value # 클라이언트 객체의 이름공간을 확인해서 값 갱신
class C:
value = Descriptor_c()
value1 = Descriptor_c()
c = C()
c.value # get, set, 0 순으로 출력
c.value1 # get, set, 0 순으로 출력
c.__dict__ # {'_value': 0, '_value1': 0}, 2개의 속성이 있다.
c.value = 111 # set
c.value1 = 999 # set
c.__dict__ # {'_value': 111, '_value1': 999}, 2개의 속성 값이 갱신되었다.
데이터 디스크립터란 데이터를 갱신할 수 있는 디스크립터이다.
비데이터 디스크립터란 단순히 조회만 하는 디스크립터이다.
디스크립터의 활용
def add(x, y):
return x + y
add = add.__get__(add) # 함수에서 __ get__에 인자로 함수 객체를 넣고 조회하면 메소드를 반환한다.
add # <bound method add of <function add at ~~~~~>>
# 메소드로 정의 되면 __func__ 내부에 함수가 저장된다.
add.__func__(1, 2) # 3
비데이터 디스크립터
class Descriptor_nd:
def __init__(self, func):
self._func = func
def __set_name__(self, owner, name): # 클라이언트 속성의 이름을 받아서 내부에 저장
self._name = '_' + name
def __get__(self, instance, owner): # 이름으로 접근하면 내부 함수 반환
print('get')
def inner(*args, **kwargs): # 내부함수는 저장된 함수의 인자를 받는다.
return self._func(*args, *kwargs)
return inner
def mul(x, y):
return x * y
class Non_data:
mul = Descriptor_nd(mul)
nd = Non_data()
nd.mul # 속성에 접근하면 내부 함수를 반환한다.
# get과 <function __main__.Descriptor_nd.__get__.<locals>.inner(*args, **kwargs)> 출력
nd.mul(1, 2) # get, 2 순으로 출력
디스크립터 클래스를 정의한 후에 데코레이터 사용
from functools import parial
class descriptor:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner): # 부분함수로 전달해서 다시 호출하면 실행한 결과를 보여준다.
return partial(self.func, instance)
class A:
@descriptor
def sum(self, a, b, c):
return a + b + c
A.__dict__ # sum을 클라이언트 클래스의 이름공간에 있는 것을 확인할 수 있고, sum은 디스크립터 속성임도 알 수 있다.
a = A()
a.sum(1, 2, 3) # 6, 객체가 sum에 접근하면 내부에 저장된 sum함수를 부분함수로 반환하고 이를 호출하면 실행 결과를 반환한다.
데이터 디스크립터를 작성하려면 __set__과 __delete__를 추가한다.
class MutableAttribute: # 속성을 갱신하는 디스크립터 클래스 정의
def __init__(self, value=None):
self.value = value
def __set_name__(self, owner, name): # 클라이언트 속성의 이름을 받아서 객체의 속성에 이름 저장
self._name = '_' + name
def __get__(self, instance, owner): # 클라이언트 객체 속성이 없으면 디스크립터 객체의 속성 값 반환
if self._name in instance.__dict__:
return instance.__dict__[self._name]
else:
return self.value
def __set__(self, instance, value): # 클라이언트 객체의 속성 갱신
instance.__dict__[self._name] = value
def __delete__(self, instance): # 디스크립터 객체의 값 삭제
del self.value
디스크립터 클래스에서 변경할 수 없는 객체를 만들려면 __set__과 __delete__로 정의하지만 변경이나 삭제할 때 예외가 발생한다.
class ImmutableAttribute: # 변경할 수 없는 디스크립터 클래스 정의
def __init__(self, func):
self.func = func
def __get__(self, instance, owner): # 이름으로 접근하면 저장된 함수 실행
return self.func(instance)
def __set__(self, instance, value): # 변경이 들어오면 예외 처리
raise AttributeError('Can not Change')
def __delete__(self, instance): # 삭제가 들어오면 예외 처리
raise AttributeError('Can not Delete')
class Circle:
pi = 3.1415
radius = MutableAttribute(10) # 반지름은 변경할 수 있는 디스크립터
diameter = ImmutableAttribute(lambda self : self.radius * 2) # 지름은 변경할 수 없는 람다 함수 등록
@ImmutableAttribute # 원의 둘레를 계산하는 함수를 변경할 수 없게 데코레이터 처리
def circumference(self):
return self.radius * self.pi * 2
@ImmutableAttribute # 원의 면적도 변경할 수 없게 처리
def area(self):
return self.radius ** 2 * self.pi
c = Circle()
c.radius, c.diameter # (10, 20), 이름으로 접근
c.circumference, c.area # 원 둘레와 면적도 이름으로 조회가 가능하다.
c.radius = 100 # 반지름을 갱신하고 면적을 다시 확인하면 면적 값이 변경된 것을 확인할 수 있다.
디스크립터 클래스를 작성하고 속성에 특정한 제약을 처리할 수 있다.
class Field:
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, int): # 정수가 아니면 갱신할 수 없도록 제약
raise ValueError(f'Not Int, Please Enter Int Value {self.name}')
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class A:
var = Field()
a = A()
a.var = 100 # 정수로 변경하면 값이 변경된다.
a.__dict__ # {'var': 100}
a.var = 100.1 # 실수로 변경하면 내부의 예외가 발생한다.