def extract(data, ids, default=None):
    """
    inputs:
        data: hierarchical data structure consisting of lists, dicts, and objects with attributes.
        ids: a list of idices, key names, and attribute names
        default: optional object
    output:
        the value stored at the location indicated by the ids list or default if the location
        does not exist.

    Strings in ids are first tried as key name and if no such key name exists, then they are
    tried as attribute names. To designate that a member of ids should be used as an attribute
    and not a key name, make it a tuple with the attribute name string as the first member, such
    as: ('attribute_name',). If you want to make it more explict to the reader, you can add a
    second member to the tuple, which will not be used, such as ('attribute_name', 'as attribute')
    """
    if len(ids) == 0:
        return data
    try:
        if isinstance(ids[0], tuple):
            sub_data = getattr(data, ids[0][0])
        else:
            try:
                sub_data = data[ids[0]]
            except TypeError:
                sub_data = getattr(data, ids[0])
    except (AttributeError, IndexError, KeyError):
        return default
    else:
        return extract(sub_data, ids[1:], default)