Replace key(s) in OrderedDict ``od`` by new key(s) in-place (i.e.,
    preserving the order(s) of the key(s))

    Optional new value(s) for new key(s) can be provided as a positional
    argument (otherwise the old value(s) will be used):

        od_replace_key(od, key, new_key, new_value)

    To replace multiple keys, pass argument ``key`` as a list instance,
    or explicitly pass a keyword argument ``multi=True``:

        od_replace_key(od, keys, new_keys, [new_values,] multi=True)

def od_replace_key(od, key, new_key, *args, **kw):
    '''
    Replace key(s) in OrderedDict ``od`` by new key(s) in-place (i.e.,
    preserving the order(s) of the key(s))

    Optional new value(s) for new key(s) can be provided as a positional
    argument (otherwise the old value(s) will be used):

        od_replace_key(od, key, new_key, new_value)

    To replace multiple keys, pass argument ``key`` as a list instance,
    or explicitly pass a keyword argument ``multi=True``:

        od_replace_key(od, keys, new_keys, [new_values,] multi=True)

    '''
    multi = kw.get('multi', False) or isinstance(key, list)

    if multi:
        if len(key) != len(new_key):
            raise ValueError('Length of keys (%s) does not match '
                             'length of new keys (%s)' % (len(key), len(new_key)))
        if args:
            new_value = args[0]
            if len(new_key) != len(new_value):
                raise ValueError('Length of new keys (%s) does not match '
                                 'length of new values (%s)' %
                                 (len(new_key), len(new_value)))
            for k_old, k_new, v_new in zip(key, new_key, new_value):
                od_replace_key(od, k_old, k_new, v_new)
        else:
            for k_old, k_new in zip(key, new_key):
                od_replace_key(od, k_old, k_new)
        return

    # single key

    if args:
        value = args[0]

    if new_key == key:
        if args:
            OrderedDict.__setitem__(od, key, value)
        return

    # new_key != key
    if new_key in od: # new_key overwrites another existing key
        OrderedDict.__delitem__(od, new_key)

    if PY2:
        # modify internal variables
        _map = od._OrderedDict__map
        link = _map[key]
        link[2] = new_key
        del _map[key]
        _map[new_key] = link
        val = dict.pop(od, key)
        if args:
            val = value
        dict.__setitem__(od, new_key, val)

    else:
        # in PY3, OrderedDict is implemented in C
        # no access to private variables __map
        found = False
        keys = [] # keys after key
        for k in od:
            if k == key:
                found = True
                continue
            if found:
                keys.append(k)
        # warning: can not use OrderedDict.pop, which calls del self[key]
        getitem = dict.__getitem__
        setitem = OrderedDict.__setitem__
        delitem = OrderedDict.__delitem__
        v = getitem(od, key)
        delitem(od, key)
        if args:
            v = value
        setitem(od, new_key, v)
        # shift keys to after new_key
        for k in keys:
            # od[k] = od.pop(k) # can not call this directly in MIDict
            v = getitem(od, k)
            delitem(od, k)
            setitem(od, k, v)