CS

[Python] 동적 속성과 프로퍼티

왕밤빵도라에몽 2025. 6. 1. 13:04
728x90

동적 속성

Python 객체는 실행 중에 자유롭게 속성을 추가할 수 있다.

class User:
    def __init__(self, name):
        self.name = name

u = User("Kyu")
u.age = 25  # 동적으로 속성 추가
print(u.age)  # 25
print(u.__dict__)  # {'name': 'Kyu', 'age': 25}

동적 속성을 활용한 데이터 랭글링

JSON같은 경우, depth가 깊은 속성에 접근하고자 하면,
feed['Schedule']['events'][40]['name']과 같은 구문으로 접근해야할 것이다.
하지만 속성에 접근하는 클래스를 따로 둔다면,
feed.Schedule.events[40].name 과 같이 자바스크립트같은 문법으로도 접근 가능해질 것이다.

from collections import abc
import keyword

class FrozenJSON:
    def __new__(cls, arg):
        if isinstance(arg, abc.Mapping):
            return super().__new__(cls)
        elif isinstance(arg, abc.MutableSequence):
            return [cls(item) for item in arg]
        else:
            return arg

    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            if keyword.iskeyword(key):
                key += '_'
            self.__data[key] = value

    def __getattr__(self, name):
        try:
            return getattr(self.__data, name)
        except AttributeError:
            return FrozenJSON(self.__data[name])

    def __dir__(self):
        return self.__data.keys()
data = {
    "Schedule": {
        "events": [
            {"name": "Event A"},
            {"name": "Event B"}
        ]
    }
}

feed = FrozenJSON(data)
print(feed.Schedule.events[1].name)  # Event B

프로퍼티 기본 사용법

@property - 메서드를 속성처럼 사용할 수 있도록 해주는 Python의 데코레이터
주로 내부 상태를 캡슐화하거나, 유효성 검사 및 계산된 속성에 사용

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("반지름은 0 이상이어야 합니다.")
        self._radius = value
c = Circle(5)
print(c.radius)  # 5
c.radius = 10
# c.radius = -1  # ValueError 발생

프로퍼티 팩토리

비슷한 프로퍼티를 반복해서 만들어야 할 경우, 팩토리 함수를 활용하여 코드 중복을 줄인다

def typed_property(name, expected_type):
    private_name = f"_{name}"

    @property
    def prop(self):
        return getattr(self, private_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError(f"{name}는 {expected_type.__name__} 타입이어야 합니다.")
        setattr(self, private_name, value)

    return prop
class Point:
    x = typed_property('x', int)
    y = typed_property('y', int)

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(10, 20)
print(p.x, p.y)  # 10 20
# p.x = 'hello'  # TypeError 발생

프로퍼티 캐싱

속성 값이 계산 비용이 크고 변하지 않을 경우, 매번 호출할 필요 없이 한 번만 계산하고 캐싱해두면 성능이 향상됨
@cached_property는 딕셔너리 캐시에 값을 저장해서, 두 번째 접근부터는 그냥 저장된 값만 리턴함
쓰기 불가능(read-only) 이고, 값 변경 시엔 객체를 다시 만들어야 함

from functools import cached_property

class Data:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @cached_property
    def summary(self):
        print("summary 계산 중...")
        return sum(self.raw_data) / len(self.raw_data)
d = Data([10, 20, 30])
print(d.summary)  # summary 계산됨
print(d.summary)  # 캐시된 값 사용됨 (계산 X)

References
-『 전문가를 위한 파이썬(2판) 』, 루시아누 하말류, 한빛미디어, Part 5 - Chapter 22

728x90