Дженерики / шаблоны в Python?

86

Как python обрабатывает сценарии универсального / шаблонного типа? Скажем, я хочу создать внешний файл «BinaryTree.py» и заставить его обрабатывать двоичные деревья, но для любого типа данных.

Поэтому я мог бы передать ему тип настраиваемого объекта и получить двоичное дерево этого объекта. Как это делается в питоне?

ключи
источник
16
у python есть шаблоны уток
Дэвид Хеффернан

Ответы:

84

Python использует утиную типизацию , поэтому ему не нужен специальный синтаксис для обработки нескольких типов.

Если вы знакомы с C ++, вы помните, что до тех пор, пока операции, используемые в функции / классе шаблона, определены для некоторого типа T(на уровне синтаксиса), вы можете использовать этот тип Tв шаблоне.

Итак, в основном это работает так же:

  1. определить контракт для типа элементов, которые вы хотите вставить в двоичное дерево.
  2. задокументируйте этот контракт (т.е. в документации класса)
  3. реализовать бинарное дерево, используя только операции, указанные в контракте
  4. наслаждаться

Однако вы заметите, что до тех пор, пока вы не напишете явную проверку типа (что обычно не рекомендуется), вы не сможете обеспечить, чтобы двоичное дерево содержало только элементы выбранного типа.

Андре Карон
источник
6
Андре, я хотел бы понять, почему явная проверка типов в Python обычно не приветствуется. Я сбит с толку, потому что, похоже, это язык с динамической типизацией, у нас может возникнуть множество проблем, если мы не сможем гарантировать возможные типы, которые войдут в функцию. Но опять же, я новичок в Python. :-)
ScottEdwards2000 09
3
@ ScottEdwards2000 У вас может быть неявная проверка типов с помощью подсказок типов в PEP 484 и средства проверки типов
noɥʇʎԀʎzɐɹƆ
7
С точки зрения пуристов Python, Python - это динамический язык, а утиная типизация - это парадигма; т.е. безопасность типов считается «непитонической». Это то, что мне было трудно найти приемлемым - какое-то время - поскольку я в значительной степени увлечен C #. С одной стороны, я считаю безопасность типов необходимостью. Когда я уравновесил масштабы между миром .Net и парадигмой Pythonic, я согласился с тем, что безопасность типов - это действительно пустышка, и, если мне нужно, все, что мне нужно сделать, это if isintance(o, t):или if not isinstance(o, t):... довольно просто.
IAbstract
2
Спасибо комментаторам, отличные ответы. Прочитав их, я понял, что действительно хочу, чтобы проверка типов выявляла мои собственные ошибки. Поэтому я просто буду использовать неявную проверку типов.
ScottEdwards2000
3
Я думаю, что многие питонисты упускают из виду главное - дженерики - это способ одновременно обеспечить свободу и безопасность. Даже если оставить в стороне универсальные шаблоны и просто использовать типизированные параметры, автор функций знает, что они могут изменить свой код, чтобы использовать любой метод, предоставляемый классом; с уткой, если вы начнете использовать метод, которым раньше не пользовались, вы внезапно изменили определение утки, и что-то, вероятно, сломается.
Кен Уильямс
60

Другие ответы совершенно нормальные:

  • Не требуется специальный синтаксис для поддержки дженериков в Python.
  • Python использует утиную типизацию, как указал Андре .

Однако, если вам по-прежнему нужен типизированный вариант, есть встроенное решение, начиная с Python 3.5.

Общие классы :

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

Общие функции:

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.

Ссылка: документация mypy о дженериках .

Момо
источник
1
Определенно лучший ответ здесь
Денис Ицкович
Важное примечание от docs.python.org/3.5/library/typing.html : « Метакласс, используемый Generic, является подклассом abc.ABCMeta».
Джонатан Комар
20

На самом деле теперь вы можете использовать дженерики в Python 3.5+. См. Документацию по PEP-484 и модулю набора текста .

Согласно моей практике, это не очень гладко и понятно, особенно для тех, кто знаком с Java Generics, но все же можно использовать.

8bitjoey
источник
12
Похоже на дешевый срыв дженериков tbh. Это как если бы кто-то взял дженерики, поместил их в блендер, дал ему поработать и забыл об этом, пока мотор блендера не перегорел, а затем вынул его через 2 дня и сказал: «Эй, у нас есть дженерики».
Все
3
Это «подсказки типа», они не имеют ничего общего с обобщениями.
шерсть.in.silver 07
То же самое в машинописном тексте, но там работает как в Java (синтаксически). Обобщения на этих языках - просто подсказки типа
Давиде
11

Придумав несколько хороших мыслей о создании универсальных типов в python, я начал искать других, у кого была такая же идея, но я не мог их найти. Итак, вот оно. Я попробовал это, и он хорошо работает. Это позволяет нам параметризовать наши типы в Python.

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 

