What is the best way to do automatic attribute assignment in Python, and is it a good idea?

前端 未结 10 1380
深忆病人
深忆病人 2020-11-30 00:00

Instead of writing code like this every time I define a class:

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


        
相关标签:
10条回答
  • 2020-11-30 00:16

    One drawback: many IDEs parse __init__.py to discover an object's attributes. If you want automatic code completion in your IDE to be more functional, then you may be better off spelling it out the old-fashioned way.

    0 讨论(0)
  • 2020-11-30 00:21

    On many occasions i did actually need this functionality, so i decided to implement my, My solution may not be the best fit for every situation, but it has some cool features that makes it performs seamlessly with inheritance and default parameters, the usage is very simple:

    1. Inherit from EasyObj.
    2. Define your parameters by setting EasyObj_KWARGS.
    3. your __init__ signature must be __init__(self, *args, **kwargs).
    4. call super from __init__ as super().__init__(*args, **kwargs)

    Example :

    class A(EasyObj):
       EasyObj_KWARGS  = OrderedDict((
                ('name'     , {'default': 'Sal' , 'adapter': lambda x: 'My name is '+x  }),
                ('age'      , {'default : 20    }                                        ),
                ('degree'   , {}                                                         ),
                ('degree'   , {'adapter': lambda x: x.strip()}                           )))
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
    

    The full code is below and is pretty documented, inheritance is also supported

    from    collections     import  OrderedDict
    from    enum            import  Enum
    from    inspect         import  getmro
    
    class   InfoExceptionType(Enum):
        PROVIDED_TWICE  = 1
        MISSING         = 2
        EXTRA           = 3
    
    class   ExceptionKwargs(Exception):
        '''
            Raised on kwargs setting errors by EasyObj.
            Args    :
                kwargs  (list)                  : List of kwargs.
                error   (InfoExceptionType)     : Error to print to the user
        '''
    
        def __init__(
            self        , 
            kwargs      ,
            error       ,
            all_kwargs  ):
            self.kwargs     = kwargs
            self.error      = error
            self.all_kwargs = '\nPossible kwargs:\n\t'+ '\n\t'.join(
                [ '{}{}'.format(x, (': '+ str(all_kwargs[x]['default']) if 'default' in all_kwargs[x] else ''))
                    for x in all_kwargs])
    
        def __str__(self):
            return 'The following kwargs/args were {}: {}'.format(
                self.error.name.lower().replace('_',' ')    ,
                ', '.join(self.kwargs)                      )+ self.all_kwargs
    
        def __repr__(self):
            return str(self)
    
    class   EasyObj():
        '''
            Allows automatic attribute setting from within __init__.
            All derived classes must call super with the provided kwargs 
                when implementing __init__ :
                super().__init__(**kwargs)
            EasyObj_KWARGS dict must be overridden.
            If args are supplied to init, they will be assigned automatically 
                using the order specified in EasyObj_KWARGS.
            Kwarg dict keys are the name of the kwargs, values are dict 
                containing a default value and an adapter, both are optional.
            If no value was given to a kwarg, default value is used, if no default value
                is found, ExceptionKwargs is raised.
            Adapters are applied to parameters even to default values.
    
            Support for kwargs inheritance:
                If a class B is derived from A and both A and B are EasyObj then 
                    B.EasyObj_KWARGS will be A.EasyObj_KWARGS + B.EasyObj_KWARGS
                In this case, the EasyObj_KWARGS order will be dependent on the order of 
                types returned by inspect.getmro in reverse.
    
            Examples:
                >>> class A(EasyObj):
                        EasyObj_KWARGS  = OrderedDict((
                            ('name'     , {'default': 'Sal' , 'adapter': lambda x: 'My name is '+x  }),
                            ('age'      , {'default': 20    }                                        ),
                            ('degree'   , {}                                                         ),
                            ('degree'   , {'adapter': lambda x: x.strip()}                           )))
                        def __init__(self, *args, **kwargs):
                            super().__init__(*args, **kwargs)
    
                >>> #Class be doesn't have to implement __init__ since A already does that
                >>> class B(A):
                        EasyObj_KWARGS  = OrderedDict((
                            ('male' , {'default': True  }   ),))
    
                >>> A(degree= ' bachelor ').__dict__
                >>> {'degree': 'bachelor', 'name': 'My name is Sal', 'age': 20}
                >>> B(degree= ' bachelor ').__dict__
                >>> {'degree': 'bachelor', 'name': 'My name is Sal', 'age': 20, 'male': True}
        '''
        #Contains kwargs and validators for creating the object, must be overridden
        #Must be an ordered dict.
        EasyObj_KWARGS  = OrderedDict()
    
        def __init__(self, *args, **kwargs):
            all_kwargs = OrderedDict()
            for _type in reversed(getmro(type(self))):
                if hasattr(_type, 'EasyObj_KWARGS'):
                    all_kwargs.update(_type.EasyObj_KWARGS)
    
            if len(args) > len(all_kwargs):
                extra_args = ['Arg at postition '+ str(i+1) for i in range(len(all_kwargs), len(args))]
                raise ExceptionKwargs(extra_args, InfoExceptionType.EXTRA, all_kwargs)
    
            args_kwargs     = {
                list(all_kwargs.keys())[i] : args[i] for i in range(len(args))}
            twice_kwargs    = [kwarg for kwarg in kwargs if kwarg in args_kwargs]
    
            if twice_kwargs:
                raise ExceptionKwargs(twice_kwargs, InfoExceptionType.PROVIDED_TWICE, all_kwargs)
    
            kwargs.update(args_kwargs)
            default_kwargs = {
                x:all_kwargs[x]['default'] for x in all_kwargs \
                    if 'default' in all_kwargs[x] and x not in kwargs}
            kwargs.update(default_kwargs)
    
            extra_kwargs    = [k for k in kwargs if k not in all_kwargs] 
            if extra_kwargs     :
                raise ExceptionKwargs(extra_kwargs, InfoExceptionType.EXTRA, all_kwargs)
    
            missing_kwargs  = [k for k in all_kwargs if k not in kwargs] 
            if missing_kwargs   :
                raise ExceptionKwargs(missing_kwargs, InfoExceptionType.MISSING, all_kwargs)
    
            for k in kwargs :
                if 'adapter' in all_kwargs[k]:
                    setattr(self, k, all_kwargs[k]['adapter'](kwargs[k]))
                else :
                    setattr(self, k, kwargs[k])
    
    0 讨论(0)
  • 2020-11-30 00:23

    Is there a better way to achieve similar convenience?

    I don't know if it is necessarily better, but you could do this:

    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]
    >>> 
    

    Courtesy Peter Norvig's Python: Infrequently Answered Questions.

    0 讨论(0)
  • 2020-11-30 00:25

    There are some things about the autoassign code that bug me (mostly stylistic, but one more serious problem):

    1. autoassign does not assign an 'args' attribute:

      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 acts like a decorator. But autoassign(*argnames) calls a function which returns a decorator. To achieve this magic, autoassign needs to test the type of its first argument. If given a choice, I prefer functions not test the type of its arguments.

    3. There seems to be a considerable amount of code devoted to setting up sieve, lambdas within lambdas, ifilters, and lots of conditions.

      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)
      

      I think there might be a simpler way. (See below).

    4. for _ in itertools.starmap(assigned.setdefault, defaults): pass. I don't think map or starmap was meant to call functions, whose only purpose is their side effects. It could have been written more clearly with the mundane:

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

    Here is an alternative simpler implementation which has the same functionality as autoassign (e.g. can do includes and excludes), and which addresses the above points:

    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
    

    And here is the unit test I used to check its behavior:

    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. Using autoassign or autoargs is compatible with IPython code completion.

    0 讨论(0)
  • 2020-11-30 00:29
    class MyClass(object):
        def __init__(self, **kwargs):
            for key, value in kwargs.iteritems():
                setattr(self, key, value)
    

    You just can't use *args, but you can store in some instance list (like self.args, don't know)

    0 讨论(0)
  • 2020-11-30 00:30

    This a simple implementation by 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'}
    
    0 讨论(0)
提交回复
热议问题