Python Protocol und mypy

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Hallo,
Ich spiele gerade etwas mit Python Protocol und mypy dabei ist mir aufgefallen das meine mypy Version, diesen minimalen Code ohne Fehler akzeptiert.

Code: Alles auswählen

import collections
import typing


class Command(typing.Protocol):
    def execute(self):
        ...


class CommandBatch(collections.UserList[Command]):
    def execute(self):
        for command in self:
            command.execute()


class Duck:
    def execute(self):
        print("duck quark")


class Swan:
    def execute(self):
        print("Swan QUARK")


def main():
    batch = CommandBatch[Duck]()

    batch.append(Duck())
    batch.append(Swan())
    batch.execute()


if __name__ == '__main__':
    main()

Meine Erwartung wäre aber gewesen, dass mypy bei batch.append(Swan()) eine Warnung oder einen Fehler generiert, da Swan zwar das Command Protocol erfüllt, aber die CommandBatch an Duck gebunden wurde. Was ist mein Denkfehler?

mypy Version: 0.991
python Version: 3.10.9
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich kenne mich mit mypy wenig aus, aber aus meiner Sicht ist CommandBatch doch schon vollstaendig typisiert fuer deinen Anwendungsfall hier. CommandBatch braeuchte fuer mein Verstaendnis eine Typvariable, aber die sehe ich nicht. Das [Duck] wuerde doch wenn gar nichts bedeuten, denn Duck ist ja nix (in Bezug auf einen Typ). Es implementiert implizit Command, aber das reicht doch nicht fuer ein Typkalkuel.
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

__deets__ hat geschrieben: Mittwoch 25. Januar 2023, 18:33 Ich kenne mich mit mypy wenig aus, aber aus meiner Sicht ist CommandBatch doch schon vollstaendig typisiert fuer deinen Anwendungsfall hier. CommandBatch braeuchte fuer mein Verstaendnis eine Typvariable.
Kling erstmal logisch, hast du eine Idee wie ich denn Code ändern kann, damit es für Typkalkuel, reicht und mypy hier eine Warnung/Fehlerausgibt.
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@imonbln: mypy prüft nix in `main()` solange Du da keine Typsignatur anbringst. Also den Rückgabetyp von `main()` mit `` -> None`` annotieren und schon meldet mypy auch einen Fehler bei ``CommandBatch[Duck]``:

Code: Alles auswählen

forum21.py:27: error: Value of type "Type[CommandBatch]" is not indexable
Found 1 error in 1 file (checked 1 source file)
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

__blackjack__ hat geschrieben: Mittwoch 25. Januar 2023, 18:47 @imonbln: mypy prüft nix in `main()` solange Du da keine Typsignatur anbringst. Also den Rückgabetyp von `main()` mit `` -> None`` annotieren und schon meldet mypy auch einen Fehler bei ``CommandBatch[Duck]``:
Guter Hinweis ist aber nur die halbe Wahrheit, selbst mit der annotieren Signatur bekomme ich noch nicht, was ich mir eigentlich wünsche.

Code: Alles auswählen

import collections
import typing


T= typing.TypeVar("T", covariant=True)


class Command(typing.Protocol[T]):
    def execute(self):
        ...


class CommandBatch(collections.UserList[Command[T]]):

    def execute(self):
        for command in self:
            command.execute()


class Duck:
    def execute(self):
        print("duck quark")


class Swan:
    def execute(self):
        print("Swan QUARK")


def main() -> None:
    batch = CommandBatch[Duck]()

    batch.append(Duck())
    batch.append(Swan())
    batch.execute()


if __name__ == '__main__':
    main()
Wenn ich das richtig verstehen müsste hier eigentlich covariant=False stehen, aber dann gibt es einen anderen Fehler, als erwartet. Irgendwie fehlt mir noch ein Stück Erkenntnis.

Code: Alles auswählen

bla.py:8: error: Invariant type variable "T" used in protocol where covariant one is expected  [misc]
Found 1 error in 1 file (checked 1 source file)
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na es scheint, als ob collections.UserList eben nur covariante Typen akzeptiert. Wieso das so ist kann ich leider auf die Schnelle nicht eruieren, wie auch immer mypy das ableitet.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wollte mal spielen, aber bei mir kommt der Fehler nicht. Wie genau provozierst du den?

Code: Alles auswählen

21:12 $ mypy /tmp/test.py 
Success: no issues found in 1 source file
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

ich muss hier nur das True in False ändern um den Fehler zu bekommen.

Code: Alles auswählen

T= typing.TypeVar("T", covariant=True)

Ich vermute das Protokoll nur kovariante Typen akzeptiert, aber den Grund kenne ich noch nicht.
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Ich habe es gefunden im PEP 544 wurde das verhalten, was ich gerne hätte abgelehnt. Es geht also per Sprachdefinition nicht.
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Mach mal hilft es das Problem ein wenig ruhen zu lassen, was ich wollte, geht doch. Man muss nur die TypeVar T an das Protokoll binden, dann kann mypy erkennen das Swan keine Duck ist. So erkennt mypy denn Fehler.

Code: Alles auswählen

import collections
import typing


T = typing.TypeVar("T", bound=Command)


class Command(typing.Protocol):
    def execute(self):
        ...


class CommandBatch(collections.UserList[T]):

    def execute(self):
        for command in self:
            command.execute()


class Duck:
    def execute(self):
        print("duck quark")


class Swan:
    def execute(self):
        print("Swan QUARK")


def main() -> None:
    batch = CommandBatch[Duck]()

    batch.append(Duck())
    batch.append(Swan())
    batch.execute()


if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich habe da nochmal drüber nachgedacht, und was du hier machst bedeutet doch, einfach nur zu sagen „es muss Duck sein“. Warum also der Umweg über ein Protokoll, das ja genau eine solche spezifische Typisierung vermeidet? Dann braucht’s doch das Protokoll garnicht.
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Stell dir einfach vor, es gibt noch eine zweite CommandBatch[Swan] und beide platzieren Bestellungen in Handelssysteme. Also ich finde es dann schon sinnvoll, wenn die CommandBatch generisch ist, aber der Statische Codeanalyse erkennt kann, dass ich gerade eine Duck in den Swan Handelsplatz senden will.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Den Teil verstehe ich. Nicht den, warum das über eine Protokoll läuft. Du sagst „hier ist eine Collection of Command“, aber dann soll batch eine Collection of Ducks sein. Mach es doch gleich eine Collection of Ducks?
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Aber dann bräuchte ich auch noch eine Collection of Swan, welche exakt den gleichen Code hat wie die Collection of Duck. Daher denke ich, dass es, als eine Art Template hier sinnvoll ist, das Protokoll zu verwenden.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Aber die muss doch keinem Protocol gehorchen, oder vertue ich mich da? Zumindest in C++ würde das gehen, da prüft der Compiler dann,w as der konkrete Typ kann. Aber es kann sein, das genau da mein Missverständnis liegt.
Antworten