在Python中自动属性赋值的最佳方法是什么?这样做是否合适?
每次我定义一个类的时候,不想总是写这样的代码:
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
有两个问题:
- 这个快捷方式有没有什么缺点或者陷阱?
- 有没有更好的方法来实现类似的方便?
9 个回答
有没有更好的方法来实现类似的方便性呢?
我不知道这是否一定更好,但你可以这样做:
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]
>>>
感谢彼得·诺维格的《Python:不常见的问题》。
从Python 3.7+开始,你可以使用数据类,这能帮你实现你想要的功能,还能做更多事情。
它允许你为你的类定义字段,这些字段就是类的属性,会自动被赋值。
看起来大概是这样的:
@dataclass
class Foo:
a: str
b: int
c: str
...
在你的类里,__init__
方法会自动生成,它会把创建实例时传入的参数赋值给这些属性(并且会验证这些参数)。
需要注意的是,这里需要类型提示,所以我在例子中用了int
和str
。如果你不知道字段的类型,可以使用来自typing
模块的Any。
关于这个自动分配代码,有一些地方让我觉得不太舒服(大部分是风格上的问题,但有一个比较严重的问题):
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'
autoassign
的行为像一个装饰器。但autoassign(*argnames)
实际上是调用一个函数,这个函数返回一个装饰器。为了实现这个效果,autoassign
需要检查它第一个参数的类型。如果可以选择,我更喜欢函数不去检查参数的类型。似乎有很多代码是用来设置
sieve
的,还有嵌套的lambda表达式、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)
我觉得可能有更简单的方法。(见下面的内容)。
for _ in itertools.starmap(assigned.setdefault, defaults): pass
。我觉得map
或starmap
并不是用来调用那些仅仅为了副作用而存在的函数的。可以用更简单明了的方式来写:for key,value in defaults.iteritems(): assigned.setdefault(key,value)
这里有一个更简单的替代实现,它和autoassign
有相同的功能(比如可以做包含和排除),并且解决了上面提到的问题:
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的代码补全是兼容的。