[Tutor] How to invoke different member-function according to different object?

Peter Otten __peter__ at web.de
Sat Jan 7 13:00:24 CET 2012


daedae11 wrote:

> I was asked to write a program to move files between ZIP(.zip) and
> TAR/GZIP(.tgz/.tar.gz) or TAR/BZIP2(.tbz/.tar.bz2) archive.
> 
> my code is:
> 
> 
> import zipfile;
> import tarfile;
> import os;
> from os import path ;
> 
> def showAllFiles(fileObj):
>     if fileObj.filename.endswith("zip"):
>         if isinstance(fileObj, zipfile.ZipFile):
>             print "j"*20;
>         for name in fileObj.namelist():
>             print name;
>     else:
>         for name in fileObj.getnames():
>             print name;
> 
> def moveFile(srcObj, dstObj):
>     fileName = raw_input("input the name of the file to move: ");
>     srcObj.extract(fileName);
>     if isinstance(dstObj, zipfile.ZipFile):
>         dstObj.write(fileName);
>     else:
>         dstObj.addfile(tarfile.TarInfo(fileName));
>     os.remove(fileName);
>     
> def main():
>     intro = """
> enter a choice
> (M)ove file from source file to destinatiom file
> (S)how all the files in source file
> (Q)uit
> your choice is: """
>     srcFile = raw_input("input the source file name: ");
>     dstFile = raw_input("input the destination file name: ");
>     while True:
>         with ( zipfile.ZipFile(srcFile, "r") if srcFile.endswith("zip")
>         else tarfile.open(srcFile, "r"+":"+path.splitext(srcFile)[1][1:])
>         ) as srcObj, \ ( zipfile.ZipFile(dstFile, "r") if
>            dstFile.endswith("zip") else
>             tarfile.open(dstFile, "w"+":"+path.splitext(dstFile)[1][1:]) )
>             as dstObj:
>                 choice = raw_input(intro)[0].lower();
>                 if choice == "s":
>                     showAllFiles(srcObj);
>                 elif choice == "m":
>                     moveFile(srcObj, dstObj);
>                 elif choice == "q":
>                     break;
>                 else:
>                     print "invalid command!"
> 
> if __name__ == '__main__':
>     main();
> 
> But there are some problems.
> 1. It could extract file successfully, but can't add files to .tar.gz
> file. 2. I think it's a little tedious, but I don't know how to improve
> it.
> 
> Please  give me some help , thank you!

First, but least: don't pollute your code with trailing semicola.
Second: what Steven says, plus it will always take you longer to implement a 
script than you initially think. I ran into that for the umpteenth time when 
trying to reorganize your script.

Now to your code:
- Try to separate the user interface (aka raw_input()) from the other 
functionality of your script. moveFile() will be easier to test when you  
pass the filename as an argument rather than asking the user for it. 

- Don't overuse inlined if-else expressions. A separate

if complex condition:
   var = ...
else:
   var = ...

is easier to read -- and easier to extend with another elif.

- If you have a lot of if-else switches consider classes

# wrong
animal = dog_or_duck()
if its_a_dog(animal):
   bark()
elif its_a_duck(animal):
   quack()

# right
animal = dog_or_duck()
animal.make_some_noise()

In your case there already are classes, but with incompatible interfaces. 
You can solve that by subclassing or making wrapper classes.

Another approach that addresses the same problem is table-driven:

def quack(): print "quack"
def bark(): print "wuff"
lookup_noise = {
    Dog: bark,
    Duck: quack,
}
animal = dog_or_duck()
lookup_noise[animal.__class__]()

In my rewritten version of your script I use both approaches. It is still 
incomplete and has already become almost too long to post. It is probably a 
bit too complex for a beginner, too, but the idea is simple and if you have 
questions I'm willing to answer. 

Basically there is a class for every archive type which has to implement an 
open(name) method which has to return a file-like object and an add(name, 
file) which has to add a file-like object as name to the archive.
You can ask if a certain archive class can deal with a file with e. g.

ZipArchive.can_handle(filename)

The script features an adhoc test that you can invoke with

python scriptname.py --test

Look into http://docs.python.org/library/unittest.html if you are serious 
about testing. I think you should be ;)

import os
import shutil
import sys
import tarfile
import zipfile

def _not_implemented(obj, methodname):
    typename = obj.__class__.__name__
    print >> sys.stderr, "{}.{}() not implemented".format(typename, 
methodname)

class Archive(object):
    @classmethod
    def can_handle(class_, filename):
        return filename.lower().endswith(class_.suffixes)

    def __init__(self, filename, mode):
        if mode not in ("r", "w"):
            raise ValueError("Mode {} not understood".format(mode))
        self.filename = filename
        self.mode = mode
        self._archive = self._open_archive()
    def getnames(self):
        return self._archive.getnames()
    def __enter__(self):
        return self
    def __exit__(self, *args):
        self._archive.close()

    def _open_archive(self):
        _not_implemented(self, "_open")
    def add(self, name, srcfile):
        _not_implemented(self, "add")
    def open(self, name):
        _not_implemented(self, "extract")

class ZipArchive(Archive):
    suffixes = (".zip",)
    def _open_archive(self):
        return zipfile.ZipFile(self.filename, self.mode)
    def getnames(self):
        return self._archive.namelist()
    def open(self, name):
        return self._archive.open(name)

class TarArchive(Archive):
    suffixes = (".tar",)
    def _open_archive(self):
        return tarfile.open(self.filename, self.mode)
    def add(self, name, srcfile):
        tmpname = "tmp.xxx" # TODO use tempfile module
        with open(tmpname, "wb") as dstfile:
            shutil.copyfileobj(srcfile, dstfile)
        self._archive.add(tmpname, name)
        os.remove(tmpname)

class TgzArchive(TarArchive):
    suffixes = (".tar.gz", ".tgz")
    def _open_archive(self):
        return tarfile.open(self.filename, self.mode + ":gz")

lookup = [
    ZipArchive,
    TarArchive,
    TgzArchive,
]

def move_file(src, dst, filename):
    dst.add(filename, src.open(filename))

def open_archive(filename, mode):
    for Archive in lookup:
        if Archive.can_handle(filename):
            return Archive(filename, mode)
    raise ValueError("Archive type of {!r} not understood".format(filename))

def show_all_files(archive):
    for name in archive.getnames():
        print name

def main():
    if "--test" in sys.argv:
        with zipfile.ZipFile("one.zip", "w") as dst:
            for data in "alpha beta gamma".split():
                dst.writestr(data + ".txt", data)
        with open_archive("one.zip", "r") as src:
            with open_archive("two.tar", "w") as dst:
                move_file(src, dst, "alpha.txt")
            with open_archive("three.tar.gz", "w") as dst:
                move_file(src, dst, "beta.txt")
                move_file(src, dst, "gamma.txt")
        with tarfile.open("two.tar") as src:
            assert src.getnames() == ["alpha.txt"]
        with tarfile.open("three.tar.gz") as src:
            assert src.getnames() == ["beta.txt", "gamma.txt"]
        # TODO: check contents
        return

    intro = """
enter a choice
(M)ove file from source file to destinatiom file
(S)how all the files in source file
(Q)uit
your choice is: """
    srcname = raw_input("input the source file name: ")
    dstname = raw_input("input the destination file name: ")
    with open_archive(srcname, "r") as src:
        while True:
            choice = raw_input(intro).strip().lower()
            if choice == "s":
                show_all_files(src)
            elif choice == "m":
                filename = raw_input("input the name of the file to move: ")
                with open_archive(dstname, "w") as dst:
                    move_file(src, dst, filename)
            elif choice == "q":
                break
            else:
                print "invalid command ({!r})!" % choice

if __name__ == '__main__':
    main()



More information about the Tutor mailing list