【问题标题】:What is the best way to do automatic attribute assignment in Python, and is it a good idea?在 Python 中进行自动属性分配的最佳方法是什么,这是一个好主意吗?
【发布时间】:2011-04-08 20:19:08
【问题描述】:

而不是每次定义类时都编写这样的代码:

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

我可以使用this recipe for automatic attribute assignment

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

两个问题

  1. 是否存在与此快捷方式相关的缺点或陷阱?
  2. 有没有更好的方法来实现类似的便利?

【问题讨论】:

  • +1 这是 Python 中的一个循环模式。我没有看到使用自动分配装饰器有任何缺点,因为它允许您定义要分配的参数列表 not

标签: python attributes decorator


【解决方案1】:

Python 3.7+ 开始,您可以使用 Data Class,它可以实现您想要的以及更多。

它允许你为你的类定义字段,它们是自动分配的属性。

它看起来像这样:

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

__init__ 方法将在您的类中自动创建,并将实例创建的参数分配给这些属性(并验证参数)。

注意这里需要类型提示,这就是我在示例中使用intstr 的原因。如果您不知道您的字段类型,可以使用Any from the typing module

【讨论】:

【解决方案2】:

您现在可以在this package 中找到

请注意,这已针对 python 3.5+ 进行了验证

【讨论】:

    【解决方案3】:

    关于自动分配代码的一些问题让我感到困惑(主要是风格,但还有一个更严重的问题):

    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, lambdas 中的 lambdas, ifilters,还有很多条件。

      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。我不认为 mapstarmap 是用来打电话的 函数,其唯一目的是它们的 副作用。本来可以 用平凡写得更清楚:

      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。使用 autoassignautoargs 与 IPython 代码完成兼容。

    【讨论】:

    • 非常感谢。这很有帮助。
    • 我还发现您的 autoargs 非常方便。我注意到的一个问题是,如果defaultsNone,它似乎会引发错误,因为对reversed 的调用失败。我是否认为这应该可以通过在for attr,val in zip(reversed(attrs),reversed(defaults)): 行前面添加if defaults: 来解决,还是会产生意想不到的副作用?单元测试通过。
    • @m01:很抱歉这么晚才回复。谢谢指正!
    【解决方案4】:

    这是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'}
    

    【讨论】:

      【解决方案5】:

      和上面类似,虽然不一样……下面很短,处理argskwargs

      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())
      

      【讨论】:

        【解决方案6】:
        class MyClass(object):
            def __init__(self, **kwargs):
                for key, value in kwargs.iteritems():
                    setattr(self, key, value)
        

        你只是不能使用*args,但你可以存储在一些实例列表中(比如self.args,不知道)

        【讨论】:

          【解决方案7】:

          一个缺点:许多 IDE 解析 __init__.py 以发现对象的属性。如果您希望 IDE 中的自动代码完成功能更强大,那么您最好用老式的方式将其拼写出来。

          【讨论】:

            【解决方案8】:

            有没有更好的方法来实现类似的便利?

            我不知道它是否一定更好,但你可以这样做:

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

            感谢 Peter Norvig 的 Python: Infrequently Answered Questions

            【讨论】:

            • 这是一个快速解决问题的方法,但它有两个缺点:1)如果要确保将某些参数传递给函数,则必须添加更多代码; 2)函数的签名是完全不透明的,不能用来理解函数的调用方式;需要依赖文档字符串来代替
            【解决方案9】:

            如果你有很多变量,你可以传递一个配置字典或对象。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2012-03-28
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-02-19
              • 1970-01-01
              相关资源
              最近更新 更多