Computer Engineering/Fluent Python 정리

Fluent Python Chapter 19. 동적 속성과 프로퍼티

jordan.bae 2022. 4. 3. 20:00

Introduction

19장 부터 21장 까지는 이 책에서 마지막 Part인 메타프로그래밍입니다. 메타 프로그래밍에 대한 정의는 7장에서 데코레이터를 살펴보면서 잠시 살펴봤습니다.  다시 한 번 정의만 간단히 살펴보면 다음과 같습니다. 메타 프로그래밍은 컴퓨터 프로그램이 다른 컴퓨터 프로그램을 데이터와 같이 처리하는 능력을 가지도록 프로그래밍하는 방법(technique)이다. 
이번 장에서는 동적으로 속성에 접근할 수 있는 방법을 살펴보면서 해당 방법으로 프로그램이 데이터에 따라서 동작하게 됩니다. 동적으로 접근할 수 있는 다양한 방법을 살펴보고, 프로퍼티(property)를 이용해서 데이터에 대한 접근이나 속성에 대한 validation 등을 구현합니다. 또, 마지막 부분에서 다양한 속성 프로그래밍을 지원하는 핵심적인 특별 속성, 내장 함수, 특별 메서드에 대해 간략히 살펴봅니다. 

 

책 정리를 시작한 이유

책 정리를 시작한 이유

Chapter1의 Introduction 부분에서 이야기 한 것처럼 지난 5년간 다양한 언어나 프레임워크 및 프로그램을 공부하고 이용하여 소프트웨어를 개발했는데 이것저것 하다 보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신 있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한 해를 만들고 싶은 소망을 가지고 있습니다. 21장까지 정리를 성공하고 맛있는 걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요! 혹시, 제가 정리한 글이 괜찮으시면 구독 하시면 다음편이 나오면 바로 알림을 받으실 수 있습니다. 어느 덧 19장 정리하는 날이 왔네요. 예상한 것 보다는 많이 늦었지만 조금 만 더 힘내봐야겠습니다.

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

Fluent Python Chapter 8. 객체 잠조, 가변성, 재활용

Fluent Python Chapter 9. 파이썬스러운 객체

Fluent Python Chapter 10. 시퀀스 해킹, 해시, 슬라이스

Fluent Python Chapter 11. 인터페이스: 프로토콜에서 ABC까지

Fluent Python Chapter 12. 내장 자료형 상속과 다중 상속

Fluent Python Chapter 13. 연산자 오버로딩(feat. 제대로 하기)

Fluent Python Chapter 14. 반복형, 반복자, 제너레이터

Fluent Python Chapter 15. Context manager와 else 블록

Fluent Python Chapter 16. 코루틴 (Coroutine)

Fluent Python Chapter 17. Future를 이용한 동시성

Fluent Python Chapter 18. asyncio를 이용한 동시성

 

파이썬에서 데이터의 접근

파이썬에서는 객체의 속성과 메서드를 통틀어 속성이라고 합니다. 파이썬에서는 property나 __getattr__등을 통해서도 동적 속성을 구현할 수 있습니다. __getattr__ 같은 속성을 호출하는 매직메서드를 통해서 '가상속성' 을 구현할 수도 있습니다.

property나 __getattr__()를 사용하더라도 클래스의 인터페이스를 변경하지 않기 때문에 통일된 접근 원칙을 지킬 수 있습니다.

데이터에 대한 접근을 할 때 통일된 접근 원칙은 중요한 부분입니다.

통일된 접근 원칙

모듈이 제공하는 모든 서비스는 통일된 표기법을 이용해서 접근할 수 잇어야 한다. 통일된 표기법은 저장소를 이용해서 구현하거나 계산을 통해 구현하는 경우에도 동일하게 적용된다.

 

 

데이터 랭글링 예제를 통해 동적 속성과 관련 구현 살펴보기

데이터 랭글링에 대해 먼저 간단히 설명하면 아래와 같습니다.

데이터 랭글링이란, 분석과 같은 다양한 다운스트림 목적에 적합하고 가치 있게 만들기 위해 하나의 원시 데이터(raw data) 양식에서 다른 형식으로 데이터를 변환하고 매핑하는 과정이다.

예제로 osconfeed.json 파일을 사용한다. 파일은 스키마는 대략적으로 아래와 같다.

# oscon.json

