Python 装饰器自动定义 __init__ 变量

Python decorator to automatically define __init__ variables

我已经受够了在我的 __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。

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

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

(或几乎等效(v 不能是元变量名称))

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

或者根据 Getting method parameter names in python

使用 CPython 的实现细节从运行时检索参数名称
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()

第二种方法看起来更 "automated" 但是, 结果证明它相当不灵活。如果你的参数列表超过 3-4 个,你可能只需要这样处理一些参数,在这种情况下,你别无选择,只能手动构建它们的列表。

你可以这样做:

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

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

这是我第一次尝试装饰器:

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

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

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

我不同意这有用。我发现强迫开发人员(包括我自己)敲出成员变量启动的痛苦样板是阻止人们使用接受大量参数的 __init__ 方法的好方法,这些参数会变成大量的参数成员变量。

当有人想通过使用额外的参数、功能标志和控制自定义实例化的布尔开关变量来扩展 class 中可用的功能时,这种情况经常发生。我认为所有这些都不足以满足适应新的或可选的扩展复杂性的需要。

被要求输入这种特殊的样板文件就像是对 class 臃肿征税。如果您发现自己在 __init__ 中接受了如此多的参数以至于您需要此功能,这通常是一个很好的指标,表明您应该使用更小的、分隔的 classes 重构您的设计,也许是 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}

对于 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'

我正在寻找一个 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

对于那些可能找到此 post 但对 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