Genéricos / plantillas en Python?

83

¿Cómo maneja Python los escenarios de tipo genérico / de plantilla? Digamos que quiero crear un archivo externo "BinaryTree.py" y que maneje árboles binarios, pero para cualquier tipo de datos.

Entonces podría pasarle el tipo de objeto personalizado y tener un árbol binario de ese objeto. ¿Cómo se hace esto en Python?

llaves
fuente
14
Python tiene plantillas de pato
David Heffernan

Respuestas:

82

Python usa el tipo pato , por lo que no necesita una sintaxis especial para manejar varios tipos.

Si tiene experiencia en C ++, recordará que, siempre que las operaciones utilizadas en la función / clase de la plantilla estén definidas en algún tipo T(a nivel de sintaxis), puede utilizar ese tipo Ten la plantilla.

Entonces, básicamente, funciona de la misma manera:

  1. defina un contrato para el tipo de elementos que desea insertar en el árbol binario.
  2. documentar este contrato (es decir, en la documentación de la clase)
  3. implementar el árbol binario utilizando solo las operaciones especificadas en el contrato
  4. disfrutar

Sin embargo, observará que, a menos que escriba una verificación de tipo explícita (que generalmente no se recomienda), no podrá exigir que un árbol binario contenga solo elementos del tipo elegido.

André Caron
fuente
5
André, me gustaría entender por qué normalmente se desaconseja la verificación de tipos explícita en Python. Estoy confundido porque parecería ser con un lenguaje escrito dinámicamente, podríamos meternos en muchos problemas si no podemos garantizar los posibles tipos que entrarán en la función. Pero, de nuevo, soy muy nuevo en Python. :-)
ScottEdwards2000
2
@ ScottEdwards2000 Puede tener una verificación de tipo implícita con sugerencias de tipo en PEP 484 y un verificador de tipo
noɥʇʎԀʎzɐɹƆ
6
En la perspectiva del purista de Python, Python es un lenguaje dinámico y la tipificación de pato es el paradigma; es decir, la seguridad de tipos se considera "no pitónica". Esto es algo que me resultó difícil de encontrar aceptable, por un tiempo, ya que estoy fuertemente investido en C #. Por un lado, considero que la seguridad de tipos es una necesidad. Como he equilibrado las escalas entre el mundo .Net y el paradigma Pythonic, he aceptado que la seguridad de tipos es realmente un chupete y, si es necesario, todo lo que tengo que hacer es if isintance(o, t):o if not isinstance(o, t):... bastante simple.
IAbstract
2
Gracias comentaristas, excelentes respuestas. Después de leerlos, me di cuenta de que realmente solo quiero verificar el tipo para detectar mis propios errores. Así que solo usaré la verificación de tipos implícita.
ScottEdwards2000
1
Creo que muchos Pythonistas no entienden esto: los genéricos son una forma de brindar libertad y seguridad al mismo tiempo. Incluso dejando de lado los genéricos y solo usando parámetros escritos, el escritor de funciones sabe que puede modificar su código para usar cualquier método que la clase proporcione; con la escritura de pato, si comienza a usar un método que no usaba antes, de repente cambió la definición de pato y probablemente las cosas se rompan.
Ken Williams
45

Las otras respuestas están totalmente bien:

  • No se necesita una sintaxis especial para admitir genéricos en Python
  • Python usa la escritura pato como lo señaló André .

Sin embargo, si aún desea una variante escrita , existe una solución incorporada desde Python 3.5.

Clases genéricas :

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

Funciones genéricas:

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.

Referencia: documentación mypy sobre genéricos .

momo
fuente
18

De hecho, ahora puede usar genéricos en Python 3.5+. Consulte PEP-484 y la documentación de la biblioteca de mecanografía .

Según mi práctica, no es muy transparente y claro, especialmente para aquellos que están familiarizados con Java Generics, pero aún se puede usar.

8bitjoey
fuente
10
Eso parece una estafa barata de genéricos tbh. Es como si alguien hubiera comprado genéricos, los puso en una licuadora, lo dejó correr y se olvidó de él hasta que el motor de la licuadora se quemó, y luego lo sacó 2 días después y dijo: "Oye, tenemos genéricos".
Todo el mundo
3
Esas son "sugerencias de tipo", no tienen nada que ver con los genéricos.
lana.en.plata
Lo mismo en mecanografiado pero funciona como en Java (sintácticamente). Los genéricos en estos idiomas son solo sugerencias de tipo
Davide
11

Después de pensar en algunos buenos pensamientos sobre cómo hacer tipos genéricos en Python, comencé a buscar otros que tuvieran la misma idea, pero no pude encontrar ninguno. Asi que aqui esta. Probé esto y funciona bien. Nos permite parametrizar nuestros tipos en 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 