{'Schedule': {'conferences': [{'serial': 115}],
  'events': [{'serial': 33451,
    'name': 'Migrating to the Web Using Dart and Polymer - A Guide for Legacy OOP Developers',
    'event_type': '40-minute conference session',
    'time_start': '2014-07-23 17:00:00',
    'time_stop': '2014-07-23 17:40:00',
    'venue_serial': 1458,
    'description': 'The web development platform is massive. With tons of libraries, frameworks and concepts out there, it might be daunting for the 'legacy' developer to jump into it.\r\n\r\nIn this presentation we will introduce Google Dart & Polymer. Two hot technologies that work in harmony to create powerful web applications using concepts familiar to OOP developers.',
    'website_url': 'http://oscon.com/oscon2014/public/schedule/detail/33451',
    'speakers': [149868],
    'categories': ['Emerging Languages']},
    
 ....
}
 

# 가장 root level key.
feed['Schedule'].keys()
dict_keys(['conferences', 'events', 'speakers', 'venues'])

현재는 dictionary 자료구조이기 때문에 이를 .(쩜) 속성 접근으로 동적으로 가지고 올 수 있도록 수정한다. 

객체.속성을 호출하면 객체의 __getattr__() 매직메서드가 호출되므로 해당 메서드를 수정해서 이를 가능하게 한다.

from collections import abc

class FrozenJSON:
    # 점 표기법으로 JSON과 유사한 객체를 순회하는 읽기 전용 클래스
    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

test = FrozenJSON(feed)
test.Schedule.events[0].name
'Migrating to the Web Using Dart and Polymer - A Guide for Legacy OOP Developers'

이 처럼 __getattr__() 메서드를 이용해서 동적으로 속성에 접근 할 수 있게 되었다.

 

위의 FrozenJSON class에는 몇 가지 문제가 있다.

1) keyword와 같은 속성으로 접근할 때

2) 올바른 파이썬 식별자(변수명이 아닐때) dfjk23

3) 존재 하지 속성에 대한 에러가 KeyError가 발생.

 

 1,2번은 변칙적으로 변수명에 _(언더바)를 붙이던지 약간 변형해서 처리할 수 있고 3번 같은 경우는 __getattr__() 함수에서 추가적인 예외처리를 통해 handling이 가능하다.

 

build classmethod대신 생성자 매직 메서드인 __new__() 를 통해서도 구현이 가능하다.

from collections import abc
from keyword import iskeyword


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 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])

이렇게 하면 초기화 함수인 __init__ 매직 메서드를 항상 호출하는 것이 아니라 해당 클래스의 객체를 반환할 때만 실행된다.

 

 

Shelve를 이용해서 OSCON feed 데이터 구조 변경하기

shelve는 pickle로 처리된 데이터를 보관하는 패키지로 abc.MutableMapping 클래스를 상속받아서, 매핑형이 제공하는 핵심 메서드들을 제공한다. 또, key는 문자열이어야 하고 value는 pickle 모듈이 처리할 수 있는 객체 타입이어야 한다.

import warnings

DB_NAME = 'data/schedule1_db'
CONFERENCE = 'conference.115'


class Record:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)  # <2>


def load_db(db):
    raw_data = load()  # <3>
    warnings.warn('loading ' + DB_NAME)
    for collection, rec_list in raw_data['Schedule'].items():  # <4>
        record_type = collection[:-1]  # <5>
        for record in rec_list:
            key = '{}.{}'.format(record_type, record['serial'])  # <6>
            record['serial'] = key  # <7>
            db[key] = Record(**record)  # <8>
            
  
db = shelve.open(DB_NAME)
load_db(db)
 
speaker = db['event.33950']
speaker.name
'There *Will* Be Bugs'

db.close()

위의 코드 중 중요한 부분만 정리해 보면 아래와 같다. 

 

- shelve.Shelf를 사용한 후에는 반드시 닫아야 한다. 컨텍스트를 안전하게 처리하기 위해서 with절을 사용하는 것이 좋다.

- self.__dict__.update(kwargs) 키워드 인수로부터 생성된 속성으로 객체를 생성할 때 간편히 사용하는 방법이다. 객체의 __slot__이 없으면  __dict__에 속성들이 존재하기 때문에 __dict__를 이용해 새로운 속성을 추가할 수 있다.

 

책에는 추가 적인 예제로 property를 사용해서 event 레코드 안의 venue나 spearkers의 본 레코드를 읽어 올 수 있도록 구현한 코드가 있다. 이 부분은 Django model에서 Foreignkey가 동작하는 방법과 비슷하다고 설명이 되어 있다. 이제 계속 property를 사용하는 부분이 나오기 때문에 이 부분은 생략한다.

 

관심이 있으신 분들은 이 코드를 참고해보시면 좋을 것 같습니다.

https://github.com/fluentpython/example-code/blob/master/19-dyn-attr-prop/oscon/schedule2.py

 

 

Property

속성 검증을 위한 property 사용.

코드 예를 들면 무게단위로 해당 품목을 판매하는 객체를 만드는 class LineItem이 있다.

class LineItem:

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

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

