web-dev-qa-db-ja.com

ネストされたオブジェクトのgetattrとsetattr?

これはおそらく単純な問題なので、誰かが私の間違いを指摘したり、これが可能かどうかを簡単に指摘したりすることができれば幸いです。

プロパティとして複数のオブジェクトを持つオブジェクトがあります。これらのオブジェクトのプロパティを次のように動的に設定できるようにしたいと思います。

class Person(object):
    def __init__(self):
        self.pet = Pet()
        self.residence = Residence()

class Pet(object):
    def __init__(self,name='Fido',species='Dog'):
        self.name = name
        self.species = species

class Residence(object):
    def __init__(self,type='House',sqft=None):
        self.type = type
        self.sqft=sqft


if __name__=='__main__':
    p=Person()
    setattr(p,'pet.name','Sparky')
    setattr(p,'residence.type','Apartment')
    print p.__dict__

出力は次のとおりです。

{'pet':<main。Petオブジェクトat 0x10c5ec050>、 'residence':<main。Residence object at 0x10c5ec0d0>、 'pet.name': 'Sparky' 、 'residence.type': 'アパート'}

ご覧のとおり、個人のペットオブジェクトにname属性を設定するのではなく、新しい属性「pet.name」が作成されます。

Person.petをsetattrに指定することはできません。異なる子オブジェクトが同じメソッドによって設定されるためです。このメソッドは、テキストを解析し、関連するキーが見つかった場合にオブジェクト属性を入力します。

これを達成するための簡単な方法がありますか?

またはおそらく、文字列を解析し、必要な子オブジェクトが見つかるまでgetattrを複数回呼び出してから、見つかったオブジェクトでsetattrを呼び出す再帰関数を作成する必要がありますか?

ありがとうございました!

26
TPB

functools.reduce

import functools

def rsetattr(obj, attr, val):
    pre, _, post = attr.rpartition('.')
    return setattr(rgetattr(obj, pre) if pre else obj, post, val)

# using wonder's beautiful simplification: https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects/31174427?noredirect=1#comment86638618_31174427

def rgetattr(obj, attr, *args):
    def _getattr(obj, attr):
        return getattr(obj, attr, *args)
    return functools.reduce(_getattr, [obj] + attr.split('.'))

rgetattrrsetattrは、getattrsetattrのドロップイン置換であり、ドット付きattr文字列も処理できます。


import functools

class Person(object):
    def __init__(self):
        self.pet = Pet()
        self.residence = Residence()

class Pet(object):
    def __init__(self,name='Fido',species='Dog'):
        self.name = name
        self.species = species

class Residence(object):
    def __init__(self,type='House',sqft=None):
        self.type = type
        self.sqft=sqft

def rsetattr(obj, attr, val):
    pre, _, post = attr.rpartition('.')
    return setattr(rgetattr(obj, pre) if pre else obj, post, val)

def rgetattr(obj, attr, *args):
    def _getattr(obj, attr):
        return getattr(obj, attr, *args)
    return functools.reduce(_getattr, [obj] + attr.split('.'))

if __name__=='__main__':
    p = Person()
    print(rgetattr(p, 'pet.favorite.color', 'calico'))
    # 'calico'

    try:
        # Without a default argument, `rgetattr`, like `getattr`, raises
        # AttributeError when the dotted attribute is missing
        print(rgetattr(p, 'pet.favorite.color'))
    except AttributeError as err:
        print(err)
        # 'Pet' object has no attribute 'favorite'

    rsetattr(p, 'pet.name', 'Sparky')
    rsetattr(p, 'residence.type', 'Apartment')
    print(p.__dict__)
    print(p.pet.name)
    # Sparky
    print(p.residence.type)
    # Apartment
51
unutbu

1つの親と1つの子の場合:

if __name__=='__main__':
    p = Person()

    parent, child = 'pet.name'.split('.')
    setattr(getattr(p, parent), child, 'Sparky')

    parent, child = 'residence.type'.split('.')
    setattr(getattr(p, parent), child, 'Sparky')

    print p.__dict__

これは、この特定のユースケースの他の回答よりも簡単です。

6
ChaimG

magicattr と呼ばれるubntuの答えに基づいた単純なバージョンを作成しました。これは、astを解析およびウォーキングすることにより、attrs、リスト、およびdictでも機能します。

たとえば、このクラスでは:

class Person:
    settings = {
        'autosave': True,
        'style': {
            'height': 30,
            'width': 200
        },
        'themes': ['light', 'dark']
    }
    def __init__(self, name, age, friends):
        self.name = name
        self.age = age
        self.friends = friends