Теперь вы можете наследовать типы от этого универсального типа.

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

Это упрощенное решение, и у него есть свои ограничения. Каждый раз, когда вы создаете универсальный тип, он создает новый тип. Таким образом, несколько классов, наследующих List( str )как родительские, будут наследовать от двух отдельных классов. Чтобы преодолеть это, вам нужно создать команду для хранения различных форм внутреннего класса и возврата ранее созданного внутреннего класса, а не создания нового. Это предотвратит создание повторяющихся типов с одинаковыми параметрами. Если интересно, можно сделать более элегантное решение с помощью декораторов и / или метаклассов.

Ariyo Live
источник
Не могли бы вы рассказать, как можно использовать dict в приведенном выше примере? У вас есть отрывок в git или что-то в этом роде? Спасибо ..
gnomeria
У меня нет примера, и сейчас это может занять немного времени. Однако принципы не так уж и сложны. Диктовка действует как кеш. Когда создается новый класс, ему необходимо посмотреть параметры типа, чтобы создать идентификатор для этого типа и конфигурации параметров. Затем он может использовать его как ключ в dict для поиска ранее существовавшего класса. Таким образом, он будет использовать этот один класс снова и снова.
Ariyo Live
Спасибо за вдохновение - см. Мой ответ о расширении этой техники с помощью метаклассов
Эрик
4

Поскольку Python имеет динамическую типизацию, типы объектов во многих случаях не имеют значения. Лучше принять что-нибудь.

Чтобы продемонстрировать, что я имею в виду, этот древовидный класс будет принимать что угодно для своих двух ветвей:

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

И это можно было бы использовать так:

branch1 = BinaryTree(1,2)
myitem = MyClass()
branch2 = BinaryTree(myitem, None)
tree = BinaryTree(branch1, branch2)
Андреа
источник
7
Типы объектов делают дело. Если вы перебираете элементы контейнера и вызываете метод fooдля каждого объекта, то размещение строк в контейнере - плохая идея. Принимать что-либо - не лучшая идея . Однако удобно не требовать, чтобы все объекты в контейнере были производными от класса . HasAFooMethod
André Caron
1
Собственно, тип имеет значение: он должен быть заказным.
Fred Foo
О, ладно. Я тогда неправильно понял.
Андреа
3

Поскольку python динамически типизирован, это очень просто. Фактически, вам придется проделать дополнительную работу, чтобы ваш класс BinaryTree не работал с каким-либо типом данных.

Например, если вам нужны ключевые значения, которые используются для размещения объекта в дереве, доступном внутри объекта из метода, подобного тому, как key()вы просто вызываете key()объекты. Например:

class BinaryTree(object):

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

Обратите внимание, что вам никогда не нужно определять, что это за класс object_to_insert. Пока у него есть key()метод, он будет работать.

Исключение составляют случаи, когда вы хотите, чтобы он работал с основными типами данных, такими как строки или целые числа. Вам нужно будет обернуть их в класс, чтобы заставить их работать с вашим универсальным BinaryTree. Если это звучит слишком тяжело, и вам нужна дополнительная эффективность, заключающаяся в том, чтобы просто хранить строки, извините, это не то, в чем Python хорош.

Леопд
источник
7
Напротив: все типы данных в Python являются объектами. Их не нужно оборачивать (как в Java с Integerупаковкой / распаковкой).
Джордж Хиллиард,
2

Вот вариант этого ответа, который использует метаклассы, чтобы избежать беспорядочного синтаксиса, и использует синтаксис typing-style 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]
        ))

С помощью этого нового метакласса мы можем переписать пример в ответе, на который я ссылаюсь, как:

@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

У этого подхода есть хорошие преимущества

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

Посмотрите, как это делают встроенные контейнеры. dictи listтак далее, содержат разнородные элементы любых типов. Если вы определите, скажем, insert(val)функцию для своего дерева, она в какой-то момент сделает что-то вроде, node.value = valа Python позаботится обо всем остальном.

Джон Цвинк
источник
1

К счастью, были предприняты некоторые усилия по универсальному программированию на Python. Есть библиотека: родовая

Вот документация к нему: http://generic.readthedocs.org/en/latest/

Он не прогрессировал годами, но вы можете иметь приблизительное представление о том, как использовать и создавать свою собственную библиотеку.

Ура

igaurav
источник
1

Если вы используете Python 2 или хотите переписать код Java. Это не настоящее решение. Вот то, над чем я работаю за ночь: https://github.com/FlorianSteenbuck/python-generics У меня до сих пор нет компилятора, поэтому вы сейчас используете его вот так:

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

  • Компилятор
  • Получите работу общих классов и типов (например, для таких вещей <? extends List<Number>>)
  • Добавить superподдержку
  • Добавить ?поддержку
  • Очистка кода
Флориан Стинбак
источник