Python装饰器自动定义__init__变量

时间:2015-02-10 22:59:56

标签: python class constructor initialization decorator

我已经厌倦了在我的__init__功能中反复输入相同的重复命令。我想知道我是否可以写一个装饰师为我做这项工作。这是我的问题的一个例子:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

有没有什么方法可以让我自动将所有传递给函数的参数变成具有相同名称的实例变量?例如:

class Point:
    @instance_variables
    def __init__(self, x, y):
        pass

@instance_variables会自动设置self.x = xself.y = y。我怎么能这样做?
谢谢!

编辑:我应该提一下,我使用的是CPython 2.7。

7 个答案:

答案 0 :(得分:4)

这是我对装饰者的第一次尝试:

[编辑第二次尝试:我添加了变量处理默认值并检查有效关键字。谢谢ivan_pozdeev]

[编辑3:添加的默认检查不是无]

def instanceVariables(func):
    def returnFunc(*args, **kwargs):
        selfVar = args[0]

        argSpec = inspect.getargspec(func)
        argumentNames = argSpec[0][1:]
        defaults = argSpec[3]
        if defaults is not None:
            defaultArgDict = dict(zip(reversed(argumentNames), reversed(defaults)))
            selfVar.__dict__.update(defaultArgDict)

        argDict = dict(zip(argumentNames, args[1:]))
        selfVar.__dict__.update(argDict)


        validKeywords = set(kwargs) & set(argumentNames)
        kwargDict = {k: kwargs[k] for k in validKeywords}
        selfVar.__dict__.update(kwargDict)

        func(*args, **kwargs)

    return returnFunc

这是一个例子:

class Test():

    @instanceVariables
    def __init__(self, x, y=100, z=200):
        pass

    def printStr(self):
        print(self.x, self.y, self.z)

a = Test(1, z=2)

a.printStr()

>>> 1 100 2

答案 1 :(得分:1)

你可以这样做:

def __init__(self, x, y):
    self.__dict__.update(locals())
    del self.self   # redundant (and a circular reference)

但这可能不是一个真正的改进,可读性方面。

答案 2 :(得分:1)

我不同意这很有用。我发现强迫开发人员(包括我自己)输入成员变量启动的痛苦样板是一种阻止人们接受一些荒谬的参数的__init__方法的好方法,这些方法变成了荒谬的数量成员变量。

当有人想要通过使用控制自定义实例化的额外参数,功能标志和布尔开关变量来扩展类中可用的功能时,会发生很多事情。我认为所有这些都是缺乏方法来满足适应新的或可选的扩展复杂性的需要。

要求输入这种特殊类型的样板文件,就像对类膨胀一样征税。如果您发现自己在__init__中接受了如此多的args以至于需要此功能,那么通常您应该使用较小的分隔类(可能是MixIn设计)来重构您的设计。

尽管如此,这里有一种方法可以在没有装饰器误导的情况下完成。我没有尝试处理*args但是在这种特殊情况下你必须为未命名的位置参数意味着定义特殊逻辑。

def init_from_map(obj, map):
    for k,v in map.iteritems():
        if k not in ["self", "kwargs"]:
            setattr(obj, k, v)
        elif k == "kwargs":
            for kk, vv in v.iteritems():
                setattr(obj, kk, vv)

class Foo(object):
    def __init__(self, x, y, **kwargs):
        init_from_map(self, locals())

f = Foo(1, 2, z=3)
print f.x, f.y, f.z
print f.__dict__

打印:

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

答案 3 :(得分:0)

您可以使用反射来减少代码重复

self.__dict__.update(v,locals()[v] for v in 'x','y')

(或几乎等效(v不得为元变量名称))

for v in 'x','y': setattr(self,v,locals()[v])

或者使用CPython的实现细节来根据Getting method parameter names in python