Ahora puede derivar tipos de este tipo genérico.

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

Esta solución es simplista y tiene sus limitaciones. Cada vez que crea un tipo genérico, creará un nuevo tipo. Por lo tanto, varias clases que heredan List( str )como padre heredarían de dos clases separadas. Para superar esto, debe crear un dictado para almacenar las diversas formas de la clase interna y devolver la clase interna creada anteriormente, en lugar de crear una nueva. Esto evitaría que se creen tipos duplicados con los mismos parámetros. Si está interesado, se puede hacer una solución más elegante con decoradores y / o metaclases.

Ché en la escena
fuente
¿Puede explicar cómo se puede usar el dict en el ejemplo anterior? ¿Tiene un fragmento de eso en git o algo así? Gracias ..
gnomeria
No tengo un ejemplo, y podría llevar un poco de tiempo en este momento. Sin embargo, los principios no son tan difíciles. El dictado actúa como caché. Cuando se crea la nueva clase, necesita mirar los parámetros de tipo para crear un identificador para ese tipo y configuración de parámetros. Luego, puede usarlo como clave en un dictado para buscar la clase existente previamente. De esta manera, usará esa clase una y otra vez.
Ché on The Scene
Gracias por la inspiración - vea mi respuesta para una extensión de esta técnica con metaclases
Eric
4

Debido a que Python se escribe dinámicamente, los tipos de objetos no importan en muchos casos. Es una mejor idea aceptar cualquier cosa.

Para demostrar lo que quiero decir, esta clase de árbol aceptará cualquier cosa para sus dos ramas:

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

Y podría usarse así:

branch1 = BinaryTree(1,2)
myitem = MyClass()
branch2 = BinaryTree(myitem, None)
tree = BinaryTree(branch1, branch2)
Andrea
fuente
6
Los tipos de objetos importan. Si está recorriendo los elementos del contenedor y llamando a un método fooen cada objeto, entonces poner cadenas en el contenedor es una mala idea. No es mejor idea aceptar nada . Sin embargo, es conveniente no requerir que todos los objetos del contenedor deriven de class HasAFooMethod.
André Caron
1
En realidad, el tipo importa: hay que ordenar.
Fred Foo
Oh, vale. Entonces entendí mal.
Andrea
3

Dado que Python se escribe dinámicamente, esto es muy fácil. De hecho, tendría que hacer un trabajo adicional para que su clase BinaryTree no funcione con ningún tipo de datos.

Por ejemplo, si desea que los valores clave que se utilizan para colocar el objeto en el árbol estén disponibles dentro del objeto desde un método como el key()que acaba de llamar key()a los objetos. Por ejemplo:

class BinaryTree(object):

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

Tenga en cuenta que nunca es necesario definir qué tipo de clase es object_to_insert. Siempre que tenga un key()método, funcionará.

La excepción es si desea que funcione con tipos de datos básicos como cadenas o números enteros. Tendrá que envolverlos en una clase para que funcionen con su BinaryTree genérico. Si eso suena demasiado pesado y desea la eficiencia adicional de almacenar cadenas, lo siento, Python no es bueno para eso.

Leopd
fuente
7
Al contrario: todos los tipos de datos son objetos en Python. No es necesario envolverlos (como en Java con Integerboxing / unboxing).
George Hilliard
2

Aquí hay una variante de esta respuesta que usa metaclases para evitar la sintaxis desordenada y usa la sintaxis de typingestilo 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]
        ))

Con esta nueva metaclase, podemos reescribir el ejemplo en la respuesta a la que me vinculo como:

@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

Este enfoque tiene algunos beneficios interesantes

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
Eric
fuente
1

Mira cómo lo hacen los contenedores incorporados. dicty listasí sucesivamente contienen elementos heterogéneos del tipo que desee. Si define, digamos, una insert(val)función para su árbol, en algún momento hará algo como node.value = valy Python se encargará del resto.

John Zwinck
fuente
1

Afortunadamente, ha habido algunos esfuerzos para la programación genérica en Python. Hay una biblioteca: genérica

Aquí está la documentación para ello: http://generic.readthedocs.org/en/latest/

No ha progresado durante años, pero puede tener una idea aproximada de cómo usar y hacer su propia biblioteca.

Salud

igaurav
fuente
1

Si usa Python 2 o desea reescribir el código java. No hay una solución real para esto. Esto es lo que consigo trabajando en una noche: https://github.com/FlorianSteenbuck/python-generics Todavía no obtengo un compilador, así que actualmente lo usas así:

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

  • Compilador
  • Hacer que las clases y tipos genéricos funcionen (para cosas como <? extends List<Number>>)
  • Agregar supersoporte
  • Agregar ?soporte
  • Limpieza de código
Florian Steenbuck
fuente