weight는 음수로 설정할 수 없도록 하기 위해서 property를 사용해서 getter나 setter를 만든다. 

class LineItem:

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight  # <1>
        self.price = price

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

    @property  # <2>
    def weight(self):  # <3>
        return self.__weight  # <4>

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

melon = LineItem('melon', -100, 10)
...
ValueError: value must be > 0

price 또한 음수로 설정되지 않도록 property를 구현할 수 있지만 반복이 들어간다. 이 부분은 20장에서 살펴보는 descriptor class로 해결할 수 있어서 넘긴다. 

 

property는 비록 데커레이터로 사용되는 경우가 많지만, 사실상 클래스다. 클래스 또한 callable로 사용할 수 있다.

property 생성자의 전체 시그니처는 다음과 같다.

property(fget=None, fset=None, fdel=None, doc=None)

그렇기 때문에 weight에 대한 getter, setter를 다음과 같이 표현할 수도 있다.

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

    def get_weight(self):  # <1>
        return self.__weight

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

    weight = property(get_weight, set_weight)  # <3>

지금부터는 property가 존재했을 때 객체 속성을 찾는 방식이 어떻게 바뀌는지 살펴본다.

 

 

class Class:
    data = "data"
    
    @property
    def prop(self):
        return 'the prop value'

c = Class()
# vars는 객체의 __dict__를 가져옴 -> class의 property나 속성은 포함되지 않음.
vars(c)
{}

# 객체의 속성을 먼저 체크하고 class의 속성을 찾음.
c.data
'data'

c.data = 'bar'
vars(c)
{'data': 'bar'}

c.data
'bar'

# 아래 코드를 통해 파이썬 인터프리터는 class의 property먼저 검색을 하는 것을 알 수 있따.
c.prop
'the prop value'

c.prop = 'instance prop'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [103], in <module>
----> 1 c.prop = 'instance prop'

AttributeError: cant set attribute

c.__dict__['prop'] = 'instance prop'
vars(c)
{'data': 'bar', 'prop': 'instance prop'}

c.prop
'the prop value'

# class의 property를 지우니깐 객체의 속성을 가져온다.
del Class.prop
c.prop
'instance prop'

코드에도 적었지만 파이썬 인터프리터의 객체의 속성의 검사는 obj.__class__에서 시작하고, class 안에 attr이라는 이름의 property가 없을 때만 c객체를 살펴본다. 

 

property 팩토리를 구현해서 여러 속성에 property를 구현할 때 반복되는 코드를 줄일 수 있다.

# BEGIN LINEITEM_V2_PROP_FACTORY_FUNCTION
def quantity(storage_name):  # <1>

    def qty_getter(instance):  # <2>
        return instance.__dict__[storage_name]  # <3>

    def qty_setter(instance, value):  # <4>
        if value > 0:
            instance.__dict__[storage_name] = value  # <5>
        else:
            raise ValueError('value must be > 0')

    return property(qty_getter, qty_setter)  # <6>
# END LINEITEM_V2_PROP_FACTORY_FUNCTION


# BEGIN LINEITEM_V2_PROP_CLASS
class LineItem:
    weight = quantity('weight')  # <1>
    price = quantity('price')  # <2>

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight  # <3>
        self.price = price

    def subtotal(self):
        return self.weight * self.price  # <4>

 

 

속성을 처리하는 핵심 속성 및 함수

동적 속성을 처리하기 위해 파이썬이 제공하는 명 가지 내장 함수와 특별메서드를 간단히 정리해본다.

 

속성 처리에 영향을 주는 특별 속성

- __class__

- __dict__

- __slots__

 

속성을 처리하는 내장 함수 (내부에서 매직 메서드를 호출하는 것들이 많음.)

- dir([object]) -> 객체 속성을 나열.

- getattr(object, name[, default])

- hasattr(object, name)

- setattr(object, name, value)

- vars([object])

 

속성을 처리하는 매직 메서드

- __delattr__(self, name)

- __dir__(self)

- __getattr__(self, name) -> __getattribute__(self, name)이 AttributeError를 발생시켰을 때 호출됨.

- __getattribute__(self, name)

- __setattr__(self, name, value)

 

마무리

19장에서는 동적으로 가상의 속성을 접근할 수 있도록 __getattr__() 매직메서드를 이용해서 JSON데이터를 다뤘고, 프로퍼티를 통해서 속성을 보호하거나 검증하였다. 또, property가 어떻게 동작하는지 객체의 속성을 참조했을 때 어떤 순서로 파이썬 인터프리터가 검색을 하는지도 살펴보았다.  

 

Reference

- Fluent Python Chapter 19

- https://github.com/fluentpython/example-code/tree/master/19-dyn-attr-prop

반응형