bob = Person(name="Bob", age=31, friends=[])
jill = Person(name="Jill", age=29, friends=[bob])
jack = Person(name="Jack", age=28, friends=[bob, jill])

あなたはこれを行うことができます

# Nothing new
assert magicattr.get(bob, 'age') == 31

# Lists
assert magicattr.get(jill, 'friends[0].name') == 'Bob'
assert magicattr.get(jack, 'friends[-1].age') == 29

# Dict lookups
assert magicattr.get(jack, 'settings["style"]["width"]') == 200

# Combination of lookups
assert magicattr.get(jack, 'settings["themes"][-2]') == 'light'
assert magicattr.get(jack, 'friends[-1].settings["themes"][1]') == 'dark'

# Setattr
magicattr.set(bob, 'settings["style"]["width"]', 400)
assert magicattr.get(bob, 'settings["style"]["width"]') == 400

# Nested objects
magicattr.set(bob, 'friends', [jack, jill])
assert magicattr.get(jack, 'friends[0].friends[0]') == jack

magicattr.set(jill, 'friends[0].age', 32)
assert bob.age == 32

また、evalを使用したり、Assign/Callノードを許可したりしないため、関数を呼び出したり値を割り当てたりすることもできません。

with pytest.raises(ValueError) as e:
    magicattr.get(bob, 'friends = [1,1]')

# Nice try, function calls are not allowed
with pytest.raises(ValueError):
    magicattr.get(bob, 'friends.pop(0)')
2
frmdstryr

unutbuの答え( https://stackoverflow.com/a/31174427/2683842 )には「バグ」があります。 getattr()が失敗し、defaultに置き換えられた後、getattrdefaultの呼び出しを続けます。

例:rgetattr(object(), "nothing.imag", 1)は私の意見では_1_に等しいはずですが、_0_を返します:

  • getattr(object(), 'nothing', 1) == 1。
  • getattr(1, 'imag', 1) == 0(1は実数であり、複素数成分がないため)。

解決

最初に欠落している属性でdefaultを返すようにrgetattrを変更しました:

_import functools

DELIMITER = "."

def rgetattr(obj, path: str, *default):
    """
    :param obj: Object
    :param path: 'attr1.attr2.etc'
    :param default: Optional default value, at any point in the path
    :return: obj.attr1.attr2.etc
    """

    attrs = path.split(DELIMITER)
    try:
        return functools.reduce(getattr, attrs, obj)
    except AttributeError:
        if default:
            return default[0]
        raise
_
2
jimbo1qaz

わかりましたので、質問を入力している間、私はこれを行う方法のアイデアを持っていて、それはうまくいくようです。ここに私が思いついたものがあります:

def set_attribute(obj, path_string, new_value):
    parts = path_string.split('.')
    final_attribute_index = len(parts)-1
    current_attribute = obj
    i = 0
    for part in parts:
        new_attr = getattr(current_attribute, part, None)
        if current_attribute is None:
            print 'Error %s not found in %s' % (part, current_attribute)
            break
        if i == final_attribute_index:
            setattr(current_attribute, part, new_value)
        current_attribute = new_attr
        i+=1


def get_attribute(obj, path_string):
    parts = path_string.split('.')
    final_attribute_index = len(parts)-1
    current_attribute = obj
    i = 0
    for part in parts:
        new_attr = getattr(current_attribute, part, None)
        if current_attribute is None:
            print 'Error %s not found in %s' % (part, current_attribute)
            return None
        if i == final_attribute_index:
            return getattr(current_attribute, part)
        current_attribute = new_attr
        i += 1

私はこれで私の質問が解決したと思いますが、これを行うためのより良い方法があるかどうか私はまだ興味がありますか?

これはOOPとpythonでかなり一般的なものでなければならないので、gatattrとsetattrはこれをネイティブにサポートしていないことに驚いています。

1
TPB

そして、jimbo1qazの答えに基づいたわかりやすい3ライナーは、極限まで削減されました。

def rgetattr(obj, path, default):
    try:
        return functools.reduce(getattr, path.split(), obj)
    except AttributeError:
        return default

使用法:

>>> class O(object):
...     pass
... o = O()
... o.first = O()
... o.first.second = O()
... o.first.second.third = 42
... rgetattr(o, 'first second third', None)
42

「スペース」はこのユースケースの典型的な区切り文字ではないことに注意してください。

0

再帰関数が大好きです

def rgetattr(obj,attr):
    _this_func = rgetattr
    sp = attr.split('.',1)
    if len(sp)==1:
        l,r = sp[0],''
    else:
        l,r = sp

    obj = getattr(obj,l)
    if r:
        obj = _this_func(obj,r)
    return obj
0
shouldsee