typechecking decorator für python 2.x

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
khz
User
Beiträge: 38
Registriert: Freitag 3. August 2012, 22:47
Kontaktdaten:

Hallo,

habe hier einen decorator mit dem man dynamisches Typechecking für python 2.x machen kann.

Er prüft beim Funktions oder Methodenaufruf ob die tatsächlich beim aufruf übergebenen Parameter den spezifizierten Typen entsprechen und raised einen TypeError wenn nicht.
Als Parametertypen kann man jeden Python Typ (also insbesondere auch eigene Klassen) verwenden.
Auf der Github Seite sind noch ein paar Beispiele. ( oder auch hier: )

Projectname: IcanHasTypeCheck.
Projekt auf Github: https://github.com/pythononwheels/icanhastypecheck

Die Parameter Spezifikation wird über (sphinx style) docstrings gemacht. Ich bin dabei noch eine Alternative über eine Namenskonvention (Parameterprefix) zu erstellen.

Benutzung:Also mehr als das hinzufügen des Decorators und die Parameter spec (siehe Beispiel unten) ist es eigentlich nicht.

Kurzer Extrakt aus dem README zu Benutzung:
IcanHasTypeCheck (ICHTC)

is a small and easy to use decorator to enable dynamic type checking for python
method and function calls. Working ans tested for python 2.7 but should run in other 2.x as well.
Create an [issue](https://github.com/pythononwheels/icanh ... eck/issues) on github if you encounter any problems using it.
(Plans and an example implementation for python 3.x is included )

Function type specification is based on a naming/docstring convention for python < 3.x

Typechecking is implemented as a decorator that can be attached to any function or method and will
perform the according (dynamic) typechecking.
It will raise a TypeError if the arguments don't match the function specification.

Python 2.x
Since function annotations are not available in python 2.x the way I chose to implement typechecking for
python 2.x is a documentation convention for parameters based on [the info field lists of sphinx](http://sphinx-doc.org/markup/desc.html#info-field-lists). So even when you don't use typechecking you can use it to auto-generate a function documentation.
There is an alternative approach for those of you who don't like docstings in sphinx format which úses a naming convention. (See Alternatives below)

Syntax for python 2.x / docstrings:

Code: Alles auswählen

    from typecheck import typesafe

    @typesafe
    def foo(param_a, param_b):
        """ 
        :type param_a:    types.StringType
        :type param_b:    types.IntType
        :rtype:               types.BooleanType	
        """
        # Do Something 
        return True
You can use any python type.


Warum: Eigentlich bin ich über die Function Annotations von Python 3 gestolpert und damit beim Thema typechecking gelandet.
Ich benötige typechecking, da ich für PythonOnWheels gerade ein MongoDB backend hinzufüge und MongoDB selbst keinerlei typechecks macht sondern munter alles frisst was man ihr vorsetzt.
Das ist supergenial für schnelles arbeiten (keinerlei Schemadefinitionen) ist aber auf dauer etwas sehr unberechenbar ;)
Deshalb möchte ich typechecking zumindest im Werkzeugkasten haben. Die Benutzung bleibt ja jedem selbst überlassen.

Andere Arten der Benutzung als duch sphinx docstrings kann man rel. leicht über Anpassung der regex hinbekommen. (gerne bei github ergänzen).
http://www.pythononwheels.org
11to1 sparetime development and all pm.
lunar

@khz Es fällt mir schwer, den tieferen Sinn dieses Projekts zu verstehen. Offenbar dient es dazu, eine Ausnahme auszulösen, wenn der Typ eines Arguments nicht mit dem im Docstring deklarierten Typen übereinstimmt.

Ich finde schon die Verwendung von Dokumentation zur Spezifikation von Typen abenteuerlich. Dokumentation, auch in Docstrings, ist Freitext, und unterliegt a priori keinen syntaktischen Regeln. Die von Dir gezeigte Konvention ist nur eine der vielen verschiedenen Arten, Docstrings zu schreiben. Viele Projekte nutzen überhaupt kein besonderes Markup in Docstrings, sondern geben Typen im Fließtext an. Docstrings sind für Menschen da, nicht für Programme.

Für die Idee, gar eine Namenskonvention zu verwenden, sollte man Dich zu drei Monaten Windows-API-Programmierung verurteilen, damit Du ein Gefühl dafür bekommst, wie sich die ungarische Notation auf die Les- und Wartbarkeit von Quelltext auswirkt.

Die Typen einfach als Argumente an den Decorator zu übergeben, war Dir wohl zu uncool, oder?

Ganz allgemein frage ich mich aber, warum der "TypeError" dieser Bibliothek besser ist als der, den Python ohnehin auslöst, wenn Du einen Typen übergibst, den die Funktion nicht erwartet.
BlackJack

@khz: Wie bist Du auf die Idee gekommen das `types`-Modul zu verwenden? Dessen Dokumentation rät davon ab es zu benutzen weil es seit Python 2.2 für Typtests nicht mehr notwendig ist. Das stammt noch aus der grauen Vorzeit als `int`, `str`, und Co noch keine Typen waren sondern nur Funktionen. Das ist jetzt mehr als 10 Jahre her.

Das Vorhaben ist unpythonisch. Es unterläuft das „duck typing”. Statt den Typ zu prüfen würde man entweder davon ausgehen dass es ein kompatibler Typ ist, oder ihn umwandeln.

Und was den Aspekt der Programmfehler angeht: Da helfen Unittests mehr, denn die decken sowohl falsche Typen als auch semantische Fehler auf. Und das ohne das „duck typing” unmöglich zu machen.
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@khz: noch was zum DRY-Prinzip: 12 von 13 Zeilen des Python3.x-Zweigs kommen identisch im Python2.x-Zweig vor. Ein Prüfen ob spec.annotations existiert (per AttributeError) macht die seltsame Fallunterscheidung überflüssig.

PS: Nochetwas, was die Typprüfung ad absurdum führt: Wie teste ich auf irgendeinen iterierbaren Typ, irgendeinen, der Rechenoperationen unterstützt, usw...
khz
User
Beiträge: 38
Registriert: Freitag 3. August 2012, 22:47
Kontaktdaten:

Moin Leute,

@Blackjack: ich nehme das types Modul nur im Beispiel. Ansonsten kannst du, wie gesagt auch alle anderen Typen (Besipielsweise eigene Klassen o.ä.) verwenden.
Unittests helfen da nicht viel, da es hier um den Test zur Laufzeit geht. Nicht exemplarisch.

@Sirius: Der Pyhton 3 Zweig ist, wie geschrieben, bisher nur eine Rahmenimplementierung die aktuell noch brach liegt. Der Bedarf definitiv der Überarbeitung. Bzgl der Pytprüfung solltren Listen oder andere genauso gehen.

@lunar: Die Docstrings sind eben vielfach einfach schon da und wer sie hat braucht nichts zu ändern. Das format ist in dieaem Fall das sphinx format, welches (genauso) definiert ist. Ich definiere hier also nix neues. docstrings müssen für doc-tools wie sphinx sowieso bestimmte formate einhalten.
Die Übergabe an den Decorator ist eine gute Idee ( die ich einfach nicht hatte) die sicherlich besser ist als eine Namenskonvention der Parameter, da stimme ich dir voll zu.

Ob das nun pythonic ist ... ist mir wie geschrieben klar, das es i.d.R mit Pyhton eherum Ducktyping als Typrüfung geht. Aber wenn man wirklich einen echten Typecheck braucht und eben nicht möchte das
andere Typen (die sich gleich verhalten) akzeptiert werde, dann ist das hier eben ein Lösungsansatz.
Da schmeisst dann eben python selbst auch _keinen_ TypeError.

Den Anlass für mich mit MongoDB habe ich ja oben aufgeführt .
http://www.pythononwheels.org
11to1 sparetime development and all pm.
khz
User
Beiträge: 38
Registriert: Freitag 3. August 2012, 22:47
Kontaktdaten:

So,

man kann die Parameter-Type Spezifikation jetzt auch als Argument an den decorator übergeben.
(In Form eines Dictionaries)

Beispiel:

Code: Alles auswählen

  
    @typesafe( { "param_a" : "types.StringType", 
                 "param_b" : "types.IntType", 
                 "rtype" : "types.BooleanType" }
               )
    def foo(param_a, param_b):
        """ Some Docstring Info	 """
        # Do Something 
        return True    
Das war ein guter Vorschlag und das mit dem prefix wahrlich nicht die beste Idee.
http://www.pythononwheels.org
11to1 sparetime development and all pm.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Ich verstehe immer noch nicht, warum du erstens ein Dictionary mit Strings benutzt, zweitens das types Modul verwendest (darauf hat BlackJack schon hingewiesen) und drittens, warum du überhaupt versuchst, in einer dunamisch typisierten Sprache statische Typisierung einzuführen. Du arbeitest da gegen die Sprache. Wenn dir dynamische Typisierung nicht gefällt (was ich gut nachvollziehen könnte, beide Systeme haben ihre Vor- und Nachteile), dann ist es vielleicht besser, sich nach einer statisch typisierten Sprache umzuschauen.

Ich habe vor langer Zeit übrigens mal so etwas ähnliches geschrieben, als Übung für Dekoraten: https://github.com/derdon/hodgepodge/bl ... rappers.py. Allerdings wäre ich auch damals nie auf die Idee gekommen, so etwas ernsthaft irgendwo einsetzen zu wollen.
lunar

@khz Uhm, nun ja, ich würde sagen, es wird nicht besser. Ich nehme an, Du verwendest "types", obwohl BlackJack Dir davon abgeraten hat, weil Du nicht wusstest, wie Du "str" oder andere eingebaute Typen über ihren Namen auflösen kannst, nicht wahr?

Ist Dir da wirklich nichts besseres eingefallen, als Typen per Zeichenketten(!) anzugeben?!

Wieso machst Du Dir nicht die Ausdrucksstärke von Python zu nutze, und gibst die Typen einfach direkt an? Dann vielleicht auch nicht als Wörterbuch, sondern direkt in der Signatur des Dekorators. Also in etwa so:

Code: Alles auswählen

@typesafe(str, int, cookit=bool).returns(bool)
def foo(spam, eggs, cookit=False):
    return True
Ist das nicht die einzig offensichtliche Art, einen solchen Dekorator zu implementieren?

@derdon Ich denke, Du tust dem OP Unrecht, ihm vorzuwerfen, gegen die Sprache zu arbeiten. Ist ja nicht gesagt, dass der OP jetzt jede Funktion mit diesem Dekorator versieht, und ich glaube auch nicht, dass der OP das wirklich vorhat.

Ich entnehme den Aussagen des OP vielmehr, dass er diese Bibliothek bei der Arbeit mit einer Datenbank einsetzt, die auf exakten Typen besteht. In derartigen Fällen ist dieser Dekorator durchaus sinnvoll. Stell Dir eine Serialisierungsschnittstelle vor, die auf exakten Typen besteht, sprich wirklich nur "list" serialisieren kann, nicht aber Implementierung von "collections.MutableSequence". Dann möchte man vielleicht wirklich vorher prüfen, ob der übergebene Typ stimmt, anstatt auf kryptische Fehler der Datenbank zu warten, oder gar das Risiko einer stillschweigend fehlerhaften Serialisierung einzugehen.

Ein anderer Einsatzzweck wäre die Arbeit mit nativen Bibliotheken, die ebenfalls auf exakten Typen bestehen, e.g. Qt mit seinen Signalen, die ebenfalls strikt statisch typisiert sind (oder zumindest waren, keine Ahnung ob das bei der neuen API noch immer der Fall ist). Auch da möchte man vielleicht lieber mit diesem Dekorator eine Typprüfung und damit eine Ausnahme erzwingen, anstatt zur Laufzeit Ereignisse stillschweigend zu verpassen.
khz
User
Beiträge: 38
Registriert: Freitag 3. August 2012, 22:47
Kontaktdaten:

@lunar: klar, richtig, bei Übergabe als decorator Argument kann ich die Typen auch direkt angeben. Das ist da der bessere Weg. Manchmal ist man betriebsblind...
http://www.pythononwheels.org
11to1 sparetime development and all pm.
khz
User
Beiträge: 38
Registriert: Freitag 3. August 2012, 22:47
Kontaktdaten:

geändert.

Jetzt kann man die Typen direkt als Parameter angeben. (Nicht nur als Namen)

Form:

Code: Alles auswählen

@typesafe( { "param_a" : str, 
                 "param_b" : types.IntType, 
                 "param_c" : own_module.OwnType
                 "rtype"   : bool }
              )
    def foo(param_a, param_b, param_c):
        """ Some Docstring Info      """
        # Do Something 
        return True
jetzt besser ;)
http://www.pythononwheels.org
11to1 sparetime development and all pm.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Warum müssen bei dir alle Parameternamen noch einmal aufgeführt werden? Das ist doch nur eine unnütze Fehlerquelle und führt zu zusätzlichem Code. lunars Beispiel zeigt doch eigentlich recht gut, wie es im Idealfall aussieht.
Das Leben ist wie ein Tennisball.
lunar

