Generika / Vorlagen in Python?


83

Wie geht Python mit generischen Szenarien / Szenarien vom Typ Vorlage um? Angenommen, ich möchte eine externe Datei "BinaryTree.py" erstellen und Binärbäume verarbeiten lassen, jedoch für jeden Datentyp.

So könnte ich ihm den Typ eines benutzerdefinierten Objekts übergeben und einen Binärbaum dieses Objekts haben. Wie geht das in Python?


14
Python hat Entenvorlagen
David Heffernan

Antworten:


82

Python verwendet die Enten-Typisierung , sodass keine spezielle Syntax erforderlich ist, um mehrere Typen zu verarbeiten.

Wenn Sie einen C ++ - Hintergrund haben, werden Sie sich daran erinnern, dass TSie diesen Typ Tin der Vorlage verwenden können , solange die in der Vorlagenfunktion / -klasse verwendeten Operationen für einen Typ (auf Syntaxebene) definiert sind .

Im Grunde funktioniert es also genauso:

  1. Definieren Sie einen Vertrag für die Art der Elemente, die Sie in den Binärbaum einfügen möchten.
  2. diesen Vertrag dokumentieren (dh in der Klassendokumentation)
  3. Implementieren Sie den Binärbaum nur mit den im Vertrag angegebenen Operationen
  4. genießen

Sie werden jedoch feststellen, dass Sie nicht erzwingen können, dass ein Binärbaum nur Elemente des ausgewählten Typs enthält, es sei denn, Sie schreiben eine explizite Typprüfung (von der normalerweise abgeraten wird).


5
André, ich würde gerne verstehen, warum explizite Typprüfungen in Python normalerweise nicht empfohlen werden. Ich bin verwirrt, weil es so aussieht, als ob es sich um eine dynamisch typisierte Sprache handelt. Wir könnten in große Schwierigkeiten geraten, wenn wir nicht garantieren können, welche Typen in die Funktion kommen werden. Aber andererseits bin ich sehr neu in Python. :-)
ScottEdwards2000

2
@ ScottEdwards2000 Sie können implizite Typprüfung mit Typhinweisen in PEP 484 und einer Typprüfung haben
noɥʇʎԀʎzɐɹƆ

6
Aus der Sicht des Python-Puristen ist Python eine dynamische Sprache, und die Typisierung von Enten ist das Paradigma. Das heißt, die Typensicherheit wird als "nicht pythonisch" eingestuft. Dies war etwas, das für mich für eine Weile schwer akzeptabel zu finden war, da ich stark an C # interessiert bin. Einerseits halte ich Typensicherheit für eine Notwendigkeit. Da ich die Skalen zwischen der .Net-Welt und dem Pythonic-Paradigma ausgewogen habe, habe ich akzeptiert, dass Typensicherheit wirklich ein Schnuller ist, und wenn ich muss, muss ich nur if isintance(o, t):oder if not isinstance(o, t):... ziemlich einfach tun .
IAbstract

2
Danke Kommentatoren, tolle Antworten. Nachdem ich sie gelesen hatte, wurde mir klar, dass ich wirklich nur eine Typprüfung möchte, um meine eigenen Fehler zu erkennen. Also werde ich nur implizite Typprüfung verwenden.
ScottEdwards2000

1
Ich denke, viele Pythonisten verpassen den Punkt - Generika sind eine Möglichkeit, gleichzeitig Freiheit und Sicherheit zu bieten. Selbst wenn Generika weggelassen werden und nur typisierte Parameter verwendet werden, weiß der Funktionsschreiber, dass er seinen Code so ändern kann, dass er jede von der Klasse bereitgestellte Methode verwendet. Wenn Sie beim Entenschreiben eine Methode verwenden, die Sie zuvor nicht verwendet haben, haben Sie plötzlich die Definition der Ente geändert, und die Dinge werden wahrscheinlich kaputt gehen.
Ken Williams

45

Die anderen Antworten sind völlig in Ordnung:

  • Zur Unterstützung von Generika in Python ist keine spezielle Syntax erforderlich
  • Python verwendet die Ententypisierung, wie von André hervorgehoben .

Wenn Sie jedoch weiterhin eine typisierte Variante wünschen , gibt es seit Python 3.5 eine integrierte Lösung.

Generische Klassen :

from typing import TypeVar, Generic

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        # Create an empty list with items of type T
        self.items: List[T] = []

    def push(self, item: T) -> None:
        self.items.append(item)

    def pop(self) -> T:
        return self.items.pop()

    def empty(self) -> bool:
        return not self.items
# Construct an empty Stack[int] instance
stack = Stack[int]()
stack.push(2)
stack.pop()
stack.push('x')        # Type error

Allgemeine Funktionen:

from typing import TypeVar, Sequence

T = TypeVar('T')      # Declare type variable

def first(seq: Sequence[T]) -> T:
    return seq[0]

def last(seq: Sequence[T]) -> T:
    return seq[-1]


n = first([1, 2, 3])  # n has type int.

Referenz: mypy Dokumentation über Generika .


18

