Python中的泛型/模板?


83

python如何处理通用/模板类型的场景?假设我想创建一个外部文件“ BinaryTree.py”,并使其处理二进制树,但是对于任何数据类型。

因此,我可以将自定义对象的类型传递给它,并为该对象提供一个二叉树。如何在python中完成?


14
python有鸭子模板
David Heffernan

Answers:


82

Python使用鸭子类型,因此它不需要特殊的语法即可处理多种类型。

如果您来自C ++,那么您会记住,只要模板函数/类中使用的操作是在某种类型T(在语法级别)上定义的,就可以T在模板中使用该类型。

因此,基本上,它的工作方式相同:

  1. 为要在二叉树中插入的项目类型定义合同。
  2. 记录此合同(即在班级文件中)
  3. 仅使用合同中指定的操作来实现二叉树
  4. 请享用

但是,您会注意到,除非编写显式类型检查(通常不建议这样做),否则您将无法强制二叉树仅包含所选类型的元素。


5
André,我想了解为什么通常不建议在Python中使用显式类型检查。我很困惑,因为它似乎使用的是动态类型的语言,如果我们不能保证函数中可能包含的类型,我们可能会遇到很多麻烦。但是,再说一次,我对Python还是很陌生。:-)
ScottEdwards2000 '16

2
@ ScottEdwards2000你可以隐式类型的PEP 484与类型提示检查和类型检查
noɥʇʎԀʎzɐɹƆ

6
在Python纯粹的角度来看,Python是一种动态语言和鸭打字范式; 也就是说,类型安全性被裁定为“非Pythonic”。一段时间以来,我很难找到可以接受的东西,因为我深深地归属于C#。一方面,我认为类型安全是必要的。当我平衡了.Net世界和Pythonic范例之间的规模时,我已经接受了类型安全确实是一个安慰者,并且如果需要,我要做的就是...if isintance(o, t):或者if not isinstance(o, t):很简单。
IAbstract

2
感谢评论者,很好的答案。阅读它们后,我意识到我真的只希望类型检查来捕获自己的错误。因此,我将只使用隐式类型检查。
ScottEdwards2000

1
我认为许多Python专家都对此一无所知-泛型是同时提供自由和安全性的一种方式。即使抛开泛型并只使用类型化的参数,函数编写者也知道他们可以修改代码以使用该类提供的任何方法。如果您开始使用以前从未使用过的方法进行鸭子输入,则突然更改了鸭子的定义,事情可能会中断。
肯·威廉姆斯

45

其他答案也很好:

  • 不需要特殊的语法即可支持Python中的泛型
  • Python使用André指出的鸭子类型。

但是,如果您仍然想要类型化的变体,那么从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文档。


18

实际上,现在您可以在Python 3.5+中使用泛型。请参阅PEP-484键入库文档

根据我的实践,它并不是非常无缝和清晰,特别是对于那些熟悉Java泛型但仍然可用的人。


10
这看起来像是廉价的仿制药TBH。就像有人获得了仿制药一样,将它们放入搅拌机中,让它运行并忘记它,直到搅拌机电动机烧毁,然后两天后取出并说:“嘿,我们得到了仿制药”。
大家

3
这些是“类型提示”,它们与泛型无关。
羊毛。银.

打字稿中的内容相同,但在Java中(语法上)类似。这些语言的泛型只是类型提示
Davide

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 )作为父代继承的多个类将从两个单独的类继承。为了克服这个问题,您需要创建一个字典来存储内部类的各种形式并返回之前创建的内部类,而不是创建一个新的内部类。这将防止创建具有相同参数的重复类型。如果有兴趣,可以使用装饰器和/或元类提出更优雅的解决方案。


您能否详细说明如何在上面的示例中使用dict?您是否有git或类似内容的代码段?谢谢..
gnomeria

我没有一个例子,现在可能有点耗时。但是,原理并不难。该字典充当缓存。创建新类时,它需要查看类型参数以为该类型和参数配置创建标识符。然后,它可以将其用作字典中的键以查找先前存在的类。这样,它将一遍又一遍地使用该类。
现场的Ché'Apr 9'17

感谢您的启发-请参阅我的答案以扩展此技术的元类
Eric,

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)

6
对象的类型重要。如果要遍历容器的项并foo在每个对象上调用一个方法,则将字符串放入容器中是个坏主意。接受任何东西不是一个更好的主意。但是,方便的是,不需要容器中的所有对象都派生自class 。HasAFooMethod
安德烈·卡隆

1
实际上,类型确实很重要:必须排序。
弗雷德·富

哦好的。那我误会了。
Andrea

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中的对象。它们不需要包装(如在带Integer装箱/拆箱的Java中一样)。
乔治·希利亚德

2

这是此答案的一个变体,它使用元类来避免语法混乱,而使用typing-styleList[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

看一下内置容器是如何做到的。 dictlist等含有任何你喜欢的类型的异质元素。例如,如果您insert(val)为树定义一个函数,则该函数有时会执行类似的操作,node.value = val而Python将负责其余的工作。



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支持
  • 增加?支持
  • 代码清理
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.