khz hat geschrieben:jetzt besser ;)
Kaum.
khz
User
Beiträge: 38
Registriert: Freitag 3. August 2012, 22:47
Kontaktdaten:

@EyDu: weil nicht jeder Parameter gecheckt werden muss.

@lunar: schlechte Laune?? ;)
http://www.pythononwheels.org
11to1 sparetime development and all pm.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Und was spricht dann gegen ein einfaches 'None', wenn der Typ nicht geprüft werden soll? Namen doppelt sind einfach überflüssig, außer du willst nur einen Bruchteil der Parameter testen. Und wenn man schon Namen angibt, dann benutzt man doch wenigstens Keyword Arguments und nicht ein Dictionary als einzigen Parameter. In deinem Fall führt dass auch noch dazu, dass ein Schlüssel "rtype" reserviert ist. Wie gesagt: lunar hat schon den richtigen Weg gezeigt und ist deinem Ansatz in allen Punkten überlegen.
Das Leben ist wie ein Tennisball.
lunar

khz hat geschrieben:@lunar: schlechte Laune?? ;)
Nein. Ich habe Dir bereits erklärt, was ich für die richtige API halte, und ich hielt es für unnötig, all das zu wiederholen.
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Mein erster Gedanke beim Lesen war auch, »warum muß ich jetzt die Parameter in einem Dictionary angeben?«. Für normale Positionsargumente nimmt man eben None oder True oder False oder ANY um anzuzeigen, dass der Typ egal ist, bzw. man kann, wenn man lieber mit Parameternamen arbeitet auch Keywordargumente benutzen.
Jetzt bin ich noch auf die ganz verwegene Idee gekommen, warum eigentlich nicht noch Funktionen zulassen, die mit denen man beliebige Bedingungen an die Parameter stellen kann:

