Typy / szablony w Pythonie?


86

W jaki sposób Python obsługuje scenariusze typu ogólnego / szablonu? Powiedzmy, że chcę utworzyć zewnętrzny plik „BinaryTree.py” i zlecić mu obsługę drzew binarnych, ale dla dowolnego typu danych.

Mogłem więc przekazać mu typ obiektu niestandardowego i mieć drzewo binarne tego obiektu. Jak to się robi w Pythonie?


16
python ma szablony kaczek
David Heffernan

Odpowiedzi:


84

Python używa pisania kaczego , więc nie potrzebuje specjalnej składni do obsługi wielu typów.

Jeśli jesteś w tle C ++, będziesz pamiętać, że dopóki operacje używane w funkcji / klasie szablonu są zdefiniowane na jakimś typie T(na poziomie składni), możesz użyć tego typu Tw szablonie.

Zasadniczo działa to w ten sam sposób:

  1. zdefiniuj kontrakt dla typu elementów, które chcesz wstawić do drzewa binarnego.
  2. udokumentuj tę umowę (tj. w dokumentacji zajęć)
  3. zaimplementuj drzewo binarne, używając tylko operacji określonych w kontrakcie
  4. cieszyć się

Zauważysz jednak, że jeśli nie napiszesz jawnego sprawdzania typu (co jest zwykle odradzane), nie będziesz w stanie wymusić, że drzewo binarne zawiera tylko elementy wybranego typu.


6
André, chciałbym zrozumieć, dlaczego jawne sprawdzanie typów jest zwykle odradzane w Pythonie. Jestem zdezorientowany, ponieważ wydaje się, że jest to język dynamicznie typowany, możemy mieć wiele problemów, jeśli nie możemy zagwarantować możliwych typów, które pojawią się w funkcji. Ale z drugiej strony jestem bardzo nowy w Pythonie. :-)
ScottEdwards2000

3
@ ScottEdwards2000 Możesz mieć niejawne sprawdzanie typów za pomocą wskazówek typu w PEP 484 i sprawdzania typów
nieɥʇʎԀʎzɐɹƆ

7
W perspektywie pytona purist, w Python jest językiem dynamiczny i kaczki-typowania jest paradygmat; tzn. bezpieczeństwo typu jest określane jako „nie-Pythonic”. Jest to coś, co było dla mnie trudne do zaakceptowania - przez chwilę - ponieważ jestem mocno przywiązany do C #. Z jednej strony uważam, że bezpieczeństwo typu jest koniecznością. Jak już wyważone wagi między światem NET i paradygmatu pythonic, mam przyjąć, że bezpieczeństwo typu jest naprawdę smoczek i jeśli trzeba, wszystko co musisz zrobić, to albo ... całkiem proste. if isintance(o, t):if not isinstance(o, t):
IAbstract

2
Dzięki komentatorom, świetne odpowiedzi. Po przeczytaniu zdałem sobie sprawę, że naprawdę chcę tylko sprawdzić typ, aby wychwycić własne błędy. Więc po prostu użyję niejawnego sprawdzania typu.
ScottEdwards2000

3
Myślę, że wielu Pythonistów nie rozumie w tej kwestii - generyczne są sposobem na zapewnienie wolności i bezpieczeństwa w tym samym czasie. Nawet pomijając typy generyczne i używając tylko parametrów typowanych, program zapisujący funkcje wie, że może zmodyfikować swój kod, aby użyć dowolnej metody dostarczonej przez klasę; przy pisaniu kaczką jeśli zaczniesz używać metody, której wcześniej nie używałeś, nagle zmieniłeś definicję kaczki i prawdopodobnie wszystko się zepsuje.
Ken Williams

60

Pozostałe odpowiedzi są całkowicie w porządku:

  • Nie potrzeba specjalnej składni do obsługi typów ogólnych w Pythonie
  • Python używa pisania kaczego, jak wskazał André .

Jeśli jednak nadal potrzebujesz wariantu wpisanego na maszynie , istnieje wbudowane rozwiązanie od wersji Python 3.5.

Klasy ogólne :

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

Funkcje ogólne:

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.

Odniesienie: mypy dokumentacja o rodzajach generycznych .


1
Zdecydowanie najlepsza odpowiedź tutaj
Denis Itskovich

Ważna uwaga z docs.python.org/3.5/library/typing.html to „Metaklasa używana przez Generic jest podklasą abc.ABCMeta”.
Jonathan Komar

20

Właściwie teraz możesz używać typów ogólnych w Pythonie 3.5+. Zobacz dokumentację PEP-484 i moduł do pisania .

Zgodnie z moją praktyką nie jest to zbyt płynne i jasne, zwłaszcza dla tych, którzy znają język generyczny Java, ale nadal można go używać.


12
To wygląda na tani zdzierstwo leków generycznych. To tak, jakby ktoś wziął leki generyczne, włożył je do blendera, pozwolił mu pracować i zapomniał o tym, dopóki silnik blendera nie wypalił się, a potem wyjął go 2 dni później i powiedział: „hej, mamy leki generyczne”.
Wszyscy

3
To są „wskazówki dotyczące typów”, nie mają one nic wspólnego z rodzajami.
wool.in.silver

To samo w maszynopisie, ale tam działa jak w Javie (składniowo). Typy generyczne w tych językach to tylko podpowiedzi do wpisywania
Davide

11

