我如何取消列出' dict'的子类。用python3中的__setitem__验证?

发布于 2025-02-04 04:04:37 字数 1622 浏览 4 评论 0 原文

我正在使用Python3.3。在2.x的Pickle协议中,这个问题可能不存在,但我实际上没有得到验证。

假设我已经创建了一个 dict 子类,该子类每次更新键时都计算。类似的内容:

class Foo(dict):
    def __init__(self):
        self.counter = 0

    def __setitem__(self, key, value):
        print(key, value, self.__dict__)
        if key == 'bar':
            self.counter += 1
        super(Foo, self).__setitem__(key, value)

您可能会这样使用:

>>> f = Foo()
>>> assert f.counter == 0
>>> f['bar'] = 'baz'
... logging output...        
>>> assert f.counter == 1

现在让我们腌制并取消挑剔:

>>> import pickle
>>> f_str = pickle.dumps(f)
>>> f_new = pickle.loads(f_str)
bar baz {}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 133, in __setitem__
    self.counter += 1
AttributeError: 'Foo' object has no attribute 'counter'

我认为 print() in __ setItem __ __ 显示问题: pickle.loads 试图在之前编写字典的键 它写入对象的属性...至少我认为这是正在发生的事情。如果您删除 self.counter 参考 foo .__ setItem __()

>>> f_mod = ModifiedFoo()
>>> f_mod['bar'] = 'baz'
>>> f_mod_str = pickle.dumps(f_mod)
>>> f_mod_new = pickle.loads(f_mod_str)
bar baz {}
>>> assert f_mod_new.counter == 0
>>>

这只是Pickle协议的副产品吗?我已经尝试在 __ setState上进行变体__ 让它正确地取消选择,但据我所知,它触发 __ setItem __ error, __ setState __ setstate __ is甚至叫。有什么办法可以修改此对象以允许取消选择?

I'm using python3.3. It's possible this problem doesn't exist in 2.x's pickle protocol, but I haven't actually verified.

Suppose I've created a dict subclass that counts every time a key is updated. Something like this:

class Foo(dict):
    def __init__(self):
        self.counter = 0

    def __setitem__(self, key, value):
        print(key, value, self.__dict__)
        if key == 'bar':
            self.counter += 1
        super(Foo, self).__setitem__(key, value)

You might use it like this:

>>> f = Foo()
>>> assert f.counter == 0
>>> f['bar'] = 'baz'
... logging output...        
>>> assert f.counter == 1

Now let's pickle and unpickle it:

>>> import pickle
>>> f_str = pickle.dumps(f)
>>> f_new = pickle.loads(f_str)
bar baz {}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 133, in __setitem__
    self.counter += 1
AttributeError: 'Foo' object has no attribute 'counter'

I think the print() in __setitem__ shows the problem: pickle.loads attempts to write the dictionary's keys before it writes the object's attributes... at least I think that's what's happening. It's pretty easy to verify if you remove the self.counter reference in Foo.__setitem__():

>>> f_mod = ModifiedFoo()
>>> f_mod['bar'] = 'baz'
>>> f_mod_str = pickle.dumps(f_mod)
>>> f_mod_new = pickle.loads(f_mod_str)
bar baz {}
>>> assert f_mod_new.counter == 0
>>>

Is this just a byproduct of the pickle protocol? I've tried variations on __setstate__ to let it unpickle correctly, but as far as I can tell, it hits the __setitem__ error before __setstate__ is even called. Is there any way I can modify this object to allow unpickling?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

少年亿悲伤 2025-02-11 04:04:37

pickle 文档:

当未挑选腌制的类实例时,其 __ init __()方法是
通常未调用。


在您的情况下,您 do 想调用 __ INIT __ 。但是,由于您的课程是新样式的类,因此您不能使用 __ getinitargs __ (无论如何,这在Python3中不支持)。您可以尝试编写自定义 __ getState __ __ setState __ 方法:

