Pythonic решение моей проблемы getattr - PullRequest
5 голосов
/ 30 июля 2010

Раньше я использовал функции lower и getattr для вызова атрибутов по цепочке, например "thisattr.thatattr.blaattar" IE:

reduce(getattr, 'xattr.yattr.zattr'.split('.'), myobject)

Работает отлично, однако теперьУ меня есть новое требование, мои строки могут вызывать конкретный номер атрибута как таковой: "thisattr.thatattr [2] .blaattar"

reduce(getattr, 'xattr.yattr[2].zattr'.split('.'), myobject)

Теперь это не работает, я получаю xattr object has no attribute 'yattr[2]'ошибка.

Каким было бы однозначное решение, которое работает в любом случае?

С уважением

Ответы [ 6 ]

1 голос
/ 30 июля 2010

Вы можете попробовать:

import re
extended_split = re.compile(r'''\[\d+\]|[^\[.]+''').findall

def extended_getattr(obj, comp):
    if comp[0] == '[':
        return obj[int(comp[1:-1])]
    else:
        return getattr(obj, comp)

reduce(extended_getattr, extended_split('xattr.yattr[2].zattr'), myobject)

Обратите внимание, что предполагается, что содержимое внутри […] является неотрицательным десятичным числом.


Если вы беспокоитесь о производительности, этовсе еще быстрее, чем eval в моем тесте:

~:491$ python -m timeit -s 'from z import f1, f3, f, rs' 'f3(rs, "f")'   # eval
100 loops, best of 3: 5.62 msec per loop

~:492$ python -m timeit -s 'from z import f1, f3, f, rs' 'f1(rs, f)'     # my method
100 loops, best of 3: 4.69 msec per loop

Содержимое z.py:

import re
import random
from functools import reduce

extended_split = re.compile(r'''\[\d+\]|[^\[.]+''').findall

def extended_getattr(obj, comp):
    if comp[0] == '[':
        return obj[int(comp[1:-1])]
    else:
        return getattr(obj, comp)

class Foo(object):
    def __init__(self):
        self.foo = self

    def __getitem__(self, i):
        return self

def construct_random_string():
    yield 'foo'
    for i in range(2000):
        if random.randrange(2):
            yield '.foo'
        else:
            yield '[0]'


random.seed(0)  # to ensure fair comparison
rs = ''.join(construct_random_string())

f = Foo()

def f1(names, obj):
    return reduce(extended_getattr, extended_split(names), obj)

def f3(attrstring, objname) :
    return eval( '%s.%s' % (objname, attrstring) )
1 голос
/ 30 июля 2010

И позже вы можете захотеть вызвать какой-нибудь метод вместо получения атрибута.Быстрая реализация частей Python станет кошмаром.Даже текущее требование поддержки getattr / getitem не может быть решено как одна строка.

Вместо этого вы можете просто использовать сам python для интерпретации python,

# Create some object for testing
>>> class A(object):
...     b = None
... 
>>> a = A()
>>> a.b = A()
>>> a.b.b = A()
>>> a.b.b.b = [A(), A(), A(), A()]
>>> a.b.b.b[1].b
>>> a.b.b.b[1].b = "Some result"
>>> 
>>> ctx = {'obj':a, 'val':None}
>>> exec("val = obj.{0}".format('b.b.b[1].b')) in ctx
>>> ctx['val']
'Some result'
0 голосов
/ 18 сентября 2015

Я использую это

reduce(lambda i, j: getattr(i, j), 'xattr.yattr.zattr'.split('.'), myobject)

0 голосов
/ 30 июля 2010

Вот крошечный синтаксический анализатор для обработки обозначений фрагментов и вложенного списка:

# define class that we can just add attributes to
class Bag(object): pass

z = Bag()
z.xattr = Bag()
z.xattr.yattr = [Bag(), Bag(), Bag()]
z.xattr.yattr[2].zattr = 100
z.xattr.yattr[1] = [0,1,2,3,4,5]

from pyparsing import *

LBRACK,RBRACK = map(Suppress,'[]')
ident = Word(alphas+"_", alphanums+"_")
integer = Word(nums+'-',nums).setParseAction(lambda t:int(t[0]))
NONE = Literal("None").setParseAction(replaceWith(None))
indexref = LBRACK + Group(delimitedList((Optional(integer|NONE,None)), delim=':')) + RBRACK
compoundAttr = delimitedList(Group(ident("name") + ZeroOrMore(indexref)("index")), delim='.')

def lookup(ob, attr):
    try:
        attrParts = compoundAttr.parseString(attr)
    except ParseException:
        raise AttributeError("could not resolve compound attribute '%s'" % attr)

    # remaining code will raise AttributeError or IndexError as appropriate

    ret = ob
    for a in attrParts:
        ret = getattr(ret, a.name)
        if a.index:
            for i in a.index:
                if len(i) == 1:
                    ret = ret[i[0]]
                else:
                    ret = ret[slice(*i.asList())]
    return ret


print len(lookup(z, 'xattr.yattr'))
print len(lookup(z, 'xattr.yattr[1:3]'))
print len(lookup(z, 'xattr.yattr[None:3]'))
print lookup(z, 'xattr.yattr[1][None:4]')
print sum(lookup(z, 'xattr.yattr[1][:4]'))
print lookup(z, 'xattr.yattr[2].zattr')
0 голосов
/ 30 июля 2010

То, что вы просите, кажется довольно сложным, поскольку вы хотите смешать выбор атрибутов с вызовами методов (так как индекс - просто сахар для вызова).Вызов функций достаточно прост, используя getattr, чтобы получить связанный метод, но затем вам нужно преобразовать часть строки, содержащую аргументы, в фактические аргументы.

Учитывая, что вам понадобится eval() для вычисления аргументов в любом случае, почему бы просто не оценить все это?

def proc(objname, attrstring ) :
  return eval( '%s.%s' % (objname,attrstring) )

Ваш пример:

proc("myobject", "xattr.yattr[2].zattr")
0 голосов
/ 30 июля 2010

Вам нужно будет

  1. Получить xattr.yattr
  2. Получить второй предмет этого
  3. Из второго предмета получите затр

Как видите, это включало две разные операции. reduce не может этого сделать (элегантно). Решение, работающее для обоих, должно было бы проанализировать строку, чтобы определить, где требуется индексированный доступ. Простое, но хрупкое решение (т. Е. Ведет себя неопределенно при использовании BS) выглядит так:

def extended_chain_getattr(names, obj):
    import re
    result = obj        
    for name in names.split('.'):
        name_match = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)(\[\d\])?', name)
        assert name_match is not None
        result = getattr(result, name_match.group(1))
        if len(name_match.groups()) == 2:
            index = int(name_match.group(2))
            result = result[index]
    return result

С макушки головы, поэтому непроверенный.

...