Seite 1 von 1

Pseudoüberladung

Verfasst: Sonntag 14. April 2013, 22:05
von bwbg
Ich möchte für eine __init__-Funktion zwei mögliche Signaturen anbieten:
  • GLShader(shader_type:GLenum, source:string)
  • GLShader(identifier:GLuint)
Mein erster Gedanke war, dieses mittels *args zu realisieren:

Code: Alles auswählen

    def __init__(self, *args):
        assert len(args) in (1, 2)
        super().__init__()
        if len(args) == 1:
            self.identifier = args[0]
        else:
            self.identifier = _create_shader(shader_type)
            self._set_shader_source(source)
            self._compile_shader()
Ich werde das Gefühl jedoch nicht los, dass das nicht ganz im Sinne des (Python-)Erfinders ist. Auf statische Factories wollte ich gerne verzichten, da ich dann eine __init__ mit drei Argumenten bereitstellen müsste und am Ende doch wieder diese abprüfen müsste.

Wie geht man dies in der Regel pythonisch an?

Grüße ... bwbg

Re: Pseudoüberladung

Verfasst: Sonntag 14. April 2013, 22:15
von BlackJack
@bwbg: Ich weiss nicht ob's pythonisch ist, oder überhaupt ein guter API-Entwurf, aber:

Code: Alles auswählen

    def __init__(self, type_or_identifier, source=None):
        super().__init__()
        if source is None:
            self.identifier = type_or_identifier
        else:
            self.identifier = _create_shader(type_or_identifier)
            self._set_shader_source(source)
            self._compile_shader()
Wobei ich nicht sehe wieso man bei einer Klassenmethode eine `__init__()` mit drei Argumenten braucht‽

Edit:

Code: Alles auswählen

    def __init__(self, identifier):
        super().__init__()
        self.identifier = identifier

    @classmethod
    def from_source(cls, shader_type, source):
        result = cls(cls._create_shader(shader_type))
        result._set_shader_source(source)
        result._compile_shader()
        return result

Re: Pseudoüberladung

Verfasst: Sonntag 14. April 2013, 22:32
von bwbg
Ich wusste doch, dass ich mich auf Dich verlassen kann ;)

classmethod habe ich ganz verdrängt. Empfinde ich am saubersten und kommt der CAPI noch am nächsten.

Grüße ... bwbg

Re: Pseudoüberladung

Verfasst: Montag 15. April 2013, 07:22
von snafu
Kann es sein, dass `GLShader(shader_type:GLenum, source:string)` den Shader von grundauf neu erstellt und `GLShader(identifier:GLuint)` anhand des Identifiers einen Shader aus dem Cache nutzt? In diesem Fall würde ich eher eine `Shader.for_id()` anbieten und die erstgenannte Signatur für `__init__()` nehmen. Sähe zumindest für mich etwas sauberer aus.

Re: Pseudoüberladung

Verfasst: Montag 15. April 2013, 15:44
von bwbg
Ich habe mich nun für die folgende Strukturierung entschieden:

Code: Alles auswählen

class GLObject:
    def __init__(self, identifier):
        self.identifier = identifier
        
    @property
    def _as_parameter_(self): # For convenience with ctypes
        return self.identifier
        
    @classmethod
    def for_id(cls, identifier):
        result =  cls.__new__()
        result.identifier = identifier
        return result

        
class GLShader(GLObject):
    def __init__(self, shader_type, source):
        pass

    # ...
Da alle (mir bisher bekannten) Objekte in OpenGL sich mittels eines GLuint/identifiers verwenden lassen, habe ich eine entsprechende Basisklasse definiert, welche eine Klassenmethode for_id bereitstellt.

Im Grunde werden alle (künftigen) von GLObject abgeleiteten Klassen lediglich den identifier als inneren Zustand besitzen. Alle weiteren Funktionalitäten plane ich durch properties und Methoden bereitzustellen.

Grüße ... bwbg

PS. Ja, ich weiß, dass ess Projekte wie PyOpenGL und pyglet gibt. Es ist ein Lernprojekt, um mich mit OpenGL und nebenbei mit ctypes vertraut zu machen ;)

Re: Pseudoüberladung

Verfasst: Montag 15. April 2013, 21:49
von snafu
Ich kenne mich mit OpenGL nicht wirklich aus, würde aber erwarten, dass bei Angabe des Identifiers eher ein bereits erstelltes Objekt aus einem Cache zurückgeliefert wird. Als Minimum würde ich zumindest erwarten, dass ich nicht munter neue Typen für bereits verwendete Identifier angeben kann. Du könntest dir bereits erstellte Objekte evtl in einem *Klassen*attribut von `GLObject` als Dictionary merken und `.from_id()` diese zurückliefern lassen. Letzteres würde ich dann auch eher als `@staticmethod` definieren. Dein jetziger Code sieht mehr nach nem Hack aus, der irgendwie die Tatsache umschiffen muss, dass `@classmethod`s eigentlich so gedacht sind, dass tatsächlich Objekte der jeweiligen Klasse erzeugt werden und nicht etwa des Basistyps. Ich lehne mich mal soweit aus dem Fenster, dass ich das Design in seiner jetzigen Form schon fast als kaputt ansehen würde. Außer natürlich, falls ich den Sinn dieser Identifier völlig fehlinterpretiert habe. Dann will ich nichts gesagt haben... :)