class Foo(dict):
    def __init__(self):
        self.counter = 0
    def __getstate__(self):
        return (self.counter, dict(self))
    def __setstate__(self, state):
        self.counter, data = state
        self.update(data)  # will *not* call __setitem__

    def __setitem__(self, key, value):
        self.counter += 1
        super(Foo, self).__setitem__(key, value)

但是,此仍然不起作用,因为因为您是在子分类 dict, dict 有一个用于腌制的特殊处理程序, __ getState __ 方法 it nes nath in ,但是 __ setState __ 方法不是

您可以围绕此定义 __降低__ 方法:

class Foo(dict):
    def __init__(self):
        self.counter = 0
    def __getstate__(self):
        return (self.counter, dict(self))
    def __setstate__(self, state):
        self.counter, data = state
        self.update(data)
    def __reduce__(self):
        return (Foo, (), self.__getstate__())

    def __setitem__(self, key, value):
        self.counter += 1
        super(Foo, self).__setitem__(key, value)

As stated by pickle documentation:

When a pickled class instance is unpickled, its __init__() method is
normally not invoked.

In your case you do want to invoke __init__. However since your class is a new-style class you cannot use __getinitargs__ (which isn't supported in python3 anyway). You could try to write your custom __getstate__ and __setstate__ methods:

class Foo(dict):
    def __init__(self):
        self.counter = 0
    def __getstate__(self):
        return (self.counter, dict(self))
    def __setstate__(self, state):
        self.counter, data = state
        self.update(data)  # will *not* call __setitem__

    def __setitem__(self, key, value):
        self.counter += 1
        super(Foo, self).__setitem__(key, value)

However this still doesn't work, because since you are subclassing dict and dict has a special handler for pickling, the __getstate__ method is called, however the __setstate__ method is not.

You can work around this defining the __reduce__ method:

class Foo(dict):
    def __init__(self):
        self.counter = 0
    def __getstate__(self):
        return (self.counter, dict(self))
    def __setstate__(self, state):
        self.counter, data = state
        self.update(data)
    def __reduce__(self):
        return (Foo, (), self.__getstate__())

    def __setitem__(self, key, value):
        self.counter += 1
        super(Foo, self).__setitem__(key, value)
橘虞初梦 2025-02-11 04:04:37

您正在子类 dict ,并且Pickle协议将使用专用 dict 处理程序将键和值存储在生成的泡菜数据中,并使用A 不同的一组opcodes,再次将其还原到您的对象。

结果, __ setState __ 仅在还原字典密钥后才被称为,并且状态仅包含 counter 属性。

这里有两个工作:

  1. 面对 __ INIT __ INIT __ 未被调用:

    ,使您的计数器代码有弹性。

     类foo(dict):
        计数器= 0
    
        def __setitem __(自我,钥匙,值):
            打印(钥匙,价值,自我.__ dict __)
            如果key =='bar':
                self.counter += 1
            super(foo,self).__ setItem __(键,值)
     

    此处

    计数器是类属性,因此始终存在。您也可以使用:

      self.counter = getAttr(self,'counter',0) + 1
     

    确保缺少属性有默认值。

  2. 提供 __ Newargs __ 方法;它可以返回一个空的元组,但指定它可以确保 __新__ 在未卖时调用,又可以调用 __ INT __ INT __

     类foo(dict):
        def __new __(cls, *args,** kw):
            f = super().__新__(cls, *args,** kw)
            f .__ Init __()
            返回f
    
        def __init __(自我):
            self.counter = 0
    
        def __setitem __(自我,钥匙,值):
            打印(钥匙,价值,自我.__ dict __)
            如果key =='bar':
                self.counter += 1
            super(foo,self).__ setItem __(键,值)
    
        def __getNewargs __(自我):
            #致电__new__(以及__Init__)
            返回 ()
     

    请注意,在调用 __ INT __ 之后,Unpickler仍然会设置所有键,然后 Restore __ dict __ dict __ self.counter 最终会反映正确的值。

演示:

第一方法:

>>> import pickle
>>> class Foo(dict):
...     counter = 0
...     def __setitem__(self, key, value):
...         print(key, value, self.__dict__)
...         if key == 'bar':
...             self.counter += 1
...         super(Foo, self).__setitem__(key, value)
... 
>>> f = Foo()
>>> f['bar'] = 'baz'
bar baz {}
>>> f.counter
1
>>> f['bar'] = 'foo'
bar foo {'counter': 1}
>>> f.counter
2
>>> f_str = pickle.dumps(f)
>>> new_f = pickle.loads(f_str)
bar foo {}
>>> new_f.counter
2
>>> new_f.items()
dict_items([('bar', 'foo')])

第二种方法:

>>> import pickle
>>> class Foo(dict):
...     def __new__(cls, *args, **kw):
...         f = super().__new__(cls, *args, **kw)
...         f.__init__()
...         return f
...     def __init__(self):
...         self.counter = 0
...     def __setitem__(self, key, value):
...         print(key, value, self.__dict__)
...         if key == 'bar':
...             self.counter += 1
...         super(Foo, self).__setitem__(key, value)
...     def __getnewargs__(self):
...         return ()
... 

>>> f = Foo()
>>> f['bar'] = 'baz'
bar baz {'counter': 0}
>>> f.counter
1
>>> f['bar'] = 'foo'
bar foo {'counter': 1}
>>> f.counter
2
>>> f_str = pickle.dumps(f)
>>> new_f = pickle.loads(f_str)
bar foo {}
>>> new_f.counter
2
>>> new_f.items()
dict_items([('bar', 'foo')])

You are subclassing dict, and the pickle protocol will use the dedicated dict handler to store the keys and values in the resulting pickle data, using a different set of opcodes to restore these to your object again.

As a result, __setstate__ is going only going to be called after restoring the dictionary keys, and the state contains only the counter attribute.

There are two work-arounds here:

  1. Make your counter code resilient in the face of __init__ not being called:

    class Foo(dict):
        counter = 0
    
        def __setitem__(self, key, value):
            print(key, value, self.__dict__)
            if key == 'bar':
                self.counter += 1
            super(Foo, self).__setitem__(key, value)
    

    Here counter is a class attribute and thus always present. You could also use:

    self.counter = getattr(self, 'counter', 0) + 1
    

    to ensure there is a default value for the missing attribute.

  2. Provide a __newargs__ method; it can return an empty tuple, but specifying it ensures that __new__ is called when unpickling, which in turn could call __init__:

    class Foo(dict):
        def __new__(cls, *args, **kw):
            f = super().__new__(cls, *args, **kw)
            f.__init__()
            return f
    
        def __init__(self):
            self.counter = 0
    
        def __setitem__(self, key, value):
            print(key, value, self.__dict__)
            if key == 'bar':
                self.counter += 1
            super(Foo, self).__setitem__(key, value)
    
        def __getnewargs__(self):
            # Call __new__ (and thus __init__) on unpickling.
            return ()
    

    Note that after __init__ is called, the unpickler still will set all the keys, then restore __dict__. self.counter will reflect the correct value in the end.

Demos:

1st approach:

>>> import pickle
>>> class Foo(dict):
...     counter = 0
...     def __setitem__(self, key, value):
...         print(key, value, self.__dict__)
...         if key == 'bar':
...             self.counter += 1
...         super(Foo, self).__setitem__(key, value)
... 
>>> f = Foo()
>>> f['bar'] = 'baz'
bar baz {}
>>> f.counter
1
>>> f['bar'] = 'foo'
bar foo {'counter': 1}
>>> f.counter
2
>>> f_str = pickle.dumps(f)
>>> new_f = pickle.loads(f_str)
bar foo {}
>>> new_f.counter
2
>>> new_f.items()
dict_items([('bar', 'foo')])

2nd approach:

>>> import pickle
>>> class Foo(dict):
...     def __new__(cls, *args, **kw):
...         f = super().__new__(cls, *args, **kw)
...         f.__init__()
...         return f
...     def __init__(self):
...         self.counter = 0
...     def __setitem__(self, key, value):
...         print(key, value, self.__dict__)
...         if key == 'bar':
...             self.counter += 1
...         super(Foo, self).__setitem__(key, value)
...     def __getnewargs__(self):
...         return ()
... 

>>> f = Foo()
>>> f['bar'] = 'baz'
bar baz {'counter': 0}
>>> f.counter
1
>>> f['bar'] = 'foo'
bar foo {'counter': 1}
>>> f.counter
2
>>> f_str = pickle.dumps(f)
>>> new_f = pickle.loads(f_str)
bar foo {}
>>> new_f.counter
2
>>> new_f.items()
dict_items([('bar', 'foo')])
回眸一遍 2025-02-11 04:04:37

您可以通过添加a __降低__() 方法将用于获取参数以传递给用户定义的函数,以在对象未进行挑选时重新构造该函数。

虽然,由于您的班级是 dict 子类,但并不像我最初想象的那样琐碎的实现,但是一旦我弄清楚需要做什么,这很简单。这是我想到的 - 请注意, _foo_unpickle_helper()函数不能是类的常规或静态方法,因此这就是为什么在模块级别定义的原因:

class Foo(dict):
    def __init__(self):
        self.counter = 0

    def __setitem__(self, key, value):
        print(key, value, self.__dict__)
        if key == 'bar':
            self.counter += 1
        super(Foo, self).__setitem__(key, value)

    def __reduce__(self):
        return _Foo_unpickle_helper, (self.counter, iter(self.items()))

def _Foo_unpickle_helper(counter, items):
    """ Reconstitute a Foo instance from the arguments. """
    foo = Foo()
    foo.counter = counter
    foo.update(items)  # apparently doesn't call __setitem__()...
    return foo

f = Foo()
f['bar'] = 'baz'
f['bar'] = 'baz'
print('f: {}'.format(f))
print('f.counter: {}'.format(f.counter))

import pickle
f_str = pickle.dumps(f)
print('----------')
f_new = pickle.loads(f_str)
print('f_new: {}'.format(f_new))
print('f_new.counter: {}'.format(f_new.counter))

输出:输出:

bar baz {'counter': 0}
bar baz {'counter': 1}
f: {'bar': 'baz'}
f.counter: 2
----------
f_new: {'bar': 'baz'}
f_new.counter: 2

You can add pickle support to your dictionary subclass by adding a __reduce__() method which will be used to get arguments to pass to a user defined function to reconstitute the object when it's unpickled.

Although, since your class is adictsubclass, not wasn't quite as trivial to implement as I originally thought, but it's fairly simple once I figured out what needed to be done. Here's what I came up with — note that the _Foo_unpickle_helper() function can't be a regular or static method of the class, so that's why it's defined at the module level:

class Foo(dict):
    def __init__(self):
        self.counter = 0

    def __setitem__(self, key, value):
        print(key, value, self.__dict__)
        if key == 'bar':
            self.counter += 1
        super(Foo, self).__setitem__(key, value)

    def __reduce__(self):
        return _Foo_unpickle_helper, (self.counter, iter(self.items()))

def _Foo_unpickle_helper(counter, items):
    """ Reconstitute a Foo instance from the arguments. """
    foo = Foo()
    foo.counter = counter
    foo.update(items)  # apparently doesn't call __setitem__()...
    return foo

f = Foo()
f['bar'] = 'baz'
f['bar'] = 'baz'
print('f: {}'.format(f))
print('f.counter: {}'.format(f.counter))

import pickle
f_str = pickle.dumps(f)
print('----------')
f_new = pickle.loads(f_str)
print('f_new: {}'.format(f_new))
print('f_new.counter: {}'.format(f_new.counter))

Output:

bar baz {'counter': 0}
bar baz {'counter': 1}
f: {'bar': 'baz'}
f.counter: 2
----------
f_new: {'bar': 'baz'}
f_new.counter: 2
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文