Zadanie 1.2 omówienie rozwiązania - WykopWyzwanie/wykop_wyzwaniepython GitHub Wiki

##Zadanie 1.2

Prezentowane rozwiązanie nie jest rozwiązaniem najkrótszym czy najsprytniejszym, ponieważ głównym celem jest pokazanie osobom początkującym w jaki sposób można podzielić duże zadanie na mniejsze części, wydzielone do małych, wyspecjalizowanych funkcji.

Link do rozwiązania

Na początek standardowo kilka importów. Narzędzie isort automatycznie posortowało je alfabetycznie.

from argparse import ArgumentParser
from collections import namedtuple
from glob import glob
from os.path import getsize, isfile

-- Następnie tworzę namedtuple - poprawia to czytelność kodu nie wpływając na jego wydajność. Dokumentacja

AccumulatedSize = namedtuple('AccumulatedSize', 'extension size files')

-- Rzeczą, która rzuca się tutaj w oczy jest konstrukcja yield from. Jest ona dostępna od wersji pythona 3.3. Pozwala ona na skrótowy zapis pętli - zamiast pisać for i in iterable: yield i można napisać po prostu yield from iterable. Konstrukcja ta ma w sobie znacznie więcej mocy, ale nie będę się o tym rozpisywał - ciekawi niech poszukają w internecie nagrań wykładów Davida Beazley'a o generatorach. Mówiąc o generatorach - w tym fragmencie dwukrotnie pojawia się generator expression. Składniowo od list comprehension różni się tylko znakami ograniczającymi - () zamiast [], w działaniu - zwraca generator zamiast listy :) Poza tym warto zwrócić uwagę na pomocniczą funkcję extract_extension - dzięki niej metoda get_files_info staje się znacznie czytelniejsza.

def get_files(directory):
    yield from (f for f in glob("{}/**".format(directory), recursive=True) if isfile(f))


def get_files_info(directory):
    yield from ((extract_extension(f), getsize(f)) for f in get_files(directory))


def extract_extension(filename):
    return filename.split('.')[-1]


def accumulate_size_by_extension(files):
    d = {}
    for (ext, size) in files:
        acc = d.get(ext, AccumulatedSize(ext, 0, 0))
        d[ext] = AccumulatedSize(acc.extension, acc.size + size, acc.files + 1)
    yield from d.values()

-- Formatowaniem outputu zajmują się te dwie wyspecjalizowane funkcje. Najciekawszą rzeczą jest tu **locals() - funkcja locals() zwraca wszystkie zmienne lokalne jako słownik, a ** to operator rozpakowania słownika.

def get_his(value, total):
    return '#' * round(50 * value / total)

def render_row(extension, size, histogram):
    return '{extension:>5}{size:>14}B{histogram:>60}\n'.format(**locals())

-- Funkcja main jest funkcją na najwyższym poziomie abstrakcji, steruje ona wykonaniem całej reszty logiki. Pierwsza jej część zbiera informacje o łącznej ilości plików w katalogu oraz o tym, jaka jest łączna waga i popularność plików o danym rozszerzeniu. Druga jej część sortuje wszystkie rozszerzenia malejąco przyjmując za klucz ilość plików o danym rozszerzeniu i dla każdego rozszerzenia zwraca (pojedynczo - funkcja main jest generatorem) wyrenderowaną jedną linijkę outputu.

def main(directory):
    total_files, extensions = 0, set()
    for accumulated in accumulate_size_by_extension(get_files_info(directory)):
        total_files += accumulated.files
        extensions |= {accumulated}

    for (ext, size, files) in sorted(extensions, key=lambda acc: acc.files, reverse=True):
        yield render_row(ext, size, get_his(files, total_files))

-- Ten fragment został odseparowany od reszty kodu w celu oddzielenia operacji I/O (Input/Output) od logiki programu. Zajmuje się on tylko i wyłącznie pobraniem argumentów z konsoli i wypisaniem wyniku wykonania pozostałej części kodu do pliku wyjściowego.

if __name__ == '__main__':
    parser = ArgumentParser('Make histogram showing popularity and total size '
                            'of files with given extension within a directory')
    parser.add_argument('operating_dir', metavar='operating_dir', type=str)
    parser.add_argument('output_file', metavar='output_file', type=str)
    args = parser.parse_args()

    with open(args.output_file, 'w+') as output_file:
        output_file.writelines(main(args.operating_dir))