pixel
Seite wählen

Python – Generator

von | 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

Philipp Dorschner
Philipp Dorschner
Technical Service Manager

Nach seiner Ausbildung zum Fachinformatiker bei der NETWAYS Professional Services GmbH wuchs das Interesse an Development und organisatorischen Themen. Heute unterstützt Philipp die Kollegen aus PS-Services als hybrider Mitarbeiter. Fünfzig Prozent seiner Zeit als Technical Service Manager und die anderen fünfzig Prozent als interner Entwickler. Als Ausgleich zu seiner Arbeit im Büro verbringt er seine Freizeit meistens beim Sport oder trifft sich mit Freunden.
Mehr Beiträge zum Thema Python

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...

Ansible – AWX|Tower State handling on Workflows

The Ansible Tower or its upstream AWX provides an easy to use GUI to handle Ansible tasks and schedules. Playbooks are configured as templates and as the name suggests, they can be modified to the needs, extended by variables, a survey or tags. Furthermore those...

Ansible – Loop over multiple tasks

The last time I wrote about Ansible and the possibility to use blocks to group multiple tasks. Which you can read here. Sadly this feature does not work with loop, so there is no clean way to loop over multiple tasks in a play without writing the same loop statement...