从运行时检索参数名称
cur_fr = sys._getframe().f_code
self.__dict__.update(v,locals()[v] for v in cur_fr.co_varnames[1:cur_fr.co_argcount])  # cur_fr.f_locals is the same as locals()

第二种方法看起来更“自动化”,但as I've said,结果却相当不灵活。如果你的参数列表超过3-4,你可能只需要用这种方式处理一些参数,在这种情况下,你没有其他选择,只能手工构建它们的列表。

答案 4 :(得分:0)

对于Python 3.3 +:

from functools import wraps
from inspect import Parameter, signature


def instance_variables(f):
    sig = signature(f)
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        values = sig.bind(self, *args, **kwargs)
        for k, p in sig.parameters.items():
            if k != 'self':
                if k in values.arguments:
                    val = values.arguments[k]
                    if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
                        setattr(self, k, val)
                    elif p.kind == Parameter.VAR_KEYWORD:
                        for k, v in values.arguments[k].items():
                            setattr(self, k, v) 
                else:
                    setattr(self, k, p.default) 
    return wrapper

class Point(object):
    @instance_variables 
    def __init__(self, x, y, z=1, *, m='meh', **kwargs):
        pass

<强>演示:

>>> p = Point('foo', 'bar', r=100, u=200)
>>> p.x, p.y, p.z, p.m, p.r, p.u
('foo', 'bar', 1, 'meh', 100, 200)

使用框架的Python 2和3的非装饰器方法:

import inspect


def populate_self(self):
    frame = inspect.getouterframes(inspect.currentframe())[1][0]
    for k, v in frame.f_locals.items():
        if k != 'self':
            setattr(self, k, v)


class Point(object):
    def __init__(self, x, y):
        populate_self(self)

演示:

>>> p = Point('foo', 'bar')
>>> p.x
'foo'
>>> p.y
'bar'

答案 5 :(得分:0)

我正在寻找一个autoinit装饰器,并遇到了这个线程。 我在网络上找不到处理可变参数,可变关键字和仅关键字参数的@autoinit。受其他解决方案的启发,我编写了支持所有内容的自己的版本。

我做了一些测试,它似乎在所有情况下都可以正常工作,但是我没有详尽地测试代码。让我知道它是否有错误。谢谢。

def autoinit(func):
"""
This decorator function auto initialize class variables from __init__() arguments
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
    if func.__name__ != '__init__':
        return func(*args, **kwargs)

    self = args[0]
    func_spec = inspect.getfullargspec(func)

    # initialize default values
    nargs = dict()
    if func_spec.kwonlydefaults is not None:
        for k,v in func_spec.kwonlydefaults.items():
            nargs[k] = v
    if func_spec.defaults is not None:
        for k,v in zip(reversed(func_spec.args), reversed(func_spec.defaults)):
            nargs[k] = v
    if func_spec.varargs is not None:
        nargs[func_spec.varargs] = []
    if func_spec.varkw is not None:
        nargs[func_spec.varkw] = {}
    # fill in positional arguments
    for index, v in enumerate(args[1:]):
        if index+1 < len(func_spec.args):
            nargs[func_spec.args[index+1]] = v
        elif func_spec.varargs is not None:
            # variable argument
            nargs[func_spec.varargs].append(v)
    # fill in keyword arguments
    for k,v in kwargs.items():
        if k in itertools.chain(func_spec.args, func_spec.kwonlyargs):
            nargs[k] = v
        elif func_spec.varkw is not None:
            # variable keywords
            nargs[func_spec.varkw][k] = v

    # set values to instance attributes
    for k,v in nargs.items():
        setattr(self, k, v)
    return func(*args, **kwargs)
return wrapper

答案 6 :(得分:0)

对于那些可能会发现这篇文章但对 Python 3.7 + 解决方案感兴趣的人(由于Python 2 End of Life是2020年1月1日;-),您可以使用Python标准库dataclasses

from dataclasses import dataclass

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

除其他事项外,还将添加如下内容的__init__()

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand