Övning 6 Ali Tofigh 24 Oktober, 2006 1 Sortering Att sortera behöver man göra väldigt ofta i programmering. Python tillhandahåller bland annat en metod i listor för detta ändamål. Det enda metoden behöver veta för att kunna sortera elementen är hur par av element jämförs med varandra. Vi ska se tre olika sätt att åstadkomma detta. Vi börjar med en enkel klass, Rectangle, vars instanser har två attribut; width och height. Vi skriver även en metod area som beräknar arean, och två metoder som bör kännas som standard vid det här laget, nämligen metoderna init och str. Dessutom lägger vi till en speciell metod som heter cmp. Denna används av Pythons sort-metod för att sortera listan. Mer om den längre ner. class Rectangle(object): def init (self, width, height): self.width = width self.height = height def str (self): return "w:" + str(self.width) + "\th:" + str(self.height) def area(self): return self.width * self.height def cmp (self, other): if self.height < other.height: return -1 elif self.height > other.height: return 1 else: return 0 Om vi nu har en lista av rektanglar och anropar metoden sort kommer rektanglarna i listan att sorteras i stigande höjd, dvs rektangeln med minst höjd kommer att vara först i listan. Detta beror på att sort letar efter den 1
speciella metoden cmp i de objekt som finns i listan och använder den för att jämföra objekten med varandra. Det som skickas tillbaka från cmp förväntas vara ett tal som är mindre än noll om parametern self ska vara före other i den sorterade listan, större än noll om self ska vara efter other i den sorterade listan, och noll om båda parametrarna är likvärdiga och deras ordning inte har någon betydelse. Titta nu igenom koden för cmp ovan och försäkra dig om att du förstår nedanstående kod: l = [Rectangle(1,7), Rectangle(3, 4), Rectangle(5,2), Rectangle(2,6)] for r in l: print r l.sort() for r in l: print r Hade vi inte skrivit metoden cmp hade rektanglarna sorterats efter deras minnesadress vilket vi inte har någon kontroll över. Observera att man ska skriva metoden cmp i de klasser där instanserna har en naturlig ordning. Vårt exempel med rektanglar är bara till för att illustrera den tekniska biten. Det hade till exempel varit mer naturligt att skriva en cmp -metod för en klass Rational som hanterar rationella tal. Ett annat sätt att sortera är att skriva en funktion och skicka denna till sort. Detta är användbart om man vill kunna sortera på olika sätt i samma program, eller om klassen man använder sig av inte har en lämpligt cmp -metod. Anta t ex att vi vill sortera våra rektanglar efter bredd istället för höjd. Vi kan då skriva en funktion som vi skickar till sort och därmed kommer sort att använda denna för att jämföra objekten i listan istället för cmp -metoden. def cmp_width(r1, r2): if r1.width < r2.width: return -1 elif r1.width > r2.width: return 1 else: return 0 Nu kan vi skriva: 2
l = [Rectangle(1,7), Rectangle(3, 4), Rectangle(5,2), Rectangle(2,6)] for r in l: print r l.sort(cmp = cmp_width) for r in l: print r Ett tredje sätt att få sort att jämföra objekt på det sätt vi vill, är att skicka med en metod. Objekten kommer då att sorteras efter det värde som metoden skickar tillbaka. Om vi till exempel vill sortera på rektanglarnas areor kan vi göra följande: l = [Rectangle(1,7), Rectangle(3, 4), Rectangle(5,2), Rectangle(2,6)] l.sort(key = Rectangle.area) for r in l: print r Observera att det är lite skillnad mellan att sätta sorts cmp-parameter till en funktion och att sätta dess key-parameter till en metod. I det senare fallet kommer metoden att anropas för objekten och metodens returvärde kommer att jämföras för att bestämma objektens ordning! Metoden sort tar tre parametrar, varav två är de som vi redan sett, nämligen cmp och key. Det tredje heter reverse och är satt till False som default. Om man sätter reverse till True kommer listan att sorteras baklänges. Vill vi till exempel sortera rektanglar efter bredd med störst bredd först kan vi återanvända vår funktion cmp_width och sätta reverse till True: l = [Rectangle(1,7), Rectangle(3, 4), Rectangle(5,2), Rectangle(2,6)] l.sort(cmp = cmp_width, reverse = True) for r in l: 3
print r 2 Sträng- och filhantering I de flesta P-uppgifter behöver man läsa och eventuellt skriva till fil. Filerna antas ha ett speciellt format som finns beskrivet i uppgiftslydelsen. Oftast läser man rad för rad från filen och använder sig av sträng-metoder för att få tag på de bitar som man behöver. Anta t ex att man har skrivit ett antal rektanglar till fil: -------------------- rects.txt -------------------- --------------------------------------------------- Om vi nu ska läsa filen och skapa rektanglar med angivet bredd och höjd kan vi göra på följande sätt: def read_rectangles(filename): # rad 1 f = open(filename, "r") # rad 2 rects = [] # rad 3 for line in f: # rad 4 wh = line.split() # rad 5 width = int(wh[0].split(":")[1]) # rad 6 height = int(wh[1].split(":")[1]) # rad 7 rects.append(rectangle(width, height)) # rad 8 return rects # rad 9 Givet funktionen ovan kan vi enkelt läsa in våra rektanglar från fil: l = read_rectangles("rects.txt") for r in l: print r 4
Några kommentarer behövs om read_rectangles. På rad 4 har vi en for-loop. För fil-objekt innebär det att vi loopar över innehållet rad för rad. Ett annat sätt att åstadkomma samma sak vore att byta ut rad 4 mot följande två rader: lines = f.readlines() for line in lines: På rad 5 använder vi sträng-metoden split. split är en väldigt användbar metod och det är värt att spendera lite tid åt att lära sig hur den fungerar. Om man anropar den utan några argument kommer man att få tillbaka en lista med delsträngar av den ursprungliga strängen. Strängen har splittats med hjälp av blanka tecken, det vill säga mellanslag, tab och nyrads-tecken. s = "hej san svej san" s.split() [ hej, san, svej, san ] s = "a-->b-->c-->def" s.split("-->") [ a, b, c, def ] Om man skickar en sträng som parameter kommer denna sträng att användas för att dela upp strängen: s = "hej, san, svej san" s.split(",") [ hej, san, svej san ] För att återgå till vårt exempel så kommer line att referera till strängen "" första varvet i for-loopen. På rad 5 kommer wh att vara listan [ w:3, h:4 ]. Och nu borde du själv kunna förstå vad rad 6 och 7 gör. 3 Exempeluppgift Anta nu att vi ska skriva ett program för att hantera information om bilar. För enklehets skull antar vi att det endast finns två saker vi ska hålla reda på för varje bil; färg och tillverkningsår. Vårt program ska kunna läsa information om bilar från en fil, skriva ut alla bilar sorterade på tillverkningsår och användaren ska kunna be om utskrift av alla bilar med en viss färg. Vi börjar med att skriva en klass Car. Instanser av Car ska ha två attribut: year och color. Dessutom ska vi skriva följande metoder: init, str och cmp. class Car(object): def init (self, year, color): self.year = year 5
self.color = color def str (self): return str(self.year) + "/" + str(self.color) def cmp (self, other): if self.year < other.year: return -1 if self.year > other.year: return 1 return 0 Nu skapar vi en klass Car_program som har i uppgift att läsa bilar från fil och hantera en lista med bilar. Vi kommer dessutom att låta den ha en dictionary med färger som nyckel och lista med bilar som värden. class Car_program(object): def init (self, filename): self.car_list = [] self.color_dict = {} self.read_file(filename) self.setup_dict() def read_file(self, filename): f = open(filename, "r") for line in f: attributes = line.split("/") year = int(attributes[0]) color = attributes[1].strip() self.car_list.append(car(year, color)) def setup_dict(self): for c in self.car_list: l = self.color_dict.get(c.color, []) l.append(c) self.color_dict[c.color] = l Vi har antagit att filen vi läser har följande format: -------------------- cars.txt -------------------- 2003/blue 1987/black 2006/red 1993/black 2002/blue 2001/green -------------------------------------------------- 6
Det mesta av koden borde vara bekant vid det här laget. Det kan vara värt att kommentera andra raden i metoden setup_dict. Jag har använt dictmetoden get. Om man försöker slå upp i en dictionary med en nyckel som inte finns kommer det att uppstå ett exception. Med get-metoden kan man instället tala om vad som kommer att returneras om nyckeln inte finns. I vårt fall har vi sagt om en viss färg inte finns i vår dictionary så ska en tom lista returneras. Observera att jag inte har skrivit kod för interaktion med användaren och jag kommer heller inte att skriva huvudprogrammet. Det viktiga nu är att inse att det mesta av jobbet är gjort. Vill man till exempel skriva ut alla bilar sorterade efter år kan man göra följande: prog = Car_program("cars.txt") prog.car_list.sort() for c in prog.car_list: print c 1987/black 1993/black 2001/green 2002/blue 2003/blue 2006/red Och vill man skriva ut alla bilar med en viss färg gör man så här: for c in prog.color_dict["black"]: print c 1987/black 1993/black for c in prog.color_dict["red"]: print c 2006/red Ett vanligt nybörjarmisstag är att lägga listan med alla bilar i klassen Car. Även om detta skulle fungera i små program så är det en dålig idé. Man vill att de datatyper man skapar ska vara så självständiga som möjligt. Det gör koden lättläst och det är lättare att lägga till och ändra ett programs beteende senare. Ett annat misstag är att försöka göra allt med en enda lista. Kom ihåg att variabler, listor och dicts endast refererar till objekt i minnet. Att vi sorterar car_list har till exempel ingen effekt på color_dict. Det är ju bara referenser till Car-objekten vi sorterar! Med andra ord, det går bra att ha samma objekt i flera datastrukturer. 7