Каков наилучший способ автоматического назначения атрибутов в Python, и это хорошая идея? - PullRequest
29 голосов
/ 06 сентября 2010

Вместо написания такого кода каждый раз, когда я определяю класс:

class Foo(object): 
     def __init__(self, a, b, c, d, e, f, g):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        self.f = f
        self.g = g

Я мог бы использовать этот рецепт для автоматического назначения атрибутов .

class Foo(object):
     @autoassign
     def __init__(self, a, b, c, d, e, f, g):
        pass

Два вопроса :

  1. Есть ли недостатки или подводные камни, связанные с этим ярлыком?
  2. Есть ли лучший способ добиться аналогичного удобства?

Ответы [ 9 ]

25 голосов
/ 06 сентября 2010

Есть некоторые вещи в коде автоматического назначения, которые меня беспокоят (в основном стилистическая, но еще одна серьезная проблема):

  1. autoassign не присваивает Атрибут 'args':

    class Foo(object):
        @autoassign
        def __init__(self,a,b,c=False,*args):
            pass
    a=Foo('IBM','/tmp',True, 100, 101)
    print(a.args)
    # AttributeError: 'Foo' object has no attribute 'args'
    
  2. autoassign действует как декоратор. Но autoassign(*argnames) называет функция, которая возвращает декоратор. Чтобы достичь этой магии, autoassign необходимо проверить тип его первого аргумент. Если дать выбор, я предпочитаю функции не тестировать тип его аргументов.

  3. Кажется, что количество кода, посвященного настройке sieve, лямбды в лямбдах, ифильтры и множество условий.

    if kwargs:
        exclude, f = set(kwargs['exclude']), None
        sieve = lambda l:itertools.ifilter(lambda nv: nv[0] not in exclude, l)
    elif len(names) == 1 and inspect.isfunction(names[0]):
        f = names[0]
        sieve = lambda l:l
    else:
        names, f = set(names), None
        sieve = lambda l: itertools.ifilter(lambda nv: nv[0] in names, l)
    

    Я думаю, что может быть проще. (Увидеть ниже).

  4. for _ in itertools.starmap(assigned.setdefault, defaults): pass. Я не думаю map или starmap должен был позвонить функции, единственной целью которых является их побочные эффекты. Это могло бы быть написано более ясно с мирским:

    for key,value in defaults.iteritems():
        assigned.setdefault(key,value)
    

Вот альтернативная более простая реализация, которая имеет те же функциональные возможности, что и автоматическое назначение (например, может включать и исключать), и которая касается вышеуказанных пунктов:

import inspect
import functools

def autoargs(*include, **kwargs):
    def _autoargs(func):
        attrs, varargs, varkw, defaults = inspect.getargspec(func)

        def sieve(attr):
            if kwargs and attr in kwargs['exclude']:
                return False
            if not include or attr in include:
                return True
            else:
                return False

        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            # handle default values
            if defaults:
                for attr, val in zip(reversed(attrs), reversed(defaults)):
                    if sieve(attr):
                        setattr(self, attr, val)
            # handle positional arguments
            positional_attrs = attrs[1:]
            for attr, val in zip(positional_attrs, args):
                if sieve(attr):
                    setattr(self, attr, val)
            # handle varargs
            if varargs:
                remaining_args = args[len(positional_attrs):]
                if sieve(varargs):
                    setattr(self, varargs, remaining_args)
            # handle varkw
            if kwargs:
                for attr, val in kwargs.items():
                    if sieve(attr):
                        setattr(self, attr, val)
            return func(self, *args, **kwargs)
        return wrapper
    return _autoargs

А вот модульный тест, который я использовал для проверки его поведения:

import sys
import unittest
import utils_method as um

