DATEC Datentechnik GmbH Python Programming Monthly Python Lösungen zu ausgewählten technischen Problemstellungen 3. Februar 2013 Autor: Dr. Franz Geiger Adresse: DATEC Datentechnik GmbH, Schmiedgasse 7, A-6890 Lustenau E-Mail: [email protected] Organisation: DATEC Datentechnik GmbH Druckdatum: 3. Februar 2013 Copyright (C) 1998 - 2013, DATEC Datentechnik GmbH Dieses Dokument ist urheberrechtlich geschützt. Alle Rechte, auch die der Übersetzung, des Nachdrucks und Vervielfältigung durch Kopieren oder Scannen sowie der Speicherung in Retrieval-Systemen des gesamten Dokumentes oder Teilen daraus, sind DATEC Datentechnik GmbH vorbehalten. Kein Teil des Dokumentes darf ohne schriftliche Genehmigung von DATEC Datentechnik GmbH in irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren), auch nicht für Zwecke der Unterrichtsgestaltung, reproduziert oder unter Verwendung elektronischer Systeme gespeichert, verarbeitet, vervielfältigt oder verbreitet werden. Die Weitergabe an Dritte ist nur mit ausdrücklicher Erlaubnis von DATEC Datentechnik GmbH gestattet. Alle Marken und Produktnamen sind Warenzeichen oder eingetragene Warenzeichen der jeweiligen Titelhalter. 2 Inhaltsverzeichnis 1 Februar 2013: Python Performance 1.1 Python Performance: Cython . . . . . . . . . . . . 1.1.1 Cython . . . . . . . . . . . . . . . . . . . . 1.1.1.1 Scaler . . . . . . . . . . . . . . . . 1.1.1.1.1 Python-Source-Code . . . 1.1.1.1.2 Cython-Source-Code . . . 1.1.1.1.3 Messergebnisse . . . . . . 1.1.1.2 Filter . . . . . . . . . . . . . . . . 1.1.2 File- bzw. Directory-Struktur für wahlfreien Python-/Cython-Code . . . . . . . . . . . . 2 Januar 2013 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf . . . . . . . 4 4 4 5 5 5 6 7 9 11 3 1 Februar 2013: Python Performance Sie haben sich mit Python beschäftigt und sind auch auf kritische Auseinandersetzungen mit dem Thema Python und Performance gestoßen. Aus der Sicht eines C- oder gar Assembler-Programmierers ist Python langsam. Aber aus wirtschaftlicher Sicht sind C- oder gar Assembler-Programmierer langsam und damit teuer: Teuer ist nicht die Laufzeit, teuer ist die Entwicklungszeit. Aus Sicht des Entwicklers sagt sich das so leicht. Es kann also nicht schaden, wenn man um Möglichkeiten weiß, Python-Programme performanter zu machen. Eine vergleichsweise wenig aufwändige Möglichkeit ist die der Verwendung von Cython-Code (s. http://cython.org/). 1.1 Python Performance: Cython 1.1.1 Cython Die Zeit eines Ingenieurs kostet mehr als CPU-Zeit, keine Frage. Es gibt aber Situationen, in denen Python-Code zu langsam ist. Es gibt eine Reihe von Möglichkeiten, Python-Programme schneller zu machen. Die, die am einfachsten ist, ist die Verwendung von Cython (s. http://cython.org/). Dabei geht man vor wie folgt: ∙ .py-File in ein gleichnamiges .pyx-File kopieren. ∙ Im .pyx-File Typdefinitionen vornehmen. ∙ Per import pyximport; pyximport.install() import scaling_cy as cy importieren - die Compilation erfolgt automatisch. Was möglich ist, wollen wir an zwei Beipielen ausprobieren. 4 1 Februar 2013: Python Performance 1.1.1.1 Scaler Das erste Beispiel ist ein Scaler, d.i. eine Klasse, die aus einem Signalwert einen Messwert macht. Im Grunde ist lediglich eine Multiplikation durchzuführen, mehr nicht. Keine Affäre also. Allerdings ist der Aufruf von Cython-Methoden effizienter als der von Python-Methoden. Das sollte entsprechend zu Buche schlagen. Aber der Reihe nach. 1.1.1.1.1 Python-Source-Code scaling_py.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 Das File, das den Python-Code enthält ist class _Scaler2 : """ Rechnet Signale in Messwerte um und umgekehrt . Basisklasse , Verwendung vorzugsweise ü ber Ableitungen . """ def __init__ ( self , p_signal , p_reading , factorS2R ): self . __p_signal = p_signal self . __p_reading = p_reading self . __factorS2R = factorS2R return def factorS2R ( self ): return self . __factorS2R def value4reading ( self , value4signal ): value4reading = value4signal * self . __factorS2R self . __p_reading . value_set ( value4reading ) return value4reading def value4signal ( self , value4reading ): value4signal = value4reading / self . __factorS2R self . __p_signal . value ( value4signal ) return value4signal class Scaler4Floats ( _Scaler2 ): """ """ def __init__ ( self , minS , maxS , dimS , minR , maxR , dimR , factorS2R ): minS = sys . float_info . min if minS is None else float ( minS ) maxS = sys . float_info . max if maxS is None else float ( maxS ) _Scaler2 . __init__ ( self , varbls . FVariableFloat ( 0. , minS , maxS , dimS ) , varbls . FVariableFloat ( 0. , minR , maxR , dimR ) , factorS2R ) return varbls.FVariableFloat sind Instanzen einer Klasse, die einen Wert, Minimalund Maximalwert sowie Dimensions-Strings (wie V“, mm“ usw.) hält. ” ” 1.1.1.1.2 Cython-Source-Code Der Cython-Code ist durch Kopieren und Einfügen von Typdefinitionen entstanden. Das File, das den Code enthält ist scaling_cy.pyx. 5 1 Februar 2013: Python Performance 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 cdef class _Scaler2 : """ Rechnet Signale in Messwerte um und umgekehrt . Basisklasse , Verwendung vorzugsweise ü ber Ableitungen . """ cdef : object __p_signal object __p_reading double __factorS2R def __init__ ( self , object p_signal , object p_reading , double factorS2R ): self . __p_signal = p_signal self . __p_reading = p_reading self . __factorS2R = factorS2R return cpdef double factorS2R ( self ): return self . __factorS2R cpdef double value4reading ( self , double value4signal ): value4reading = value4signal * self . __factorS2R self . __p_reading . value_set ( value4reading ) # Wir m ü ssen hier value_set () verwenden , # weil es auch eine Cython - Version der # hostenden Instanz gibt . return value4reading cpdef double value4signal ( self , double value4reading ): cdef double value4signal value4signal = value4reading / self . __factorS2R self . __p_signal . value ( value4signal ) return value4signal cdef class Scaler4Floats ( _Scaler2 ): """ """ def __init__ ( self , double minS , double maxS , unicode dimS , double minR , doubl minS = sys . float_info . min if minS is None else float ( minS ) maxS = sys . float_info . max if maxS is None else float ( maxS ) _Scaler2 . __init__ ( self , varbls . cy . FVariableFloat ( 0. , minS , maxS , dimS ) , varbls . cy . FVariableFloat ( 0. , minR , maxR , dimR ) , factorS2R ) return Der Unterschied zum Python-Code erschöpft sich in den cdef-s und den cpdefs. Die Definition der Typen erfolgt im Block cdef: object object double __p_signal __p_reading __factorS2R 1.1.1.1.3 Messergebnisse Um an Messergebnisse zu kommen, wurde folgender Code verwendet. Auch hier wird einiges verwendet, das nicht bekannt ist, wie z.B. Timer2 (dient der Zeitmessung). Das ist an dieser Stelle aber nicht 6 1 Februar 2013: Python Performance wichtig. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 n = 10000 scalerC = scaling . cy . Scaler4Floats ( 0 , 42 , u " V " , 0 , 4242 , u " bar " , 13) scalerP = scaling . py . Scaler4Floats ( 0 , 42 , u " V " , 0 , 4242 , u " bar " , 13) signal_values = [ random . randint ( - sys . maxint , sys . maxint ) for _ in range ( n )] # Werte vorab erzeugen , damit ihre # Erzeugung das Messergebnis nicht # beeinflusst . with Timer2 ( " Scaling by scaling . py " ) as tmr : for valueS in signal_values : reading_value = scalerP . value4reading ( valueS ) print tmr . results ( n ) with Timer2 ( " Scaling by scaling . cy " ) as tmr : for valueS in signal_values : reading_value = scalerC . value4reading ( valueS ) print tmr . results ( n ) Obiger Code führt endlich zu folgender Ausgabe test__basic_use (__main__._TESTCASE__Scaler2) ... Timer(): ’Scaling by scaling.py’ took 0.000 s = 0.004 ms = 3.775 us. Timer(): ’Scaling by scaling.cy’ took 0.000 s = 0.001 ms = 1.200 us. Wir haben also mit sehr wenig Aufwand eine dreimal kleinere Laufzeit erreicht. 1.1.1.2 Filter Bei Filtern sind ein paar Rechenoperationen mehr durchzuführen - zwar auch nicht wild, aber doch so, dass sich die statische Typisierung lohnen müsste. Als Filter wird ein Filter 1. Ordnung verwendet. Die Methode, die die effektive Berechnung durchführt, heißt execute() und präsentiert sich so: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def execute ( self ): """ Filter - Algorithmus . :: y = (1 - alpha ) y + alpha u , alpha = 1/(1 + T1 / Ts ) k k -1 k """ # Abbrevs # alpha = self . __alpha_value y_ = self . __y_ # Read input signal # u = self . input_value () # Cal . algorithm # y_ [0] = (1 - alpha )* y_ [ -1] + alpha * u # # Write output signal 7 1 Februar 2013: Python Performance 25 26 27 28 29 30 31 32 self . output_value ( y_ [0]) # Store values for next call # y_ [ -1] = y_ [0] return # We could write self . output_signal ( self . __y_ # here as well . Der Cython-Code sieht kaum anders aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 cpdef int execute ( self ): """ Filter - Algorithmus . :: y = (1 - alpha ) y + alpha u , alpha = 1/(1 + T1 / Ts ) k k -1 k """ # Abbrevs # cdef : double alpha double u double * y_ # Pointer to an array . alpha = self . __alpha_value y_ = self . __y_ # Read input signal # u = self . input_value () # Cal . algorithm # y_ [ 0] = (1 - alpha )* y_ [ 1] + alpha * u # Write output signal # self . output_value ( y_ [ 0]) # Store values for next call # y_ [ 1] = y_ [ 0] return # We could write self . output_signal ( self . __y_ # here as well . Und die Messergebnisse? Sehen so aus: test (__main__._TESTCASE__MovingAverageFilterExponentiallyWeighted) ... Timer(): ’PYFilter’ took 0.000 s = 0.018 ms = 17.607 us. Timer(): ’CY-Filter’ took 0.000 s = 0.002 ms = 1.560 us. Das ist Faktor 10 - nicht schlecht für das bisschen Aufwand. Nicht zu unterschätzen ist auch der Aufwand, der hier für die laufende Wartung eingespart werden kann. Sie brauchen nicht in eine C/C++-Umgebung wechseln, Sie können Ihre Python- und Cython-Sourcen gemeinsam verwalten. 8 1 Februar 2013: Python Performance 1.1.2 File- bzw. Directory-Struktur für wahlfreien Zugriff auf Python-/Cython-Code Wie kann man Cython-Code am besten in bestehende Apps einbinden und zwar transparent und umschaltbar? Alles was Sie dazu brauchen, ist eine entsprechende Directory-Struktur und eine konsistente Namensgebung für die involvierten Files. Hier also ein Vorschlag, skizziert anhand eines Packages, das teilweise in Cython realisiert werden soll. Der Cython-Code, um den es geht, befindet sich im einem File _common_cy.pyx, während die reine Python-Version des Codes in _common_py.py definiert ist.1 Code im Package, der auf Code in _common_py.py bzw. _common_cy.pyx zugriefen will, tut das über _common.py, das definiert ist wie folgt: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # # # # # # # # # # # # # # # # # # -* - coding : utf8 -* - # Copyright ( C ) by DATEC Datentechnik GmbH , A -6890 LUSTENAU , 1998 - 2013 This file is part of tau4 . tau4 is free software : you can redistribute it and / or modify it under the terms of the GNU General Public License as published by the Free Software Foundation , either version 3 of the License , or ( at your option ) any later version . tau4 is distributed in the hope that it will be useful , but WITHOUT ANY WARRANTY ; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the GNU General Public License for more details . You should have received a copy of the GNU General Public License along with tau4 . If not , see < http :// www . gnu . org / licenses / >. from __future__ import division import sys import tau4 from tau4 import tau4logging import import import try : if 33 34 35 36 37 38 39 40 41 42 43 _common_py as py pyximport ; pyximport . install () _common_cy as cy not tau4 . _settings . _TAU4 _USE_C Y_FOR _TAU4C OM : text = " Use of py code is forced in % s ! " % __file__ tau4logging . SysEventLog (). log_warning ( text , __file__ ) raise ImportError ( text ) text = " Cython code version is used . " from from from from from tau4 . tau4com . tbus . _common_cy tau4 . tau4com . tbus . _common_cy tau4 . tau4com . tbus . _common_cy tau4 . tau4com . tbus . _common_cy tau4 . tau4com . tbus . _common_cy 1 Der import import import import import _Message MessageAsync MessageAsync4NewValue MessageSynch MessageSynch4NewValue Unterstrich ’ ’ in _common deutet an, dass der zugehörige Code nur package-intern verwendet wird. 9 1 Februar 2013: Python Performance 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 from tau4 . tau4com . tbus . _common_cy import _SendMessage_ tau4logging . SysEventLog (). log_info ( text , __file__ ) except ImportError , e : text = " Pure Python code version is used . " print >> sys . stderr , " E R R O R : % s . % s " % (e , text ) from from from from from from tau4 . tau4com . tbus . _common_py tau4 . tau4com . tbus . _common_py tau4 . tau4com . tbus . _common_py tau4 . tau4com . tbus . _common_py tau4 . tau4com . tbus . _common_py tau4 . tau4com . tbus . _common_py import import import import import import _Message MessageAsync MessageAsync4NewValue MessageSynch MessageSynch4NewValue _SendMessage_ tau4logging . SysEventLog (). log_warning ( text , __file__ ) _ D y n a m i c l y C r e a t e d M e s s a g e C l a s s e s = {} Die File-Struktur ist also FILE.py FILE_PY.PY FILE_CY.PYX 10 2 Januar 2013 Hier finden Sie Problemstellungen aus der Praxis und die mittels PythonProgrammen realisierten Lösungen dazu. Eine Einführung in Python oder gar ein Tutorial fehlt, weil es keinen Sinn hat, die weiß Gott wievielte Einführung in die Programmierung mit Python zu schreiben. Im Laufe der Zeit wird sich im Anhang eine Link-Liste zu ausgewählten Themen entwickeln. Ansonsten bemühen Sie bitte Ihre Lieblingssuchmaschine. Richtig losgehen wird’s im Februar. Da werden wir uns mit Möglichkeiten beschäftigen, wie Programmteile intern miteinander kommunizieren können und dabei nur lose gekoppelt sind. Also, bis dann! 11