EDIT: Um's nochmal deutlich zu sagen: Falls es darum geht, neu erstellte Objekte mit Kennungen zu versehen, dann würde ich in den Signaturen der verschiedenen Typen durchaus den `identifier` als letztes Argument mitschleppen. Ich weiß z.B. von Qt, dass man da auch in vielen Signaturen ein `parent`-Argument am Ende findet (auch wenn ich hier sicherlich Äpfel mit Birnen vergleiche). Wenn aber auf bereits erstellte Objekte über ihren jeweiligen Identifier zugegriffen werden soll, dann wäre eine statische Methode - oder sogar eine simple Funktion auf Modulebene - besser. Ich hoffe einfach mal, wir reden jetzt nicht zu sehr aneinander vorbei. Vielleicht willst du ja ein paar Erläuterungen zur Verwendungsweise dieser Identifier geben...

Re: Pseudoüberladung

Verfasst: Montag 15. April 2013, 22:34
von snafu
Hier mal ein Beispiel, das in etwa zeigen soll, wie ich das meinte:

Code: Alles auswählen

class GLObject(object):
    _cache = {}

    def _register(self, identifier):
        if identifier is not None:
            self._cache[identifier] = self

    @classmethod
    def from_id(cls, identifier):
        obj = cls._cache[identifier]
        if not isinstance(obj, cls):
            msg = 'requested object is not of type %r' % cls.__name__
            raise TypeError(msg)
        return obj


class GLShader(GLObject):
    def __init__(self, shader_type, source, identifier=None):
        self._register(identifier)
        self.shader_type = shader_type
        self.source = source


class GLFoo(GLObject):
    def __init__(self, foo, bar, identifier=None):
        self._register(identifier)
        self.foo = foo
        self.bar = bar
Anwendung:

Code: Alles auswählen

>>> from gltest import GLShader, GLFoo
>>> shader = GLShader("DEFAULT_SHADER", "source", "myshader")
>>> shader_from_id = GLShader.from_id("myshader")
>>> shader_from_id is shader
True
>>> shader_from_id = GLFoo.from_id("myshader")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "gltest.py", line 13, in from_id
    raise TypeError(msg)
TypeError: requested object is not of type 'GLFoo'
Wie du siehst, simuliere ich hier mehr oder weniger statische Typisierung. Viellicht möchte man das bei einem Wrapper ja so haben. Es ist natürlich auch immer die Frage, wie weit so ein Wrapper gehen sollte in Bezug auf sein eigenes Verhalten...

Re: Pseudoüberladung

Verfasst: Montag 15. April 2013, 23:07
von bwbg
Vielleicht habe ich den Begriff "Identifier" auch einfach falsch gewählt. Im Grunde ist es nichts weiter als eine Ganzzahl, welche ein Objekt im OpenGL-Kontext kennzeichnet (ähnlich einer Speicheradresse oder ein Handle).

Eine statische Typisierung, wie von Dir angemerkt benötige ich auf der Python-Seite nicht. Die OpenGL-Funktionen prüfen ihrerseits, ob der Identifier/Adresse/Handle für die jeweilige Operation gültig ist.

Frei nach dem Motto "Quelltext sagt mehr als tausend Worte", ein erster ausbaufähiger Gehversuch:

https://gist.github.com/anonymous/5391641

Es ist schon spät und meine bessere Hälfte wartet ... :wink:

Grüße ... bwbg

Re: Pseudoüberladung

Verfasst: Montag 15. April 2013, 23:31
von snafu
Na gut, das sieht ja schon ein bißchen anders aus. Ich würde jedoch den `ctypes`-spezifischen Code in ein gesondertes Modul packen. Dieses Modul würde dann die unterste Schicht darstellen, welche den tatsächlichen Übergang zwischen Python und C realisiert. Darauf aufbauend würde ich die Abstraktionen machen.

EDIT: `GLError.raise_if_any()` finde ich als Aufruf zu kompliziert. Da würde ich wohl eher eine Funktion auf Modulebene namens `check_errors()` oder so verwenden. `check_errors()` könnte dann seinerseits wieder eine Liste von zu erwartenden Fehlertypen annehmen, womit man sich die Angabe als Kommentar sparen könnte. Im Grunde läuft das aber schon wieder auf Exceptions hinaus, die man einfach mittels `try`-`except` behandelt. Diese würden dann von dem oben vorschlagenen auf `ctypes`-Ebene agierenden Modul geworfen werden.