class Test(unittest.TestCase):
    def test_autoargs(self):
        class A(object):
            @um.autoargs()
            def __init__(self,foo,path,debug=False):
                pass
        a=A('rhubarb','pie',debug=True)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)

        class B(object):
            @um.autoargs()
            def __init__(self,foo,path,debug=False,*args):
                pass
        a=B('rhubarb','pie',True, 100, 101)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)
        self.assertTrue(a.args==(100,101))        

        class C(object):
            @um.autoargs()
            def __init__(self,foo,path,debug=False,*args,**kw):
                pass
        a=C('rhubarb','pie',True, 100, 101,verbose=True)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)
        self.assertTrue(a.verbose==True)        
        self.assertTrue(a.args==(100,101))        

    def test_autoargs_names(self):
        class C(object):
            @um.autoargs('bar','baz','verbose')
            def __init__(self,foo,bar,baz,verbose=False):
                pass
        a=C('rhubarb','pie',1)
        self.assertTrue(a.bar=='pie')
        self.assertTrue(a.baz==1)
        self.assertTrue(a.verbose==False)
        self.assertRaises(AttributeError,getattr,a,'foo')

    def test_autoargs_exclude(self):
        class C(object):
            @um.autoargs(exclude=('bar','baz','verbose'))
            def __init__(self,foo,bar,baz,verbose=False):
                pass
        a=C('rhubarb','pie',1)
        self.assertTrue(a.foo=='rhubarb')
        self.assertRaises(AttributeError,getattr,a,'bar')

    def test_defaults_none(self):
        class A(object):
            @um.autoargs()
            def __init__(self,foo,path,debug):
                pass
        a=A('rhubarb','pie',debug=True)
        self.assertTrue(a.foo=='rhubarb')
        self.assertTrue(a.path=='pie')
        self.assertTrue(a.debug==True)


if __name__ == '__main__':
    unittest.main(argv = sys.argv + ['--verbose'])

PS. Использование autoassign или autoargs совместимо с дополнением кода IPython.

7 голосов
/ 06 сентября 2010

Один недостаток: многие IDE анализируют __init__.py для обнаружения атрибутов объекта. Если вы хотите, чтобы автоматическое завершение кода в вашей среде IDE было более функциональным, вам, возможно, лучше изложить его по старинке.

7 голосов
/ 06 сентября 2010

Есть ли лучший способ добиться аналогичного удобства?

Я не знаю, обязательно ли это лучше, но вы могли бы сделать это:

class Foo(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


>>> foo = Foo(a = 1, b = 'bar', c = [1, 2])
>>> foo.a
1
>>> foo.b
'bar'
>>> foo.c
[1, 2]
>>> 

Предоставлено Питером Норвигом Питон: нечасто отвеченные вопросы

6 голосов
/ 15 мая 2018

С Python 3.7 + вы можете использовать Класс данных , который достигает того, что вы хотите, и даже больше.

Позволяет вам определить поля для вашего класса, которым автоматически присваиваются атрибуты.

Это будет выглядеть примерно так:

@dataclass
class Foo:
    a: str
    b: int
    c: str
    ...

В вашем классе будет автоматически создан метод __init__, который назначит аргументы создания экземпляра этим атрибутам (и подтвердитаргументы).

Обратите внимание, что здесь требуется подсказка типа , поэтому в этом примере я использовал int и str.Если вы не знаете тип вашего поля, вы можете использовать Any из typing module .

2 голосов
/ 02 августа 2017

В этот пакет теперь можно найти

Обратите внимание, что это было проверено для python 3.5 +

2 голосов
/ 25 апреля 2017

Это простая реализация judy2k :

from inspect import signature

def auto_args(f):
    sig = signature(f)  # Get a signature object for the target:
    def replacement(self, *args, **kwargs):
        # Parse the provided arguments using the target's signature:
        bound_args = sig.bind(self, *args, **kwargs)
        # Save away the arguments on `self`:
        for k, v in bound_args.arguments.items():
            if k != 'self':
                setattr(self, k, v)
        # Call the actual constructor for anything else:
        f(self, *args, **kwargs)
    return replacement


class MyClass:
    @auto_args
    def __init__(self, a, b, c=None):
        pass

m = MyClass('A', 'B', 'C')
print(m.__dict__)
# {'a': 'A', 'b': 'B', 'c': 'C'}
2 голосов
/ 28 апреля 2015

Аналогично вышеописанному, но не то же самое ... следующее очень короткое, имеет дело с args и kwargs:

def autoassign(lcls):
    for key in lcls.keys():
        if key!="self":
            lcls["self"].__dict__[key]=lcls[key]

Используйте вот так:

class Test(object):
    def __init__(self, a, b, *args, **kwargs):
        autoassign(locals())
2 голосов
/ 06 сентября 2010

Если у вас много переменных, вы можете передать один единственный файл конфигурации или объект.

0 голосов
/ 06 сентября 2010
class MyClass(object):
    def __init__(self, **kwargs):
        for key, value in kwargs.iteritems():
            setattr(self, key, value)

Вы просто не можете использовать * args, но вы можете сохранить в некотором списке экземпляров (например, self.args, не знаю)

...