Code: Alles auswählen

@checkparams(lambda param_a: 0<=param_a<=10, None, str)
def func(param_a, param_b, param_c):
    do_sth
lunar

@Sirius3 Das lässt sich nicht so ohne weiteres implementieren, weil man nicht zwangsläufig unterscheiden kann zwischen einem „normalen“ aufrufbaren Objekt, sprich einer Funktion, und einem aufrufbaren Objekt, dass einen vererbbaren Typen darstellt. Vergiss nicht, dass Klassen auch aufrufbare Objekte sind.
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@lunar: die Unterscheidung ist relativ einfach:
a) alles was eine __base__ hat, ist ein Typ und kann mit isinstance geprüft werden.
b) alles was __call__-able ist, wird mit dem Parameter aufgerufen und auf Wahrheit geprüft.
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Sirius3 hat geschrieben:a) alles was eine __base__ hat, ist ein Typ und kann mit isinstance geprüft werden.
Wobei man damit (in Python 2) oldstyle-Klassen ausschließen würde:

Code: Alles auswählen

>>> class Foo: pass
... 
>>> Foo.__base__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class Foo has no attribute '__base__'
>>> class Bar(object): pass
... 
>>> Bar.__base__
<type 'object'>
Wenn schon, würde ich lieber inspect.isclass verwenden.
BlackJack

@Sirius3: Aber damit schliesst man doch Exemplare aus die zwar `__base__` haben, aber auch eine `__call__`-Implementierung die zum Testen gedacht ist. Zumal ich nicht sehe dass die Sprachdefinition verbietet das Funktionsobjekte `__base__` haben dürfen. Das heisst das könnten zukünftige Versionen durchaus haben, oder auch alternative Implementierungen zu CPython.
Antworten