Welcome, guest | Sign In | My Account | Store | Cart

The str() in the standard library behaves in a slightly weird way when applied against lists: on each element of the list, the repr() is appended. In contrast, this module provides a deep_str() that deeply applies str() across lists.

Python, 244 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""Utilities for handling deep str()ingification.

Danny Yoo (dyoo@hkn.eecs.berkeley.edu)


Casual Usage:

    from deepstr import deep_str
    print deep_str(["hello", "world"])


Very unusual usage:

    import deepstr
    import xml.sax.saxutils
    
    def handle_list(obj, deep_str):
        if isinstance(obj, list):
            results = []
            results.append("<list>")
            results.extend(["<item>%s</item>" % deep_str(x)
                            for x in obj])
            results.append("</list>")
            return ''.join(results)

    def handle_int(obj, deep_str):
        if isinstance(obj, int):
            return "<int>%s</int>" % obj

    def handle_string(obj, deep_str):
        if isinstance(obj, str):
            return ("<string>%s</string>" %
                    xml.sax.saxutils.escape(obj))

    def handle_default(obj):
        return "<unknown/>"

    silly = deepstr.DeepStr(handle_default)
    silly.recursive_str = deepstr.make_shallow_recursive_str(
        "<recursion-detected/>")
    silly.register(handle_list)
    silly.register(handle_int)
    silly.register(handle_string)
    print silly([3, 1, "four", [1, "<five>", 9.0]])
    x = []
    x.append(x)
    print silly(x)


This module provides a function called deep_str() that will do a deep
str() on objects.  This module also provides utilities to develop
custom str() functions.


(What's largely undocumented is the fact that this isn't really about
strings, but can be used as a general --- and convoluted --- framework
for mapping some process across data.)

"""

import unittest
    


def make_shallow_recursive_str(recursion_label):
    def f(obj, deep_str):
        return recursion_label
    return f


class DeepStr:
    """Deep stringifier."""
    def __init__(self,
                 default_str=str, 
                 recursive_str=make_shallow_recursive_str("...")):
        """
        DeepStr: stringify_function handler -> stringify_function
        
        Creates a new DeepStr.  Once constructed, you can call as
        if this were a function that takes objects and returns
        strings.

        default_str is the default function used on types that this
        does not recognize.  It must be able to take in an object and
        return a string.

        If we hit structure that's already been traversed,
        we use recursive_str on that structure."""
        self.handlers = []
        self.default_str = default_str
        self.recursive_str = recursive_str


    def __call__(self, obj):
        """Takes an object and returns a string of that object."""
        return self.deepstr(obj, {})


    def deepstr(self, obj, seen):
        ## Notes: this code is a little trickier than I'd like, but
        ## I don't see a good way of simplifying it yet.  Subtle parts
        ## include the construction of substructure_deepstr, and use
        ## of a fresh dictionary in 'new_seen'.
        if id(obj) in seen:
            ## TRICKY CODE: the recursive function is taking in a
            ## stringifier whose 'seen' dictionary is empty.
            def fresh_deepstr(sub_obj):
                return self.deepstr(sub_obj, {})
            return self.recursive_str(obj, fresh_deepstr)

        def substructure_deepstr(sub_obj):
            new_seen = dict(seen)
            new_seen[id(obj)] = True
            return self.deepstr(sub_obj, new_seen)

        for h in self.handlers:
            result = h(obj, substructure_deepstr)
            if result != None:
                return result
        return self.default_str(obj)


    def register(self, handler):
        """register: (object str_function -> string or None)

           Registers a new handler type.  Handers take in the object
           as well as a str() function, and returns either a string if
           it can handle the object, or None otherwise.  The second
           argument should be used on substructures."""
        self.handlers.append(handler)



######################################################################

## Below here is a sample implementation for deep_str()

def handle_list(obj, deep_str):
    if isinstance(obj, list):
        return "[" + ", ".join([deep_str(x) for x in obj]) + "]"
    return None

def handle_tuple(obj, deep_str):
    if isinstance(obj, tuple):
        return "(" + ", ".join([deep_str(x) for x in obj]) + ")"
    return None

def handle_dict(obj, deep_str):
    if isinstance(obj, dict):
        return ("{" + 
                ", ".join([deep_str(k) + ': ' + deep_str(v)
                           for (k, v) in obj.items()]) + 
                "}")
    return None

def handle_recursion(obj, deep_str):
    if isinstance(obj, list): return "[...]"
    ## tuples aren't handled; from my best understanding,
    ## it's not possible to construct a tuple that contains itself.
    if isinstance(obj, dict): return "{...}"
    return "..."

deep_str = DeepStr(str, handle_recursion)
deep_str.register(handle_list)
deep_str.register(handle_tuple)
deep_str.register(handle_dict)



######################################################################
## Sample exercising code.  This is here just to show a wacky example.

def _exercise():
    import xml.sax.saxutils
    def handle_list(obj, deep_str):
        if isinstance(obj, list):
            results = []
            results.append("<list>")
            results.extend(["<item>%s</item>" % deep_str(x)
                            for x in obj])
            results.append("</list>")
            return ''.join(results)

    def handle_int(obj, deep_str):
        if isinstance(obj, int):
            return "<int>%s</int>" % obj

    def handle_string(obj, deep_str):
        if isinstance(obj, str):
            return "<string>%s</string>" % xml.sax.saxutils.escape(obj)

    def handle_default(obj):
        return "<unknown/>"

    silly = DeepStr(handle_default)
    silly.recursive_str = make_shallow_recursive_str(
        "<recursion-detected/>")
    silly.register(handle_list)
    silly.register(handle_int)
    silly.register(handle_string)
    print silly([3, 1, "four", [1, "<five>", 9.0]])
    x = []
    x.append(x)
    print silly(x)


######################################################################

## Test cases
class MyTests(unittest.TestCase):
    def testSimpleThings(self):
        for obj in [42, 'hello', 0+1j, 2.3, u'world']:
            self.assertEquals(str(obj), deep_str(obj))

    def testSimpleLists(self):
        self.assertEquals(str([1, 2, 3]), deep_str([1, 2, 3]))

    def testListsWithStrings(self):
        self.assertEquals("[hello, world]", deep_str(["hello", "world"]))

    def testRepeatedObjects(self):
        self.assertEquals("[1, 1]", deep_str([1, 1]))

    def testRecursion(self):
        L = [1, 2]
        L.append(L)
        self.assertEquals("[1, 2, [...]]", deep_str(L))

    def testSimpleDict(self):
        self.assertEquals("{hello: world}", deep_str({'hello' : 'world'}))

    def testDictWithRecursion(self):
        D = {}
        D[1] = D
        self.assertEquals("{1: {...}}", deep_str(D)) 

    def testNonRecursion(self):
        a = ['a']
        L = [a, a]
        self.assertEquals("[[a], [a]]", deep_str(L))


if __name__ == '__main__':
    unittest.main()

str() has slightly surprising behavior, so this code tries to provide alternative functionality.

Implementing code to do this is actually a bit subtle, since we have to guard against recursive loops, so most of that hard work is handled deep within DeepStr.deepstr(). The unit tests at the bottom show some of the things we have to handle.

I thought it might also be amusing to make it extensible enough to turn it toward dark ends; the _exercise() code shows that it can even be applied to XMLify data structures, though there are better modules for doing this.