Po kilku dobrych przemyśleniach na temat tworzenia typów ogólnych w Pythonie, zacząłem szukać innych, którzy mieli ten sam pomysł, ale nie mogłem znaleźć żadnego. A więc oto jest. Wypróbowałem to i działa dobrze. Pozwala nam sparametryzować nasze typy w Pythonie.

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 

Możesz teraz wyprowadzać typy z tego typu ogólnego.

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

To rozwiązanie jest uproszczone i ma swoje ograniczenia. Za każdym razem, gdy tworzysz typ ogólny, tworzy on nowy typ. Tak więc wiele klas dziedziczących List( str )jako rodzic będzie dziedziczyć z dwóch oddzielnych klas. Aby to przezwyciężyć, musisz utworzyć dyktando, aby przechowywać różne formy klasy wewnętrznej i zwrócić poprzednio utworzoną klasę wewnętrzną, zamiast tworzyć nową. Zapobiegałoby to tworzeniu zduplikowanych typów o tych samych parametrach. Jeśli jesteś zainteresowany, możesz stworzyć bardziej eleganckie rozwiązanie z dekoratorami i / lub metaklasami.


Czy możesz wyjaśnić, w jaki sposób można użyć tego dyktu w powyższym przykładzie? Masz do tego fragment kodu w git czy coś takiego? Dziękuję ..
gnomeria

Nie mam przykładu, a teraz może to zająć trochę czasu. Jednak zasady nie są takie trudne. Dykt działa jako pamięć podręczna. Po utworzeniu nowej klasy należy spojrzeć na parametry typu, aby utworzyć identyfikator dla tego typu i konfiguracji parametrów. Następnie może użyć go jako klucza w dyktandzie do wyszukania poprzednio istniejącej klasy. W ten sposób będzie używać tej jednej klasy w kółko.
Ariyo Live

Dziękuję za inspirację - zobacz moją odpowiedź na rozszerzenie tej techniki o metaklasy
Eric

4

Ponieważ Python jest typowany dynamicznie, typy obiektów w wielu przypadkach nie mają znaczenia. Lepszym pomysłem jest zaakceptowanie wszystkiego.

Aby pokazać, co mam na myśli, ta klasa drzewa zaakceptuje wszystko dla swoich dwóch gałęzi:

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

Można go użyć w ten sposób:

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

6
Rodzaje obiektów mają znaczenie. Jeśli przeglądasz elementy kontenera i wywołujesz metodę foona każdym obiekcie, to umieszczanie ciągów znaków w kontenerze jest złym pomysłem. Nie lepiej jest cokolwiek zaakceptować . Jednak wygodnie jest nie wymagać, aby wszystkie obiekty w kontenerze pochodziły z klasy HasAFooMethod.
André Caron

1
Właściwie rodzaj ma znaczenie: musi być zamówiony.
Fred Foo

Och, OK. Wtedy źle zrozumiałem.
Andrea,

3

Ponieważ python jest wpisywany dynamicznie, jest to bardzo łatwe. W rzeczywistości musiałbyś wykonać dodatkową pracę, aby Twoja klasa BinaryTree nie działała z żadnym typem danych.

Na przykład, jeśli chcesz, aby wartości kluczowe, które są używane do umieszczenia obiektu w drzewie, dostępne w obiekcie za pomocą metody takiej jak key()po prostu wywołanie key()obiektów. Na przykład:

class BinaryTree(object):

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

Zauważ, że nigdy nie musisz definiować rodzaju klasy object_to_insert. Dopóki ma key()metodę, będzie działać.

Wyjątkiem jest sytuacja, gdy chcesz, aby działał z podstawowymi typami danych, takimi jak ciągi znaków lub liczby całkowite. Będziesz musiał opakować je w klasę, aby zmusić je do pracy z twoim ogólnym BinaryTree. Jeśli to brzmi zbyt ciężko i chcesz uzyskać dodatkową efektywność przechowywania po prostu strun, przepraszam, nie w tym Python jest dobry.


7
Wręcz przeciwnie: wszystkie typy danych są obiektami w Pythonie. Nie trzeba ich Integeropakowywać (jak w Javie z boxing / unboxing).
George Hilliard,

2

Oto wariant tej odpowiedzi, który wykorzystuje metaklasy, aby uniknąć niechlujnej składni i użyć składni w typingstylu -styl 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]
        ))

Dzięki tej nowej metaklasie możemy przepisać przykład w odpowiedzi, do której odsyłam, jako:

@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

Takie podejście ma kilka fajnych zalet

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

Zobacz, jak robią to wbudowane kontenery. dicti listtak dalej zawierają heterogeniczne elementy dowolnego typu. Jeśli zdefiniujesz, powiedzmy, insert(val)funkcję dla swojego drzewa, w pewnym momencie zrobi ona coś podobnego, node.value = vala Python zajmie się resztą.



1

Jeśli używasz Pythona 2 lub chcesz przepisać kod java. To nie jest prawdziwe rozwiązanie tego problemu. Oto, co pracuję w nocy: https://github.com/FlorianSteenbuck/python-generics Nadal nie mam kompilatora, więc obecnie używasz go w ten sposób:

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

  • Kompilator
  • Spraw, aby klasy i typy generyczne działały (na przykład <? extends List<Number>>)
  • Dodaj superwsparcie
  • Dodaj ?wsparcie
  • Czyszczenie kodu
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.