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


86

Як python обробляє загальні сценарії / сценарії типу? Скажімо, я хочу створити зовнішній файл "BinaryTree.py", щоб він обробляв двійкові дерева, але для будь-якого типу даних.

Тож я міг передавати йому тип користувацького об’єкта і мати бінарне дерево цього об’єкта. Як це робиться в python?


16
python має качині шаблони
Девід Хеффернан

Відповіді:


84

Python використовує набір качок , тому йому не потрібен спеціальний синтаксис для обробки декількох типів.

Якщо ви з фону C ++, ви пам’ятаєте, що поки операції, що використовуються у функції / класі шаблону, визначені для певного типу T(на рівні синтаксису), ви можете використовувати цей тип Tу шаблоні.

Отже, в основному, це працює однаково:

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

Однак ви зауважите, що якщо ви не напишете явної перевірки типу (що зазвичай не рекомендується), ви не зможете забезпечити, щоб бінарне дерево містило лише елементи вибраного типу.


6
Андре, я хотів би зрозуміти, чому явна перевірка типу зазвичай не рекомендується в Python. Мене бентежить, оскільки, здається, мова йде про динамічно набрану мову, ми можемо зіткнутися з великою кількістю неприємностей, якщо не зможемо гарантувати можливі типи, які ввійдуть у функцію. Але знову ж таки, я дуже новачок у Python. :-)
ScottEdwards2000

3
@ ScottEdwards2000 Ви можете мати неявну перевірку типу за допомогою підказок типу в PEP 484 та перевірку типу
noɥʇʎԀʎzɐɹƆ

7
У перспективі пуристів пітона, Python є динамічним мовою і качка-типізація парадигма; тобто типова безпека визначається як "непітонічна". Це те, що мені важко було прийняти - на деякий час - оскільки я сильно покладаюсь на C #. З одного боку, я вважаю безпеку типу необхідною. Оскільки я збалансував масштаби між світом .Net та парадигмою Pythonic, я визнав, що безпека типу - це справді пустушка, і якщо мені потрібно, все, що мені потрібно зробити, це чи ... досить просто. if isintance(o, t):if not isinstance(o, t):
Абстрактно,

2
Дякую коментаторам, чудові відповіді. Прочитавши їх, я зрозумів, що я справді просто хочу перевірити тип, щоб виявити власні помилки. Тому я просто буду використовувати неявну перевірку типу.
ScottEdwards2000,

3
Думаю, багато хто з пітоністів пропускає суть щодо цього - дженерики - це спосіб забезпечити свободу та безпеку одночасно. Навіть, залишаючи осторонь загальні засоби та просто використовуючи набрані параметри, програміст функцій знає, що вони можуть модифікувати свій код, використовуючи будь-який метод, який надає клас; при наборі качки, якщо ви починаєте використовувати метод, який раніше не застосовували, ви раптом змінили визначення качки, і все, ймовірно, зламається.
Ken Williams

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, але все ще придатний для використання.


12
Це виглядає як дешеве розкрадання дженериків tbh. Це ніби хтось отримав дженерики, поклав їх у блендер, дав йому запуститись і забув про це, поки двигун блендера не перегорів, а потім вийняв його через 2 дні і сказав: "агов, ми отримали дженерики".
Усі

3
Це "підказки типу", вони не мають нічого спільного з дженериками.
Wolf.in.silver

Те саме в машинописі, але там він працює як у 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 )як батьківські, будуть успадковувати два окремі класи. Щоб подолати це, потрібно створити дикт, щоб зберігати різні форми внутрішнього класу та повертати попередній створений внутрішній клас, а не створювати новий. Це не дозволить створювати повторювані типи з однаковими параметрами. Якщо зацікавлено, можна зробити більш елегантне рішення за допомогою декораторів та / або метакласів.


Чи можете ви детальніше пояснити, як можна використовувати наказ у наведеному вище прикладі? У вас є для цього фрагмент або в git, або щось інше? Дякую ..
gnomeria

У мене немає прикладу, і зараз це може зайняти трохи часу. Однак принципи не такі складні. Дикт діє як кеш. Коли створюється новий клас, йому потрібно переглянути параметри типу, щоб створити ідентифікатор для цього типу та конфігурації параметрів. Тоді він може використовувати його як ключ у дикті для пошуку раніше існуючого класу. Таким чином, він буде використовувати цей клас знову і знову.
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.
Андре Карон,

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/

Він не прогресував роками, але ви можете приблизно уявити, як користуватись і створювати власну бібліотеку.

Ура


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)

ЗАВДАННЯ

  • Укладач
  • Працюйте загальні класи та типи (для таких речей, як <? extends List<Number>>)
  • Додайте superпідтримку
  • Додайте ?підтримку
  • Очищення коду
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.