I'm implementing `deep_get` functionality to look inside arbitrarily nested Python **2.7** objects. Primarily for further logging.

This turned out to have surprising amount of quirks. Here's what I ended up with, would appreciate the feedback as I probably missed a few more things.


    # coding=utf-8
    from __future__ import unicode_literals
    import collections
    
    _default_stub = object()
    
    
    def deep_get(obj, path, default=_default_stub, separator='.'):
        """Gets arbitrarily nested attribute or item value.
    
        Args:
            obj: Object to search in.
            path (str, hashable, iterable of hashables): Arbitrarily nested path in obj hierarchy.
            default: Default value. When provided it is returned if the path doesn't exist.
                Otherwise the call raises a LookupError.
            separator: String to split path by.
    
        Returns:
            Value at path.
    
        Raises:
            LookupError: If object at path doesn't exist.
    
        Examples:
            >>> deep_get({'a': 1}, 'a')
            1
    
            >>> deep_get({'a': 1}, 'b')
            LookupError: {'a': 1} has no element at 'b'
    
            >>> deep_get(['a', 'b', 'c'], -1)
            'c'
    
            >>> deep_get({'a': [{'b': [1, 2, 3]}, 'some string']}, 'a.0.b')
            [1, 2, 3]
    
            >>> class A(object):
            >>>     def __init__(self):
            >>>         self.x = self
            >>>         self.y = {'a': 10}
            >>>
            >>> deep_get(A(), 'x.x.x.x.x.x.y.a')
            10
    
            >>> deep_get({'a.b': {'c': 1}}, 'a.b.c')
            LookupError: {'a.b': {'c': 1}} has no element at 'a'
    
            >>> deep_get({'a.b': {'Привет': 1}}, ['a.b', 'Привет'])
            1
    
            >>> deep_get({'a.b': {'Привет': 1}}, 'a.b/Привет', separator='/')
            1
    
        """
        if isinstance(path, basestring):
            attributes = path.split(separator)
        elif isinstance(path, collections.Iterable):
            attributes = path
        else:
            attributes = [path]
    
        for i in attributes:
            try:
                success = False
                # 1. access as attr
                try:
                    obj = getattr(obj, i)
                    success = True
                except (AttributeError, TypeError, UnicodeEncodeError):
                    # 2. access as dict index
                    try:
                        obj = obj[i]
                        success = True
                    except (TypeError, AttributeError, IndexError, KeyError):
                        # 3. access as list index
                        try:
                            obj = obj[int(i)]
                            success = True
                        except (TypeError, AttributeError, IndexError, KeyError,
                                UnicodeEncodeError, ValueError):
                            pass
    
                if not success:
                    msg = "{obj} has no element at '{i}'".format(obj=obj, i=i)
                    raise LookupError(msg.encode('utf8'))
    
            except Exception:
                if _default_stub != default:
                    return default
                raise
    
        return obj