I hadn't seen the original message (perhaps it fell through the GMane
migration).  I think this would indeed be a worthwhile addition to
pickle.

Is it possible to continue this discussion on python-dev?  I don't
think this is a PEP-level topic.

Regards

Antoine.


On Fri, 7 Feb 2020 12:48:48 -0500
Daniel Spitz <spitz.da...@gmail.com>
wrote:
> I just want to say, this would be extremely powerful.
> 
> Awhile ago I implemented a demo that wraps the pickle extension machinery
> to enable "object graph transformers":
> https://gist.github.com/spitz-dan-l/3375a61fac6fe150574fac791567af4f (demo
> usage starts at line 307). Effectively it lets you match-and-transform
> arbitrary python object graphs that can include cycles and deep nesting,
> without writing any traversal logic yourself. In the demo I used it to
> implement a simple arithmetic expression parser/evaluator, that lets you go
> from strings like "3 + (4 + (5) + 7)" to the int 19.
> 
> But, it uses pickle under the hood, because pickle is what knows how to
> traverse object graphs with cycles, and pickle is what knows how to use the
> extensible __reduce__ machinery. And this makes the entire demo silly and
> really slow. (And, that was kind of the point, to do something interesting
> using esoteric, inappropriate corners of python.)
> 
> If Andrew's proposal happened, pickle would be uncoupled from that core
> machinery, and my idea, as well as other interesting ideas, could be
> implemented in a non-silly way.
> 
> Daniel Spitz
> 
> On Thu, Jan 23, 2020 at 7:11 PM Andrew Barnert via Python-ideas <
> python-ideas@python.org> wrote:
> 
> > Pickling uses an extensible protocol that lets any class determine how its
> > instances can be deconstructed and reconstructed. Both `pickle` and `copy`
> > use this protocol, but it could be useful more generally. Unfortunately, to
> > use it more generally requires relying on undocumented details. I think we
> > should expose a couple of helpers to fix that:
> >
> >     # Return the same (shallow) reduction tuple that pickle.py, copy.py,
> > and _pickle.c would use
> >     pickle.reduce(obj) -> (callable, args[, state[, litems[, ditem[,
> > statefunc]]]])
> >
> >     # Return a callable and arguments to construct a (shallow) equivalent
> > object
> >     # Raise a TypeError when that isn't possible
> >     pickle.deconstruct(obj) -> callable, args, kw
> >
> > So, why do you want these?
> >
> > There are many cases where you want to "deconstruct" an object if
> > possible. Pattern matching depends on being able to deconstruct objects
> > like this. Auto-generating a `__repr__` as suggested in Chris's thread.
> > Quick&dirty REPL stuff, and deeper reflection stuff using
> > `inspect.Signature` and friends.
> >
> > Of course not every type tells `pickle` what to do in an appropriate way
> > that we can use, but a pretty broad range of types do, including (I think;
> > I haven't double-checked all of them) `@dataclass`, `namedtuple`,
> > `@attr.s`, many builtin and extension types, almost all reasonable types
> > that use `copyreg`, and any class that pickles via the simplest
> > customization hook `__getnewargs[_ex]__`. That's more than enough to be
> > useful. And, just as important, it won't (except in intentionally
> > pathological cases) give us a false positive, where a type is correctly
> > pickleable and we think we can deconstruct it but the deconstruction is
> > wrong. (For some uses, you are going to want to fall back to heuristics
> > that are often right but sometimes misleadingly wrong, but I don't think
> > the `pickle` module should offer anything like that. Maybe `inspect`
> > should.)
> >
> > The way to get the necessary information isn't fully documented, and
> > neither is the way to interpret it. And I don't think it _should_ be
> > documented, because it changes every so often, and for good reasons; we
> > don't want anyone writing third-party code that relies on those details.
> > Plus, a different Python implementation might conceivably do it
> > differently. Public helpers exposed from `pickle` itself won't have those
> > problems.
> >
> > Here's a first take at the code.
> >
> >     def reduce(obj, proto=pickle.DEFAULT_PROTOCOL):
> >         """reduce(obj) -> (callable, args[, state[, litems[, ditem[,
> > statefunc]]]])
> >         Return the same reduction tuple that the pickle and copy modules
> > use
> >         """
> >         cls = type(obj)
> >         if reductor := copyreg.dispatch_table.get(cls):
> >    return reductor(obj)
> > # Note that this is not a special method call (not looked up on the type)
> > if reductor := getattr(obj, "__reduce_ex__"):
> >    return reductor(proto)
> > if reductor := getattr(obj, "__reduce__"):
> >    return reductor()
> >         raise TypeError(f"{cls.__name__} objects are not reducible")
> >
> >     def deconstruct(obj):
> >         """deconstruct(obj) -> callable, args, kw
> >         callable(*args, **kw) will construct an equivalent object
> >         """
> >         reduction = reduce(obj)
> >         # If any of the optional members are included, pickle/copy has to
> > # modify the object after construction, so there is no useful single
> > # call we can deconstruct to.
> >         if any(reduction[2:]):
> >             raise TypeError(f"{type(obj).__name__} objects are not
> > deconstrutable")
> >         func, args, *_ = reduction
> >         # Most types (including @dataclass, namedtuple, and many builtins)
> > # use copyreg.__newobj__ as the constructor func. The args tuple is
> > # the type (or, when appropriate, some other registered
> > # constructor) followed by the actual args. However, any function
> > # with the same name will be treated the same way (because under the
> > # covers, this is optimized to a special opcode).
> >         if func.__name__ == "__newobj__":
> >             return args[0], args[1:], {}
> > # Mainly only used by types that implement __getnewargs_ex__ use
> > # copyreg.__newobj_ex__ as the constructor func. The args tuple
> > # holds the type, *args tuple, and **kwargs dict. Again, this is
> > # special-cased by name.
> > if func.__name__ == "__newobj_ex__":
> >             return args
> > # If any other special copyreg functions are added in the future,
> > # this code won't know how to handle them, so bail.
> >         if func.__module__ == 'copyreg':
> >             raise TypeError(f"{type(obj).__name__} objects are not
> > deconstrutable")
> > # Otherwise, the type implements a custom __reduce__ or __reduce_ex__,
> > # and whatever it specifies as the constructor is the real constructor.
> >         return func, args, {}
> >
> > Actually looking at that code, I think it makes a better argument for why
> > we don't want to make all the internal details public. :)
> >
> > Here are some quick (completely untested) examples of other things we
> > could build on it.
> >
> >     # in inspect
> >     def deconstruct(obj):
> >         """deconstruct(obj) -> callable, bound_args
> > Calling the callable on the bound_args would construct an equivalent object
> > """
> > func, args, kw = pickle.deconstruct(obj)
> > sig = inspect.signature(func)
> > return func, sig.bind(*args, **kw)
> >
> >     # in reprlib, for your __repr__ to delegate to
> >     def auto_repr(obj):
> >         func, bound_args = inspect.deconstruct(obj)
> > args = itertools.chain(
> >    map(repr, bound_args.args),
> >    (f"{key!r}={value!r}" for key, value in bound_args.kwargs.items()))
> > return f"{func.__name__}({', '.join(args)})"
> >
> >     # or maybe as a class decorator
> >     def auto_repr(cls):
> >         def __repr__(self):
> >             func, bound_args = inspect.deconstruct(self)
> >        args = itertools.chain(
> >              map(repr, bound_args.args),
> >        (f"{key!r}={value!r}" for key, value in bound_args.kwargs.items()))
> >    return f"{func.__name__}({', '.join(args)})"
> > cls.__repr__ = __repr__
> > return cls
> >
> > _______________________________________________
> > Python-ideas mailing list -- python-ideas@python.org
> > To unsubscribe send an email to python-ideas-le...@python.org
> > https://mail.python.org/mailman3/lists/python-ideas.python.org/
> > Message archived at
> > https://mail.python.org/archives/list/python-ideas@python.org/message/RTZGM7L7JOTKQQICN6XDSLOAMU4A62CA/
> > Code of Conduct: http://python.org/psf/codeofconduct/
> >  
> 


_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/6I6LDMNELAK7HZVEOLOLXUUCURH4YLN3/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to