Select Page

Python – Generator

by | Apr 7, 2022 | Python

Wer schon einmal eine etwas größere Datei, bspw. 1Gb, mit dem Editor VIM geöffnet hat, der weis, wie lange dies dauern kann. Das kommt daher, dass diese Datei zunächst komplett in den Arbeitsspeicher geladen werden muss. So ähnlich verhält es sich, wenn in Python eine Variable “befüllt” wird, welche anschließend Speicherplatz im Arbeitsspeicher belegt. Bei der heutigen Hardware stellen 1 Gb große Variablen kein Problem dar, aber was passiert wenn diese deutlich größer sind und zusätzlich eine gute Performance benötigt wird? In diesem Fall empfiehlt es sich, auf einen Generator zurückzugreifen. Ein Generator liefert lapidar gesagt die Ergebnisse einer Funktion “häppchenweise” und nicht als riesiges Stück zurück. Syntaktisch unterscheidet sich ein Generator von einer “normalen” Funktion nicht großartig, wie hier zu sehen:

def a_function():
  yield x
  yield y
  yield z

Bei der Funktionsdefinition wird lediglich das Schlüsselwort return durch yield ersetzt. Der große Unterschied zu einer “normalen” Funktion dabei ist, dass sich die Funktion mit yield nicht sofort beendet. Somit erhält man, vereinfacht gesagt, ein Objekt (Iterator) als Rückgabewert, über das man iterieren kann.

Zur besseren Veranschaulichung eines Generators werde ich eine exemplarische Datenbankabfrage aufzeigen:

Hinweis: Ich habe eine Beispieldatenbank genommen.

“normale” Funktion
SELECT-Statement und anschließende Rückgabe des gesamten Ergebnisses:

def select_list():
    result = []

    db = mysql.connector.connect(host='127.0.0.1',
                                 user='thomas',
                                 password="4nD3r5on",
                                 db='employees')

    cursor = db.cursor()

    cursor.execute('select * from employees')

    for row in cursor:
        result.append(row)

    return result

Die Funktion “select_list()” liefert eine Liste zurück:

print(select_list())

[...]
(10016,'1961-05-02','Kazuhito','Cappelletti','M','1995-01-27'),
(10017,'1958-07-06','Cristinel','Bouloucos','F','1993-08-03'),
(10018,'1954-06-19','Kazuhide','Peha','F','1987-04-03'),
[...]

“generator” Funktion
Im Gegensatz zum obigen Beispiel wird keine Liste mit den Werten befüllt, sondern jeder Wert einzeln durch yield zurückgegeben:

def select_generator():
    db = mysql.connector.connect(host='127.0.0.1',
                                 user='thomas',
                                 password="4nD3r5on",
                                 db='employees')

    cursor = db.cursor()

    cursor.execute('select * from employees')

    for row in cursor.fetchmany(1000):
        yield row

Die Funktion “select_generator()” liefert keine Liste zurück, sondern einen Generator:

print(select_generator())

<generator object select_generator at 0x10ba165d0>

Durch anschließendes Iterieren des Generatorsobjektes werden die gewünschten Werte ausgegeben:

for row in select_generator():
        print(row)

[...]
(10016,'1961-05-02','Kazuhito','Cappelletti','M','1995-01-27'),
(10017,'1958-07-06','Cristinel','Bouloucos','F','1993-08-03'),
(10018,'1954-06-19','Kazuhide','Peha','F','1987-04-03'),
[...]

Wo liegt nun der Vorteil eines Generators? Das zeigt sich erst bei Benchmark-Tests, welche im folgenden Beispiel die Performance der jeweiligen Funktion aufzeigt. Die SELECT-Statements sind bei beiden Beispielen dieselbigen, werden aber nicht ausgegeben, es wird nur eine Variable initialisiert:
select_list()

print('Memory (Before): {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))
    
t1_start = perf_counter()
test_list = select_list()
t1_stop = perf_counter()
    
print('Memory (After) : {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

print("Elapsed time: {0} seconds".format(t1_stop-t1_start))

select_generator()

print('Memory (Before): {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

t1_start = perf_counter()
test_generator = select_generator()
t1_stop = perf_counter()

print('Memory (After) : {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

print("Elapsed time: {0} seconds".format(t1_stop-t1_start))

select_list():

Memory (Before): 11.776 Mb
Memory (After) : 124.960768 Mb
Elapsed time: 8.311030800000001 seconds

 

select_generator():

Memory (Before): 11.698176 Mb
Memory (After) : 11.718656 Mb
Elapsed time: 4.869999999934649e-07 seconds

Die Zahlen sprechen für sich. Beim Generator bleibt der Verbrauch des Arbeitsspeichers so gut wie unverändert, da dieser beim Ausführen des Codes nicht “alle Werte speichert”, sondern jeden einzelnen Wert ab dem Schlüsselwort yield zurückgibt. Dadurch ergibt sich auch die immense Geschwindigkeit.
An dieser Stelle ist noch zu erwähnen, dass alle Vorteile eines Generators verloren gehen, wenn man diesen in ein Liste umwandelt.

Quelle: https://media.giphy.com/media/yUrUb9fYz6x7a/giphy.gif

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

More posts on the topic Python

Warum Python?

Als Systemintegrator in der IT-Welt stellt man früher oder später fest, dass man das Verstehen und Schreiben von Programmiersprachen nicht ignorieren kann. Je nachdem, in welcher IT-Bereich man tätig ist, wird man mit Programmiersprachen konfrontiert. Wir bei NETWAYS...

Kleine Weihnachtspyramide mit Raspberry Pi

Noch einmal schlafen und dann ist Weihnachten. Passend dazu haben wir von NETWAYS ein kleines Experiment zum selber Nachbauen vorbereitet. Es handelt sich um eine kleine Weihnachtspyramide, die sich mithilfe von Python Code und ein paar Bauteilen ein wenig bewegt und...

Ansible – Testing roles with Molecule

Ansible is a widely used and a powerful open-source configuration and deployment management tool. It can be used for simple repetitive daily tasks or complex application deployments, therefore Ansible is able to cover mostly any situation. If used in complex or...

Ansible – How to create reusable tasks

Ansible is known for its simplicity, lightweight footprint and flexibility to configure nearly any device in your infrastructure. Therefore it's used in large scale environments shared between teams or departments. Often tasks could be used in multiple playbooks to...