Recipe 2.22. Computing the Relative Path from One Directory to Another
Credit: Cimarron Taylor, Alan Ezust
Problem
You need to know the relative
path from one directory to anotherfor example, to create a
symbolic link or a relative reference in a URL.
Solution
The simplest approach is to split paths into lists of directories,
then work on the lists. Using a couple of auxiliary and somewhat
generic helper functions, we could code:
import os, itertools
def all_equal(elements):
''' return True if all the elements are equal, otherwise False. '''
first_element = elements[0]
for other_element in elements[1:]:
if other_element != first_element: return False
return True
def common_prefix(*sequences):
''' return a list of common elements at the start of all sequences,
then a list of lists that are the unique tails of each sequence. '''
# if there are no sequences at all, we're done
if not sequences: return [ ], [ ]
# loop in parallel on the sequences
common = [ ]
for elements in itertools.izip(*sequences):
# unless all elements are equal, bail out of the loop
if not all_equal(elements): break
# got one more common element, append it and keep looping
common.append(elements[0])
# return the common prefix and unique tails
return common, [ sequence[len(common):] for sequence in sequences ]
def relpath(p1, p2, sep=os.path.sep, pardir=os.path.pardir):
''' return a relative path from p1 equivalent to path p2.
In particular: the empty string, if p1 == p2;
p2, if p1 and p2 have no common prefix.
'''
common, (u1, u2) = common_prefix(p1.split(sep), p2.split(sep))
if not common:
return p2 # leave path absolute if nothing at all in common
return sep.join( [pardir]*len(u1) + u2 )
def test(p1, p2, sep=os.path.sep):
''' call function relpath and display arguments and results. '''
print "from", p1, "to", p2, " -> ", relpath(p1, p2, sep)
if _ _name_ _ == '_ _main_ _':
test('/a/b/c/d', '/a/b/c1/d1', '/')
test('/a/b/c/d', '/a/b/c/d', '/')
test('c:/x/y/z', 'd:/x/y/z', '/')
Discussion
The workhorse in this recipe is the simple but very general function
common_prefix, which, given any
N sequences, returns their common prefix
and a list of their respective unique tails. To compute the relative
path between two given paths, we can ignore their common prefix. We
need only the appropriate number of move-up markers (normally,
os.path.pardire.g., ../
on Unix-like systems; we need as many of them as the length of the
unique tail of the starting path) followed by the unique tail of the
destination path. So, function relpath splits the
paths into lists of directories, calls
common_prefix, and then performs exactly the
construction just described.common_prefix centers on the loop for
elements in itertools.izip(*sequences), relying on the fact
that izip ends with the shortest of the iterables
it's zipping. The body of the loop only needs to
prematurely terminate the loop as soon as it meets a tuple of
elements (coming one from each sequence, per
izip's specifications) that
aren't all equal, and to keep track of the elements
that are equal by appending one of them to list
common. Once the loop is done, all
that's left to prepare the lists to return is to
slice off the elements that are already in common
from the front of each of the sequences.Function all_equal could alternatively be
implemented in a completely different way, less simple and obvious,
but interesting:
def all_equal(elements):or, equivalently and more concisely, in Python 2.4 only,
return len(dict.fromkeys(elements)) == 1
def all_equal(elements):Saying that all
return len(set(elements)) == 1
elements are equal is exactly the same as saying that the
set of the elements has cardinality (length) one.
In the variation using dict.fromkeys, we use a
dict to represent the set, so
that variation works in Python 2.3 as well as in 2.4. The variation
using set is clearer, but it only works in Python
2.4. (You could also make it work in version 2.3, as well as Python
2.4, by using the standard Python library module
sets).
See Also
Library Reference and Python in a
Nutshell docs for modules os and
itertools.