IsaacLab의 configclass decorator 역할

nvidia
isaaclab
robotics
rl
python
Author

김진원

Published

August 20, 2024

TL;DR

@configclass는 Python @dataclass의 부족한 부분을 채워주는 IsaacLab의 decorator다.

  • 타입 annotation 없이도 필드 선언 가능
  • mutable default 값을 field(default_factory=...) 없이 자동 처리
  • to_dict, from_dict, copy, replace 등 유틸리티 메서드 자동 추가

Introduction

IsaacLab의 IsaacLabExtensionTemplate을 분석하다가 @configclass decorator를 발견했다.

configclass 사용 예시

@dataclass를 확장한 것이라는 설명을 보고, 두 decorator를 직접 비교해보기로 했다.


Decorator란?

Note

어떤 함수를 받아 특정 역할을 수행하고, 이를 다시 함수의 형태로 반환하는 함수

Python decorator는 다른 함수를 입력으로 받아, 원래 코드를 수정하지 않고 동작을 확장하거나 변경한다.

기본 구조

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # 함수 호출 전 처리
        result = func(*args, **kwargs)
        # 함수 호출 후 처리
        return result
    return wrapper

@my_decorator
def my_function():
    print("Hello, World!")

예시 — 로깅 decorator

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_decorator
def add(a, b):
    return a + b

add(3, 4)
# Calling add
# add returned 7

예시 — 실행 시간 측정

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

@timing_decorator
def compute_square(n):
    return n * n

compute_square(10)
# compute_square took 0.0000s

@dataclass 기능

Python 3.7에서 도입된 @dataclass__init__, __repr__, __eq__ 등의 메서드를 자동으로 생성해 보일러플레이트 코드를 줄여준다.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
    job: str = "Unknown"

p = Person(name="Alice", age=30)
print(p)       # Person(name='Alice', age=30, job='Unknown')
print(p == Person(name="Alice", age=30))  # True

order=True, frozen=True 옵션으로 정렬 비교와 불변성도 지원한다.

from dataclasses import dataclass, field

@dataclass(order=True, frozen=True)
class Product:
    name: str = field(compare=False)
    price: float
    quantity: int = field(default=0, compare=False)

p1 = Product(name="Laptop", price=999.99)
p2 = Product(name="Tablet", price=499.99)
print(p1 > p2)  # True

dataclass의 한계

  1. 모든 필드에 타입 annotation이 필수
  2. mutable 기본값(list, dict 등) 사용 시 field(default_factory=...)를 명시적으로 써야 함

@configclass 분석

IsaacLab 소스코드의 configclass 구현을 보면 다음과 같다.

def configclass(cls, **kwargs):
    # 타입 annotation 자동 추가
    _add_annotation_types(cls)
    # mutable 기본값 자동 처리
    _process_mutable_types(cls)
    # __post_init__ 확장
    if hasattr(cls, "__post_init__"):
        setattr(cls, "__post_init__", _combined_function(cls.__post_init__, _custom_post_init))
    else:
        setattr(cls, "__post_init__", _custom_post_init)
    # 유틸리티 메서드 추가
    setattr(cls, "to_dict", _class_to_dict)
    setattr(cls, "from_dict", _update_class_from_dict)
    setattr(cls, "replace", _replace_class_with_kwargs)
    setattr(cls, "copy", _copy_class)
    # dataclass로 래핑
    cls = dataclass(cls, **kwargs)
    return cls

사용 예시

from dataclasses import MISSING
from omni.isaac.lab.utils.configclass import configclass

@configclass
class ViewerCfg:
    eye: list = [7.5, 7.5, 7.5]       # annotation 없어도 OK
    lookat: list = [0.0, 0.0, 0.0]    # mutable default 자동 처리

@configclass
class EnvCfg:
    num_envs: int = MISSING
    episode_length: int = 2000
    viewer: ViewerCfg = ViewerCfg()

env_cfg = EnvCfg(num_envs=24)
print(env_cfg.to_dict())

env_cfg_copy = env_cfg.copy()
env_cfg_copy = env_cfg_copy.replace(num_envs=32)

to_dict / from_dict로 dictionary 변환, copy로 깊은 복사, replace로 값을 바꾼 복사본 생성이 가능하다.


dataclass vs configclass 비교

Feature @dataclass @configclass
목적 범용 데이터 저장 설정 관리 특화
타입 annotation 필수 자동 처리
Mutable default field(default_factory=...) 필요 자동 처리
유틸리티 메서드 __init__, __repr__, __eq__ + to_dict, from_dict, copy, replace

Reference