Scribbling

Python: Dynamic attributes and properties (동적 속성과 프로퍼티) 본문

Computer Science/Python

Python: Dynamic attributes and properties (동적 속성과 프로퍼티)

focalpoint 2022. 6. 17. 14:53

 

파이썬에서는 데이터 속성과 메서드를 통틀어 속성이라고 한다. 메서드는 단지 호출가능한 속성이다. 프로퍼티를 사용하면 클래스인터페이스를 변경하지 않고도 공개 데이터 속성을 접근자 메서드(getter & setter)로 대체할 수 있다. 파이썬 인터프리터는 obj.attr과 같은 점 표기법으로 표현되는 속성에 대한 접근을 __getattr__()과 __setattr__() 등 특별 메서드를 호출하여 평가한다. 사용자 정의 클래스는 __getattr__() 메서드를 오버라이드하여 '가상 속성'을 구현할 수 있다.

 

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

다음과 같은 json data를 랭글링 하는 예제를 살펴보자.

{ "Schedule": 
  { "conferences": [{"serial": 115 }],
    "events": [
      { "serial": 34505,
        "name": "Why Schools Don´t Use Open Source to Teach Programming",
        "event_type": "40-minute conference session",
        "time_start": "2014-07-23 11:30:00",
        "time_stop": "2014-07-23 12:10:00",
        "venue_serial": 1462,
        "description": "Aside from the fact that high school programming...",
        "website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505", 
        "speakers": [157509],
        "categories": ["Education"] }
    ],
    "speakers": [
      { "serial": 157509,
        "name": "Robert Lefkowitz",
        "photo": null,
        "url": "http://sharewave.com/",
        "position": "CTO",
        "affiliation": "Sharewave",
        "twitter": "sharewaveteam",
        "bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..." }
    ],
    "venues": [
      { "serial": 1462,
        "name": "F151",
        "category": "Conference Venues" }
    ]
  }
}

 

먼저, json 파일을 불러오는 코드이다. (loader.py)

import json

def load():
    with open('data/osconfeed.json') as f:
        return json.load(f)

 

json 파일을 원래 상태 그대로 다루다보면, 아래와 같이 번거로운 구문이 반복된다.

feed['Schedule']['speakers'][0]['name']

동적 속성을 이용하여 아래와 같이 사용할 수 있다면 아주 편할 것이다.

raw_feed = load()
feed = FrozenJSON(raw_feed)
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
print(feed.Schedule.speakers[0].name)

 

FrozenJSON class는 위와 같은 기능을 구현하고 있다.

가장 핵심은 __getattr__() method이다. 피드가 순회되면서 내포된 데이터 구조체가 차례로 FrozenJSON 포켓으로 변환된다.

from collections import abc
from loader import load

class FrozenJSON:
    '''
    점 표기법을 이용하여 JSON 객체를 순회하는
    읽기 전용 parsed class
    '''

    def __init__(self, mapping):
        self.__data = dict(mapping)

    def __getattr__(self, name):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON.build(self.__data[name])

    @classmethod
    def build(cls, obj):
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        else:
            return obj


raw_feed = load()
feed = FrozenJSON(raw_feed)
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
print(feed.Schedule.speakers[0].name)

 

위의 FrozenJSON class는 파이썬 키워드가 속성명으로 사용된 경우를 처리하지 못한다. 예를 들어, 파이썬 키워드인 class가 문제를 일으킨다.

grad = FrozenJSON({'name': 'Morgan', 'class': 1994})

이는 keyword인 경우 _를 추가해주는 방식으로 해결 가능하다.

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

 

__new__()를 이용한 융통성 있는 객체 생성

파이썬에서 실제로 객체를 생성하는 특별 메서드는 __new__()이다. 이 메서드는 클래스 메서드로서 객체를 반환한다. 그리고 그 객체가 __init__()의 첫 번째 인수 self로 전달된다. __init__()은 아무것도 반환하지 못하며 '초기화 메서드'이다. 

class FrozenJSON:
    '''
    점 표기법을 이용하여 JSON 객체를 순회하는
    읽기 전용 parsed class
    '''

    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):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON(self.__data[name])

 

2. 속성 검증을 위한 프로퍼티

상품을 판매하는 온라인 마켓에서 상품을 관리하기 위해 아래와 같은 class를 작성하였다.

class LineItem:

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

위 class는 단순한만큼 아래와 같은 오류를 유발한다. 즉, 속성에 이상한 값이 들어가면 문제가 된다.

product1 = LineItem('cereal', 10, 0.95)
print(product1.subtotal())
product1.weight = -1
print(product1.subtotal())

 

프로퍼티를 구현하면 obj.weight를 여전히 사용하면서도 문제가 해결된다.

class LineItem:

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

    @property
    def weight(self):
        return self.__weight

    @weight.setter
    def weight(self, value):
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('value must be > 0')

문제는 다른 속성에 대해서도 프로퍼티가 필요하다는 것이다. 이는 쓸떼없는 코드의 반복으로 이어진다.

 

다음은 프로퍼티의 '고전적인' 구문이다.

    def get_weight(self):
        return self.__weight

    def set_weight(self, value):
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('value must be > 0')

	weight = property(get_weight, set_weight, doc='weight in kg')

 

3. 프로퍼티 팩토리 구현하기

여기서는 quantity()라는 프로퍼티 팩토리를 구현한다.

def quantity(name):

    def getter(instance):
        return instance.__dict__[name]

    def setter(instance, value):
        if value > 0:
            instance.__dict__[name] = value
        else:
            raise ValueError('value must be > 0')

    return property(getter, setter)


class LineItem:

    weight = quantity('weight')
    price = quantity('price')

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

 

4. 속성을 처리하는 핵심 속성, 함수, 메서드

__dict__

객체나 클래스의 쓰기가능 속성을 저장하는 매핑이다.

__slots__

객체가 가질 수 있는 속성을 제한하는 속성이다.

getattr(object, name, [default])

object에서 name으로 식별되는 속성을 가져온다. 존재하지 않으면 AttributeError을 발생시키거나 default 값을 반환한다.

hasattr(object, name)

object가 해당 이름의 속성을 가지고 있는지 여부를 반환한다.

setattr(object, name, value)

object가 허용하면 name 속성에 value를 할당한다.

 

아래의 특별 메서드는 점 표기법을 사용하든 내장 함수를 사용하든 호출된다.

__getattr__(self, name)

__getattribute__(self, name): __getattr__() 보다 먼저 호출된다.

__setattr__(self, name, value)