property
- 객체의 속성(attribute)을 정의하고, 그 속성을 읽고 쓰기 위한 메서드(getter와 setter)를 제공하는 기능
- OOP에서 데이터 은닉(encapsulation) 및 속성의 접근을 제어하는 데 사용된다.
- 필드를 외부에서 직접 접근하면 객체가 "잘못된 상태"를 가질 수 있기 때문에 getter와 setter을 사용하는 것이 좋다.
class Person :
def __init__(self, name : str, age : int) :
self.__name = name
self.__age = age
def get_name(self) :
return self.__name
def set_name(self, name) :
if True != isinstance(name, str) :
raise ValueError('name should be str type')
self.__name = name
name = property(get_name, set_name) # name에 대한 property
def get_age(self) :
return self.__age
def set_age(self, age) :
if True != isinstance(age, int) or 0 > age :
raise ValueError('age should be int type and not be negative')
self.__age = age
age = property(get_age, set_age) # age에 대한 property
person = Person('Jason', '24')
# print(person.get_name(), person.get_age())
print(person.name, person.age) # Jason 24
# person.set_name(10)
# person.set_age(-100)
person.name = 10 # ValueError: name should be str type
person.age = -100 # ValueError: age should be int type and not be negative
class & dict
- 너무 많은 클래스 인스턴스를 생성하는 상황에서는 클래스의 속도조차 문제가 될 수 있다.
- property가 30개 있는 class와 dict를 각각 2000개 생성하는 예시
import timeit
NUM_INSTANCES = 2000
class FeatureSet:
def __init__(
self, user_id: int,
feature1: float, feature2: float, (...) feature30: float,
):
self.user_id = user_id
self.feature1 = feature1
self.feature2 = feature2
(...)
self.feature30 = feature30
def create_class_instances() -> None:
for i in range(NUM_INSTANCES):
obj = FeatureSet(
user_id=i,
feature1=1.0 * i,
feature2=2.0 * i,
(...)
feature30=30.0 * i,
)
def create_dicts() -> None:
for i in range(NUM_INSTANCES):
obj = {
"user_id": i,
"feature1": 1.0 * i,
"feature2": 2.0 * i,
(...)
"feature30": 30.0 * i,
}
class_time = timeit.timeit(create_class_instance, number=1)
print(f"class: {class_time * 1000:.1f}ms")
dict_time = timeit.timeit(create_dict, number=1)
print(f"dict: {dict_time * 1000:.1f}ms")
class: 5.1ms
dict: 2.8ms
- class와 dict의 생성 속도가 1.8배 이상 나는 것을 확인할 수 있다.
- property의 숫자가 늘어날 수록, 생성하는 인스턴스가 많아질수록 격차는 더 커지게 된다.
- NUM_INSTANCES = 5000인 경우
__slot__을 사용하는 것은 도움이 될 수도 있지만, 이는 주로 메모리를 절약 해주는 효과가 크지, 속도를 빠르게 해주지는 않습니다.
class 대신 dataclass를 사용하는 것도 속도에 큰 차이가 나지 않았습니다.
class: 12.3ms dict: 6.7ms
__slots__ : 파이썬 클래스의 특수 속성으로, 객체의 속성을 미리 정의하고 메모리를 절약하기 위해 사용된다.
- 일반적으로 파이썬 클래스는 동적으로 속성을 추가할 수 있지만, 이 기능은 객체의 속성을 딕셔너리 형태로 저장하는데, 이는 메모리 사용에 비용이 많이 들 수 있습니다.
- __slots__을 사용하면 클래스 정의 시 어떤 속성을 사용할 것인지 미리 정의하고, 속성을 저장하는 데 필요한 메모리를 줄일 수 있습니다.(필드 생성에 대한 제약과 메모리 제약)
- 아래와 같이 static field를 만드는것 처럼 클래스 내부에 선언하고, 사용할 필드의 이름을 리스트 형태로 저장한다.
class Bar :
__slots__ = ['x', 'y']
def __init__(self, x, y) :
self.x = x
self.y = y
bar = Bar(1, 2)
bar.z = 10 # AttributeError: 'Bar' object has no attribute 'z'
- Bar 객체는 "x"과 "y"라는 두 가지 속성만 가지고 있으며, 다른 속성을 추가할 수 없습니다. 이렇게 하면 메모리 사용을 최적화할 수 있다.
- __slots__를 사용하여 클래스를 정의하면 객체가 생성 되었을 때 __dict__ attribute는 사라지고, x, y 필드는 객체 자체에 데이터가 보관 된다.
- => 위 예제를 처럼 bar객체에서 z를 접근하려 하면 __dict__ attribute가 없으므로 새로운 필드를 생성하지 못하고 z라는 attribute는 없다는를 오류가 발생 시킨다.
dataclass
- 파이썬 3.7 이후 버전에서 제공되는 데코레이터(Decorator)
- dataclass를 사용하면 데이터 클래스를 정의할 때 흔히 사용되는 일련의 메서드(예: __init__, __repr__, __eq__)를 자동으로 생성해줍니다. 이렇게 하면 클래스 정의가 간단해지고, 데이터 클래스를 생성하고 다루는데 편의성을 제공합니다.
- 말 그대로 데이터를 담는 클래스이고, import 후에 @dataclass 라는 데코레이터를 사용하면 된다.
# 그냥 클래스를 사용해서 아이스크림을 만들 때
import random
import string
def generate_id() -> str:
return "".join(random.choices(string.ascii_uppercase, k=12))
class Icecream:
def __init__(self, name: str, flavor: str):
self.name = name
self.flavor = flavor
def __str__(self) -> str:
return f"{self.name} - {self.flavor} flavor"
def main() -> None:
icecream = Icecream(name="together", flavor="vanilla")
print(icecream)
if __name__ == "__main__":
main()
# 데이터 클래스를 사용해서 아이스크림을 만들 때:
import random
import string
from dataclasses import dataclass
def generate_id() -> str:
return "".join(random.choices(string.ascii_uppercase, k=12))
@dataclass
class Icecream:
name: str
flavor: str
def main() -> None:
icecream = Icecream(name="together", flavor="vanilla")
print(icecream)
if __name__ == "__main__":
main()
- 한 눈에 봐도 코드의 길이가 짧아졌다.
- 기존의 클래스와 비교해서 데이터 클래스가 더 빠르고 쉬운 이유
- 데이터클래스가 initialize를 자동으로 만들어준다..
- __repr__ 메소드도 자동으로 생성해주기 때문에 위에서와 같이 프린트를 찍을 때 인스턴스의 값을 보기 위해 따로 __str__ 메소드를 작성해주거나 하지 않아도 된다.
(따로 메소드를 작성해주지 않아도 icecream을 프린트하면 Icecream(name='together', flavor='vanilla') 와 같이 출력됨) - name: str 같은 형식으로 타입을 쉽게 제공할 수 있다.
더불어 클래스에 property가 많으면서 동시에 많은 클래스 인스턴스를 생성하는 상황에서는 kwargs와 setattr 함수 또한 조심해야합니다.
보통 property가 많으면 타이핑하기 귀찮기에 kwargs와 setattr 함수를 사용하는 경우가 빈번할 수 있습니다. 하지만 이는 함수를 더 많이 호출하게 되어 속도가 기존보다도 2배 이상 더 느려질 수 있습니다.
kwargs
- keyword argument의 줄임말로 키워드를 제공한다.
- (키워드 = 특정 값) 형태로 함수를 호출할 수 있다. 그것은 그대로 딕셔너리 형태로 {'키워드': '특정 값'} 함수 내부로 전달된다.
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=30, city="New York")
name: Alice
age: 30
city: New York
setattr
- 객체의 속성을 동적으로 설정하는 데 사용된다.
- object에 존재하는 속성의 값을 바꾸거나, 새로운 속성을 생성하여 값을 부여
- setattr(object, attribute, value)
- object : 속성을 설정하려는 객체, attribute : 속성의 이름, value : 설정하려는 값
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# Person 객체를 생성
person = Person("Alice", 30)
# setattr을 사용하여 객체의 속성을 동적으로 설정
setattr(person, "city", "New York")
setattr(person, "email", "alice@example.com")
print(person.name) # Alice
print(person.age) # 30
print(person.city) # New York
print(person.email) # alice@example.com
dict 사용시 mypy와 같은 정적 type checking tool의 효과를 볼 수 없기에, 매우 제한적으로만 이용하고 있습니다.
Type Checker(타입 체커)
- 파이썬은 Dynamic typing(동적 타이핑) 언어라서, 잘못된 힌트를 적었더라도 실행엔 아무런 이상이 없다. 그래서 힌트가 제대로 됐는지 확인할 수가 없다.
- 파이썬에서 타입 힌트가 제대로 됐는지 확인하려면 별도의 Type Checker(타입 체커)를 사용해야 한다.
타입 체커는 mypy, pyright, pytype 등이 있는데 이 중에서 mypy와 pyright가 많이 활용되고 있다.
# example.py
def add(a: int, b: int) -> int:
return a + b
result = add("5", 3)
print(result)
mypy example.py
example.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"
- "dict"는 다양한 형태로 사용될 수 있으며, 키와 값의 데이터 형식이 동적으로 바뀔 수 있다.
=>정적 type checking tool이 코드의 타입 안정성을 확인하는 것이 어려울 수 있다.
물론 class를 포기하고 모든 객체를 dict로 관리하는 것은 기술 부채 측면에서 끔찍한 결정이 될 수 있습니다. 하지만 코드의 아주 일부분 중에서 연산량이 너무 많아 class의 생성 속도 조차 문제가 되는 상황이라면, class를 사용하지 않는 것도 고려해볼 수 있습니다
참고로 Python 3.11 부터는 class의 속도가 개선되어 dict와의 격차가 줄어듭니다.
출처
https://cocojen.tistory.com/13
https://onlywanna.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-static-type-checker