Archiwa blogu

Python – funkcje operujące na kolekcjach

2017-04-14

Dziś pokażę, w jaki sposób poprawić czytelność i prostotę kodu poprzez wykorzystanie odpowiednich funkcji Pythona. Naszym zadaniem będzie stworzenie klasy Vector, przyjmującej w konstruktorze listę liczb całkowitych oraz posiadającej kilka metod. Oto wymagania dla tej klasy:

a = Vector([1,2,3])
b = Vector([4,5,6])
a.add(b) #zwraca obiekt: Vector([5,7,9])
a.subtract(b) #zwraca obiekt: Vector([-3,-3,-3])
a.dot(b) #zwraca wynik: 1*4+2*5+3*6 = 32
a.norm() #zwraca wynik: sqrt(1^2+2^2+3^2) = 3.74
a.equals(b) #zwraca wynik: False
a.equals(Vector([1,2,3])) #zwraca wynik: True
str(a) #zwraca łańcuch: "(1,2,3)"
#w przypadku metod add, subtract i dot, gdy a i b mają inną liczbę elementów powinien zostać zgłoszony wyjątek

Skoro znamy wymagania, możemy przejść do implementacji klasy Vector:

from math import sqrt

class Vector():
    def __init__(self, items):
        self.items = items

    def add(self, other):
        if len(self.items) != len(other.items):
            raise Exception('Error')
        a = []
        for i in range(len(self.items)):
            a.append(self.items[i] + other.items[i])
        return Vector(a)

    def subtract(self, other):
        if len(self.items) != len(other.items):
            raise Exception('Error')
        a = []
        for i in range(len(self.items)):
            a.append(self.items[i] - other.items[i])
        return Vector(a)

    def dot(self, other):
        if len(self.items) != len(other.items):
            raise Exception('Error')
        a = 0
        for i in range(len(self.items)):
            a = a + (self.items[i] * other.items[i])
        return a

    def norm(self):
        a = 0
        for i in self.items:
            a = a + (i*i)
        return sqrt(a)

    def equals(self, other):
        if len(self.items) != len(other.items):
            return False
        for i in range(len(self.items)):
            if self.items[i] != other.items[i]:
                return False
        return True

    def __str__(self):
        s = "("
        for i in self.items:
            if len(s) > 1:
                s = s + ","
            s = s + str(i)
        s = s + ")"
        return s

Powyższy kod działa i realizuje wszystkie wymagania. Czy można go jednak uprościć? Jak widać we wszystkich metodach użyte zostały pętle for oraz warunki if. Czy możliwe jest pozbycie się ich wszystkich? Wykorzystując odpowiednie funkcje Pythona nie stanowi to problemu. Oto klasa Vector w nowej wersji:

from operator import add, sub, mul, eq
from functools import reduce
from math import sqrt

class Vector():
    def __init__(self, items):
        self.items = items

    def add(self, other):
        assert(len(self.items) == len(other.items))
        return Vector(map(add, self.items, other.items))

    def subtract(self, other):
        assert(len(self.items) == len(other.items))
        return Vector(map(sub, self.items, other.items))

    def dot(self, other):
        assert(len(self.items) == len(other.items))
        func = (lambda x, y: x + y)
        return reduce(func, map(mul, self.items, other.items))

    def norm(self):
        return sqrt(sum(i * i for i in self.items))

    def equals(self, other):
        eqLen = (len(self.items) == len(other.items))
        return eqLen and all(map(eq, self.items, other.items))

    def __str__(self):
        return "({0})".format(",".join(map(str, self.items)))

Zgłaszanie wyjątków zostało zastąpione funkcją assert, która robi to automatycznie gdy przekazany warunek nie zostanie spełniony. Z kolei wszystkie pętle stały się zbędne po wykorzystaniu funkcji map, reduce, sum oraz all. Oto opis tych i kilku podobnych funkcji Pythona:

  • add(a, b) – to samo co a + b
  • sub(a, b) – to samo co a – b
  • mul(a, b) – to samo co a * b
  • eq(a, b) – to samo co a == b
  • sum(iterable) – zwraca sumę przekazanych elementów
  • max(iterable) – zwraca największy z przekazanych elementów
  • min(iterable) – zwraca najmniejszy z przekazanych elementów
  • any(iterable) – zwraca True jeżeli dla któregokolwiek z przekazanych elementów bool(x) == True
  • all(iterable) – zwraca True jeżeli dla wszystkich przekazanych elementów bool(x) == True
  • map(function, *iterables) – zwraca iterator, którego elementy są wynikami przekazanej funkcji, która z kolei jako argumenty przyjmuje kolejne elementy przekazanych kolekcji. Liczba wynikowych elementów odpowiada liczbie elementów najkrótszej z przekazanych kolekcji. Np. list(map(add, [1,2,3], [4,5,6])) = [5,7,9] lub list(map(eq, [1,2,3], [1,5,3])) = [True, False, True]
  • reduce(function, sequence[, initial]) – redukuje przekazaną kolekcję do pojedynczej wartości poprzez wykonanie przekazanej dwuargumentowej funkcji na każdym elemencie kolekcji. W każdym kroku do funkcji przekazywany jest bieżący wynik operacji (poprzedni wynik funkcji) wraz z kolejnym elementem kolekcji. Jeżeli nie zostanie podana opcjonalna wartość początkowa, będzie nią pierwszy element kolekcji.
  • filter(function, iterable) – zwraca iterator zawierający te elementy przekazanej kolekcji, dla których przekazana funkcja zwróciła wartość True. Jeżeli nie zostanie przekazana funkcja (None), zwrócone zostaną elementy, dla których bool(x) == True

Wykorzystując powyższe funkcje – dodatkowo łącząc je z listami składanymi – możemy w prosty i przejrzysty sposób wykonać wiele operacji, które w innych językach programowania wymagałyby zastosowania skomplikowanych i mało czytelnych pętli. Właśnie dlatego tak lubię Pythona. 🙂