49

我有一个对象 ( Person),它有多个子对象 ( Pet, Residence) 作为属性。我希望能够像这样动态设置这些子对象的属性:

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 object at 0x10c5ec050>, 'residence': <__main__.Residence object at 0x10c5ec0d0>, 'pet.name': 'Sparky', 'residence.type': 'Apartment'}

如您所见,不是在 的子对象上设置name属性,而是在.PetPersonpet.namePerson

  • 我无法指定person.petsetattr()因为不同的子对象将由相同的方法设置,如果/当找到相关键时,该方法会解析一些文本并填写对象属性。

  • 有没有一种简单/内置的方法来实现这一点?

  • 或者我可能需要编写一个递归函数来解析字符串并getattr()多次调用,直到找到必要的子对象,然后调用setattr()找到的子对象?

4

11 回答 11

107

你可以使用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('.'))

rgetattrandrsetattr是 and 的直接替代品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
于 2015-07-02T01:49:55.813 回答
44

对于开箱即用的解决方案,您可以使用operator.attrgetter

from operator import attrgetter
attrgetter(dotted_path)(obj)
于 2020-12-18T10:55:37.510 回答
7

对于一位父母和一位孩子:

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__

这比此特定用例的其他答案更简单。

于 2017-09-07T23:05:00.057 回答
5

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 是实数并且没有复分量)。

解决方案

我修改了 rgetattr 以返回default第一个缺失的属性:

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
于 2019-02-06T05:33:46.373 回答
3

这应该是一个

def getNestedAttr(obj,nestedParam):
     next = obj
     for p in nestedParam.split('.'):
         next = getattr(next,p)
     return next


class Issue : pass    
issue = Issue()
issue.status = Issue()
issue.status.name = "Hello"
getattr(issue,'status.name')
'''
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Issue' object has no attribute 'status.name'
'''
getNestedAttr(issue,'status.name')

#'Hello'

简单的解决方案

于 2020-03-20T05:23:14.760 回答
3

我根据 ubntu 的回答做了一个简单的版本,叫做magicattr,它也可以通过解析和遍历 ast 来处理 attrs、lists 和 dicts。

例如,对于这个类:

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 或允许分配/调用节点。

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)')
于 2018-06-04T22:31:50.353 回答
2

还有一个基于 jimbo1qaz 答案的易于理解的三线,减少到极限:

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

请记住,“空格”不是此用例的典型分隔符。

于 2019-09-09T10:52:11.900 回答
1

好的,所以在输入问题时,我知道如何做到这一点,而且它似乎工作正常。这是我想出的:

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 本身不支持这个。

于 2015-07-02T01:43:33.057 回答
1

这是类似于 ChaimG 的答案,但它适用于任意数量的案例。但是,它只支持获取属性,不支持设置它们。

requested_attr = 'pet.name'
parent = Person()

sub_names = requested_attr.split('.')
sub = None

for sub_name in sub_names:

    try:
        sub = parent.__getattribute__(sub_name)
        parent = sub

    except AttributeError:
        raise Exception("The panel doesn't have an attribute that matches your request!")

pets_name = sub
于 2020-06-18T21:51:57.203 回答
1

感谢上面接受的答案。这很有帮助。如果有人想扩展使用hasattr下面的代码:

def rhasattr(obj, attr):
    _nested_attrs = attr.split(".")
    _curr_obj = obj
    for _a in _nested_attrs[:-1]:
        if hasattr(_curr_obj, _a):
            _curr_obj = getattr(_curr_obj, _a)
        else:
            return False
    return hasattr(_curr_obj, _nested_attrs[-1])
于 2021-04-28T15:34:33.830 回答
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
于 2019-08-10T19:31:46.240 回答