python 3.8 - 初始化时将属性动态设置为 class

python 3.8 - dynamically set properties to a class when it's initialized

我正在尝试创建一个 class 其实例可以具有任意数量的属性。使用 init 方法中的 **kwargs 可以很容易地实现这一点。然后,对于它的每个属性,我需要计算实例整个生命周期中的访问次数。

如果每个属性都有一个 属性,那么我可以在两个 getter[=53= 中增加一个计数器] 和 setter 在给定时间访问的属性的方法。

问题是我需要动态创建这些属性,因为我不知道 class 的实例将具有哪些属性。

我相信这可以通过 metaclasses 解决。虽然,我正试图在没有 的情况下 解决这个问题。

到目前为止我已经写了这段代码:

class CountAttr:

    def __init__(self, **kwargs):
        # here I init the counter for each keyword argument
        self.__count = {attr: 0 for attr in kwargs}
        # then I add the attribute to my instance
        for kwarg in kwargs:
            object.__setattr__(self, '_' + kwarg, kwargs[kwarg])
            def getter(self):
                self.__count[kwarg] +=1
                return object.__getattribute__(self, '_' + kwarg)
            def setter(self, value):
                self.__count[kwarg] +=1
                object.__setattr__(self, '_' + kwarg, kwargs[kwarg], value)
            #here comes the issue: the property doesn't work
            object.__setattr__(self, kwarg, property(getter, setter))

    @property
    def count(self):
        return self.__count

test = CountAttr(x=5, y=6)

print(test.x, test.y)

调试我的代码,我评估 test 具有值为 5 的属性 _x,以及值为 [=19] 的属性 _y =]. xy 显示为 property objects,因此似乎一切正常。但它并没有结束。

的确,最后的打印语句会输出这个:

<property object at 0x7fbb241d4b30> <property object at 0x7fbb241d4b80>

虽然我期望的输出是:5 6,即 _x_y 属性的值。

正如@Wups 指出的那样,设置 class 属性而不是实例属性有助于解决这种情况。 Here 他们展示了一个示例,如何使用 属性() 函数设置封装属性。 value 是一个 class 属性,其值是 属性() returns。因此,在您的循环中,您可以创建此 class 属性传递 属性() 并使用适当的 setter 和 getter 作为值。我也不会在循环中为每个 kwarg 重复声明 setters 和 getter,因为你可以声明它们一次,但由于每个 kwarg 的名称不同,你可以将它们封装在外部函数中,它将此名称作为参数,然后 return 一个具有属性集专有名称的函数。

这个例子似乎在工作,正如你所希望的那样:

class SomeClass:
    def __init__(self, **kwargs):
        for item in kwargs:
            setattr(SomeClass, item, property(self.getter(item),
                self.setter(item, kwargs[item])))

            setattr(self, '__'+item, kwargs[item])

    def setter(self, name: str, value):
        def inner_setter(self, value):
            setattr(self, '__'+name, value)
        return inner_setter

    def getter(self, name: str):
        def inner_getter(self):
            return getattr(self, '__'+name)
        return inner_getter

inst = SomeClass(r=2, s=4)

print(inst.r, inst.s)

输出:

2 4

如果你愿意使用全局变量,你可以使用全局字典:

_count = {}

class CountAttr:
    
    def __init__(self, **kw):
        for i, j in kw.items():
            object.__setattr__(self, i, j)
        global _count
        _count = {i: 0 for i in kw.keys()}
    
    def __getattribute__(self, key):
        global _count
        if key in _count:
            _count[key] += 1
        return object.__getattribute__(self, key)
    
    def __setattr__(self, key, val):
        global _count
        _count[key] += 1
        return object.__setattr__(self, key, val)
    
    @property
    def count(self):
        return _count  # expose count dict interface

用法:

>>> test = CountAttr(x=5, y=6)
>>> test.x
5
>>> test.y
6
>>> test.count['x']
1
>>> test.count['y']
1

按照@jsbueno 的回答,您可以将 count 设置为实例,只要您在 __init__:

中小心排序即可
class CountAttr:
    
    def __init__(self, **kw):
        self.count = {i: 0 for i in kw.keys()}  # If <- isn't first 
        for i, j in kw.items():                 # you will get an 
            setattr(self, i, j)                 # `AttributeError`.
        super().__init__()
    
    def __getattribute__(self, key):
        if key != 'count':
            counter = self.count 
            counter[key] += 1
        return super().__getattribute__(key)
    
    def __setattr__(self, key, val):
        if key != 'count':
            counter = self.count 
            counter[key] += 1
        return super().__setattr__(key, val

Python 中对象中的所有属性访问,无论属性实际如何存储在内存中(或者即使它是计算出来的),都通过 class 中的两个特殊方法 __setattr____getattribute__.

就是这样 - 只需将你的计数器代码放在这两个方法中,使用单独的名称作为实例属性来存储计数器数据本身,然后继续 - 无需摆弄元class。您只需要一个带有此计数器代码的基础(或混合)class。如果您不想对参数的初始设置进行计数,则可以从“-1”开始计数以进行写入,例如。

class CounterBase:
    def __init__(self, **kwargs):
        self._counters = {}
        for name, value in kwargs.items():
            setattr(self, name, value)
        super().__init__()
        
    def __getattribute__(self, name):
        if name != "_counters":
            _counters = self._counters
            _counters[name] = _counters.get(name, 0) + 1
        return super().__getattribute__(name)
        
    def __setattr__(self, name, value):
        if name != "_counters":
            _counters = self._counters
            _counters[name] = _counters.get(name, 0) + 1
        return super().__setattr__(name, value)