3

I want to create an object that 'acts' like a string but when it's accessed, fires a function and returns that result.

The simple case for __str__ and __repr__ is easy enough; but I can't work out how to get json.dumps to treat it the same way;

import json, datetime


class DynamicString(str):
    def __init__(self, genf):
        self.generate = genf
    def __repr__(self):
        print("Called Repr")
        return self.generate()
    def __str__(self):
        print("Called Str")
        return self.generate()


dater=DynamicString(lambda: datetime.datetime.now().isoformat())
print(f"{dater!s}, {dater!r}, {dater!a}")
>>> Called Str
>>> Called Repr
>>> Called Repr
>>> 2019-05-01T13:52:12.588907, 2019-05-01T13:52:12.588933, 2019-05-01T13:52:12.588950

print(json.dumps(dater))
>>> "<function <lambda> at 0x10bb48730>"

It appears that however json.dumps is evaluating the object it's ignoring the custom dunder methods.

I can't use a custom JSONEncoder as this object is intended to be sent through modules I don't have access to modify. Any ideas?

UPDATE FOR CLARITY:

Expected output

json.dumps(dater)
>>> '"2019-05-01T16:43:21.956985"'

i.e. "Exactly as if it was just a normal string, but based on the current time"

14
  • These posts can help: stackoverflow.com/a/38764817/9609843 stackoverflow.com/q/18478287/9609843 Commented May 1, 2019 at 13:13
  • Is there a particular reason you inherit from str that is not shown here? Or can you inherit from something else? Commented May 1, 2019 at 13:45
  • 1
    Can't you do json.dumps(str(dater))? Or build your own serialization method in case more complex behavior is expected? Commented May 1, 2019 at 14:12
  • 1
    @Error-SyntacticalRemorse: Changing the default within dumps won't work because default is only used for objects the JSONEncoder doesn't already know how to handle (see table). In other words it won't be used because isinstance(dater, str) is True. It works in the answer you linked to because class Doc isn't derived from something shown in the table. Commented May 2, 2019 at 12:04
  • 1
    Bolster: From your last comment, it sounds like your class doesn't need to be derived from str, just act more-or-less like one. If that's true, there may be hope. However, you still haven't said what you would like or expect to get back from using loads() on the JSON output produced. Commented May 2, 2019 at 12:09

2 Answers 2

0

The problem with this seem to lie with the fact that str class is immutable, which gets its value from initiation of your object, and does not change. __new__ function of class str is called, which takes in your genf, which then evaluated to

"<function <lambda> at 0xADDRESS>"

All subsequent call on the instance has no effect on this string. To verify this, you can overwrite the string at the beginning of your class definition by overwriting __new__:

class DynamicString(str):
    def __new__(cls, genf):
        o = super(DynamicString, cls).__new__(cls, "HAHA")
        return o
    ... rest of your class ...

... rest of your script ...

# Then:
>>> json.dumps(dater)
>>> "HAHA"

There doesn't seem to be any workaround as str is simply immutable, and by extension, your objects parent string is also immutable for the life of your instance..

Sign up to request clarification or add additional context in comments.

2 Comments

I don't think the problem is immutability, it's because JSONEncoder already know how to handle objects of type str including any classes derived from it (i.e. str subclasses). There's a table in the document showing the object types it handles automatically.
The point being that when it does that it automatically takes the immutable string that is initiated on the creation of the instance. There's no way to change that, so the only choice left is to use default function like str(instance).
0

If all you want to do is display it like a 'dynamic string' object when json.dumps() is called, the first part of my answer to the question Making object JSON serializable with regular encoder could be used provided you don't derive your class from the built-in str class and add a to_json method.

Changing the former prevent the json module from automatically handling it because strings—and any subclass of it—are among the types of the things it's hardcoded to handle).

# Monkey-patch json module.
from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder.default  # Save unmodified default.
JSONEncoder.default = _default # Replace it.


if __name__ == '__main__':

    # Sample usage.

    import datetime
    import json


    class DynamicString(object):
        def __init__(self, genf):
            self.generate = genf

        def __repr__(self):
            print("Called Repr")
            return self.generate()

        def __str__(self):
            print("Called Str")
            return self.generate()

        def to_json(self):  # Added.
            return repr(self)


    dater = DynamicString(lambda: datetime.datetime.now().isoformat())
    print(f"{dater!s}, {dater!r}, {dater!a}")
    print()
    print(json.dumps(dater))

Output:

Called Str
Called Repr
Called Repr
2019-05-07T13:11:32.061129, 2019-05-07T13:11:32.061129, 2019-05-07T13:11:32.061129

Called Repr
"2019-05-07T13:11:32.061129"

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.