Tatsächlich können Sie jetzt Generika in Python 3.5+ verwenden. Siehe PEP-484 und Dokumentation zur Typisierungsbibliothek .

Nach meiner Praxis ist es nicht sehr nahtlos und klar, insbesondere für diejenigen, die mit Java Generics vertraut sind, aber dennoch verwendbar.


10
Das sieht nach einer billigen Abzocke von Generika aus. Es ist, als hätte jemand Generika bekommen, sie in einen Mixer gegeben, laufen lassen und vergessen, bis der Mixermotor durchgebrannt ist, und dann 2 Tage später herausgenommen und gesagt: "Hey, wir haben Generika".
Jeder

3
Das sind "Typhinweise", sie haben nichts mit Generika zu tun.
Wool.in.Silver

Das gleiche gilt für Typoskript, aber dort funktioniert es wie in Java (syntaktisch). Generika in diesen Sprachen sind nur Tipphinweise
Davide

11

Nachdem ich mir einige gute Gedanken über die Herstellung generischer Typen in Python gemacht hatte, suchte ich nach anderen, die die gleiche Idee hatten, aber keine fanden. Hier ist es also. Ich habe das ausprobiert und es funktioniert gut. Es ermöglicht uns, unsere Typen in Python zu parametrisieren.

class List( type ):

    def __new__(type_ref, member_type):

        class List(list):

            def append(self, member):
                if not isinstance(member, member_type):
                    raise TypeError('Attempted to append a "{0}" to a "{1}" which only takes a "{2}"'.format(
                        type(member).__name__,
                        type(self).__name__,
                        member_type.__name__ 
                    ))

                    list.append(self, member)

        return List 

Sie können jetzt Typen von diesem generischen Typ ableiten.

class TestMember:
        pass

class TestList(List(TestMember)):

    def __init__(self):
        super().__init__()


test_list = TestList()
test_list.append(TestMember())
test_list.append('test') # This line will raise an exception

Diese Lösung ist simpel und hat ihre Grenzen. Jedes Mal, wenn Sie einen generischen Typ erstellen, wird ein neuer Typ erstellt. Somit würden mehrere Klassen, die List( str )als Eltern erben, von zwei getrennten Klassen erben. Um dies zu überwinden, müssen Sie ein Diktat erstellen, um die verschiedenen Formen der inneren Klasse zu speichern und die zuvor erstellte innere Klasse zurückzugeben, anstatt eine neue zu erstellen. Dies würde verhindern, dass doppelte Typen mit denselben Parametern erstellt werden. Bei Interesse kann mit Dekorateuren und / oder Metaklassen eine elegantere Lösung gefunden werden.


Können Sie näher erläutern, wie das Diktat im obigen Beispiel verwendet werden kann? Hast du einen Ausschnitt dafür, entweder in Git oder so? Vielen Dank ..
Gnomeria

Ich habe kein Beispiel und es könnte momentan etwas zeitaufwändig sein. Die Prinzipien sind jedoch nicht so schwierig. Das Diktat fungiert als Cache. Wenn die neue Klasse erstellt wird, müssen die Typparameter überprüft werden, um einen Bezeichner für diese Typ- und Parameterkonfiguration zu erstellen. Dann kann es als Schlüssel in einem Diktat verwendet werden, um die zuvor vorhandene Klasse nachzuschlagen. Auf diese Weise wird diese eine Klasse immer wieder verwendet.
Ché on The Scene

Vielen Dank für die Inspiration - siehe meine Antwort für eine Erweiterung dieser Technik mit Metaklassen
Eric

4

Da Python dynamisch typisiert wird, spielen die Objekttypen in vielen Fällen keine Rolle. Es ist eine bessere Idee, etwas zu akzeptieren.

Um zu demonstrieren, was ich meine, akzeptiert diese Baumklasse alles für ihre beiden Zweige:

class BinaryTree:
    def __init__(self, left, right):
        self.left, self.right = left, right

Und es könnte so verwendet werden:

branch1 = BinaryTree(1,2)
myitem = MyClass()
branch2 = BinaryTree(myitem, None)
tree = BinaryTree(branch1, branch2)

6
Die Arten von Objekten spielen eine Rolle. Wenn Sie die Elemente des Containers durchlaufen und foofür jedes Objekt eine Methode aufrufen , ist es eine schlechte Idee, Zeichenfolgen in den Container einzufügen. Es ist keine bessere Idee, etwas zu akzeptieren . Es ist jedoch zweckmäßig , nicht zu verlangen, dass alle Objekte im Container von der Klasse stammen HasAFooMethod.
André Caron

1
Eigentlich ist die Art tut Angelegenheit: es bestellt hat.
Fred Foo

Oh ok. Ich habe das damals falsch verstanden.
Andrea

3

Da Python dynamisch typisiert wird, ist dies sehr einfach. Tatsächlich müssten Sie zusätzliche Arbeit für Ihre BinaryTree-Klasse leisten, um mit keinem Datentyp zu arbeiten.

