How do I avoid the “self.x = x; self.y = y; self.z = z” pattern in __init__?

后端 未结 11 1471
醉梦人生
醉梦人生 2020-12-12 11:16

I see patterns like

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

quite frequently, often with

相关标签:
11条回答
  • 2020-12-12 11:36

    Edit: If you have python 3.7+ just use dataclasses

    A decorator solution that keeps the signature:

    import decorator
    import inspect
    import sys
    
    
    @decorator.decorator
    def simple_init(func, self, *args, **kws):
        """
        @simple_init
        def __init__(self,a,b,...,z)
            dosomething()
    
        behaves like
    
        def __init__(self,a,b,...,z)
            self.a = a
            self.b = b
            ...
            self.z = z
            dosomething()
        """
    
        #init_argumentnames_without_self = ['a','b',...,'z']
        if sys.version_info.major == 2:
            init_argumentnames_without_self = inspect.getargspec(func).args[1:]
        else:
            init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]
    
        positional_values = args
        keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
        attribute_values = positional_values + keyword_values_in_correct_order
    
        for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
            setattr(self,attribute_name,attribute_value)
    
        # call the original __init__
        func(self, *args, **kws)
    
    
    class Test():
        @simple_init
        def __init__(self,a,b,c,d=4):
            print(self.a,self.b,self.c,self.d)
    
    #prints 1 3 2 4
    t = Test(1,c=2,b=3)
    #keeps signature
    #prints ['self', 'a', 'b', 'c', 'd']
    if sys.version_info.major == 2:
        print(inspect.getargspec(Test.__init__).args)
    else:
        print(inspect.signature(Test.__init__))
    
    0 讨论(0)
  • 2020-12-12 11:39

    As others have mentioned, the repetition isn't bad, but in some cases a namedtuple can be a great fit for this type of issue. This avoids using locals() or kwargs, which are usually a bad idea.

    from collections import namedtuple
    # declare a new object type with three properties; x y z
    # the first arg of namedtuple is a typename
    # the second arg is comma-separated or space-separated property names
    XYZ = namedtuple("XYZ", "x, y, z")
    
    # create an object of type XYZ. properties are in order
    abc = XYZ("one", "two", 3)
    print abc.x
    print abc.y
    print abc.z
    

    I've found limited use for it, but you can inherit a namedtuple as with any other object (example continued):

    class MySuperXYZ(XYZ):
        """ I add a helper function which returns the original properties """
        def properties(self):
            return self.x, self.y, self.z
    
    abc2 = MySuperXYZ(4, "five", "six")
    print abc2.x
    print abc2.y
    print abc2.z
    print abc2.properties()
    
    0 讨论(0)
  • 2020-12-12 11:40

    Disclaimer: It seems that several people are concerned about presenting this solution, so I will provide a very clear disclaimer. You should not use this solution. I only provide it as information, so you know that the language is capable of this. The rest of the answer is just showing language capabilities, not endorsing using them in this way.


    There isn't really anything wrong with explicitly copying parameters into attributes. If you have too many parameters in the ctor, it is sometimes considered a code smell and maybe you should group these params into a fewer objects. Other times, it is necessary and there is nothing wrong with it. Anyway, doing it explicitly is the way to go.

    However, since you are asking HOW it can be done (and not whether it should be done), then one solution is this:

    class A:
        def __init__(self, **kwargs):
            for key in kwargs:
              setattr(self, key, kwargs[key])
    
    a = A(l=1, d=2)
    a.l # will return 1
    a.d # will return 2
    
    0 讨论(0)
  • 2020-12-12 11:43

    This is a solution without any additional imports.

    Helper function

    A small helper function makes it more convenient and re-usable:

    def auto_init(local_name_space):
        """Set instance attributes from arguments.
        """
        self = local_name_space.pop('self')
        for name, value in local_name_space.items():
            setattr(self, name, value)
    

    Application

    You need to call it with locals():

    class A:
        def __init__(self, x, y, z):
            auto_init(locals())
    

    Test

    a = A(1, 2, 3)
    print(a.__dict__)
    

    Output:

    {'y': 2, 'z': 3, 'x': 1}
    

    Without changing locals()

    If you don't like to change locals() use this version:

    def auto_init(local_name_space):
        """Set instance attributes from arguments.
        """
        for name, value in local_name_space.items():
            if name != 'self': 
                setattr(local_name_space['self'], name, value)
    
    0 讨论(0)
  • 2020-12-12 11:43

    My 0.02$. It is very close to Joran Beasley answer, but more elegant:

    def __init__(self, a, b, c, d, e, f):
        vars(self).update((k, v) for k, v in locals().items() if v is not self)
    

    Additionally, Mike Müller's answer (the best one to my taste) can be reduced with this technique:

    def auto_init(ns):
        self = ns.pop('self')
        vars(self).update(ns)
    

    And the just call auto_init(locals()) from your __init__

    0 讨论(0)
提交回复
热议问题