Wenn Sie beispielsweise möchten, dass die Schlüsselwerte, mit denen das Objekt in dem Baum platziert wird, über eine Methode, wie key()Sie sie nur key()für die Objekte aufrufen, innerhalb des Objekts verfügbar sind. Zum Beispiel:

class BinaryTree(object):

    def insert(self, object_to_insert):
        key = object_to_insert.key()

Beachten Sie, dass Sie nie definieren müssen, um welche Art von Klasse object_to_insert es sich handelt. Solange es eine key()Methode gibt, wird es funktionieren.

Die Ausnahme ist, wenn Sie möchten, dass es mit grundlegenden Datentypen wie Zeichenfolgen oder Ganzzahlen funktioniert. Sie müssen sie in eine Klasse einschließen, damit sie mit Ihrem generischen BinaryTree funktionieren. Wenn das zu schwer klingt und Sie die zusätzliche Effizienz wünschen, nur Strings zu speichern, ist Python leider nicht gut darin.


7
Im Gegenteil: Alle Datentypen sind Objekte in Python. Sie müssen nicht verpackt werden (wie in Java mit IntegerBoxen / Unboxing).
George Hilliard

2

Hier ist eine Variante dieser Antwort , die Metaklassen verwendet, um die unordentliche Syntax zu vermeiden, und die Syntax typing-style verwendet List[int]:

class template(type):
    def __new__(metacls, f):
        cls = type.__new__(metacls, f.__name__, (), {
            '_f': f,
            '__qualname__': f.__qualname__,
            '__module__': f.__module__,
            '__doc__': f.__doc__
        })
        cls.__instances = {}
        return cls

    def __init__(cls, f):  # only needed in 3.5 and below
        pass

    def __getitem__(cls, item):
        if not isinstance(item, tuple):
            item = (item,)
        try:
            return cls.__instances[item]
        except KeyError:
            cls.__instances[item] = c = cls._f(*item)
            item_repr = '[' + ', '.join(repr(i) for i in item) + ']'
            c.__name__ = cls.__name__ + item_repr
            c.__qualname__ = cls.__qualname__ + item_repr
            c.__template__ = cls
            return c

    def __subclasscheck__(cls, subclass):
        for c in subclass.mro():
            if getattr(c, '__template__', None) == cls:
                return True
        return False

    def __instancecheck__(cls, instance):
        return cls.__subclasscheck__(type(instance))

    def __repr__(cls):
        import inspect
        return '<template {!r}>'.format('{}.{}[{}]'.format(
            cls.__module__, cls.__qualname__, str(inspect.signature(cls._f))[1:-1]
        ))

Mit dieser neuen Metaklasse können wir das Beispiel in der Antwort, auf die ich verweise, wie folgt umschreiben:

@template
def List(member_type):
    class List(list):
        def append(self, member):
            if not isinstance(member, member_type):
                raise TypeError('Attempted to append a "{0}" to a "{1}" which only takes a "{2}"'.format(
                    type(member).__name__,
                    type(self).__name__,
                    member_type.__name__ 
                ))

                list.append(self, member)
    return List

l = List[int]()
l.append(1)  # ok
l.append("one")  # error

Dieser Ansatz hat einige nette Vorteile

print(List)  # <template '__main__.List[member_type]'>
print(List[int])  # <class '__main__.List[<class 'int'>, 10]'>
assert List[int] is List[int]
assert issubclass(List[int], List)  # True

1

Schauen Sie sich an, wie die eingebauten Container das machen. dictund listso weiter enthalten heterogene Elemente jeglicher Art, die Sie mögen. Wenn Sie beispielsweise eine insert(val)Funktion für Ihren Baum definieren, wird sie irgendwann so etwas tun node.value = valund Python kümmert sich um den Rest.


1

Glücklicherweise gab es einige Anstrengungen für die generische Programmierung in Python. Es gibt eine Bibliothek: generisch

Hier ist die Dokumentation dazu: http://generic.readthedocs.org/en/latest/

Es hat sich über Jahre nicht weiterentwickelt, aber Sie können eine ungefähre Vorstellung davon haben, wie Sie Ihre eigene Bibliothek verwenden und erstellen.

Prost


1

Wenn Sie Python 2 verwenden oder Java-Code neu schreiben möchten. Dafür gibt es keine wirkliche Lösung. Folgendes arbeite ich in einer Nacht: https://github.com/FlorianSteenbuck/python-generics Ich bekomme immer noch keinen Compiler, daher verwenden Sie ihn derzeit so:

class A(GenericObject):
    def __init__(self, *args, **kwargs):
        GenericObject.__init__(self, [
            ['b',extends,int],
            ['a',extends,str],
            [0,extends,bool],
            ['T',extends,float]
        ], *args, **kwargs)

    def _init(self, c, a, b):
        print "success c="+str(c)+" a="+str(a)+" b="+str(b)

TODOs

  • Compiler
  • Generische Klassen und Typen zum Laufen bringen (für Dinge wie <? extends List<Number>>)
  • superUnterstützung hinzufügen
  • ?Unterstützung hinzufügen
  • Code bereinigen
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.