Python中的泛型/模板?

171

Python如何处理通用/模板类型的场景?比如我想创建一个名为“BinaryTree.py”的外部文件,并使其处理二叉树,但适用于任何数据类型。

因此,我可以将自定义对象的类型传递给它,并拥有该对象的二叉树。这在Python中是如何实现的呢?


19
Python 有“鸭子模板”。 - David Heffernan
10个回答

197
其他回答已经非常好了:
  • Python不需要特殊的语法来支持泛型
  • 正如André所指出的,Python使用鸭子类型。

然而,如果你仍然想要一个类型化的变体,自从Python 3.5以来就有一个内置的解决方案。

可以在Python文档中找到所有可用的类型注释列表。


通用类

from typing import TypeVar, Generic, List

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.

静态类型检查:

必须使用像mypyPyre(由Meta/FB开发)这样的静态类型检查器来分析源代码。

安装mypy:

python3 -m pip install mypy

分析您的源代码,例如某个文件:
mypy foo.py

或目录:

mypy some_directory

mypy将检测和打印类型错误。对于上面提供的Stack示例,具体输出如下:

foo.py:23: error: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int"

参考资料:mypy有关泛型运行mypy的文档。


3
因为如果你知道了这个,那么你对于 abc.ABC 的所有现有知识都可以应用到这里的 Stack 类中。 - Jonathan Komar
3
我运行了上面的堆栈代码,并且因某些原因在 stack.push("x") 上没有收到任何错误。为什么会这样? - Quoc Anh Tran
2
@QuocAnhTran 我添加了一个新的部分“静态类型检查”以进行进一步解释。 - momo
2
@cikatomo 我们能够编写 Stack[int] 是因为我们的 Stack 类继承自 Generic[T],在其中使用 [T] 指定了我们的 Stack 类接受单个类型参数。 - momo
2
@cikatomo 让我给你一个指针:PEP 484 - 用户定义的通用类型 - momo
显示剩余5条评论

114

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

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

所以,基本上它的工作方式相同:

  1. 为您想要插入到二叉树中的项目类型定义一个契约。
  2. 记录此契约(即在类文档中)
  3. 仅使用契约中指定的操作实现二叉树
  4. 享受

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


10
安德烈,我想了解为什么 Python 通常不建议进行显式类型检查。我感到困惑,因为在动态类型语言中,如果我们不能保证传入函数的类型,似乎会遇到很多问题。但是,我对 Python 还非常陌生。 :-) - ScottEdwards2000
4
你可以使用 PEP 484 中的类型提示和类型检查器来实现隐式类型检查。 - noɥʇʎԀʎzɐɹƆ
14
从Python 纯粹主义者的角度来看,Python 是一种动态语言,鸭子类型是*范式;也就是说,类型安全性被认为是"不符合 Python 风格"的。对我来说,这是很难接受的——因为我深深地沉浸在 C# 中。一方面,我认为类型安全性是必要的。但在我平衡了.NET 世界和 Pythonic 范式之间的天平后,我认为类型安全性仅仅是个安抚剂,如果需要的话,我只需使用if isinstance(o, t):if not isinstance(o, t):...非常简单。 - IAbstract
3
谢谢留言者们,回答得很好。阅读完之后,我意识到我真正想要的只是类型检查能够捕获我自己的错误。因此,我将使用隐式类型检查。 - ScottEdwards2000
14
我认为许多Python程序员在这方面误解了 - 泛型是同时提供自由和安全的一种方式。即使不考虑泛型,只使用类型参数,函数编写者也知道他们可以修改代码以使用类提供的任何方法;使用鸭子类型,如果您开始使用之前没有使用过的方法,您会突然改变鸭子的定义,事情可能会出错。 - Ken Williams
显示剩余3条评论

24

实际上,现在你可以在Python 3.5+中使用泛型。

请参阅PEP-484typing模块文档

根据我的经验,这对于那些熟悉Java泛型的人来说并不是非常流畅和清晰,但仍然可以使用。


23
说实话,这看起来像是廉价山寨版的通用药。就好像有人将通用药物放入搅拌机中,让它运转并遗忘了,直到搅拌机的电机烧毁,然后两天后拿出来说:“嘿,我们有通用药”。 - Everyone
6
这里提到的是“类型提示”,与泛型无关。 - wool.in.silver
在TypeScript中与Java类似,但其语法上有效。在这些语言中,泛型只是类型提示。 - Davide
大家快来看看新的语法 - undefined

14

在想出如何在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 On The Scence
感谢您的启发 - 可以查看我的答案,其中介绍了使用元类扩展此技术的方法。 - Eric

5
这是一个使用元类避免混乱语法,并使用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

4

由于Python是动态类型的,因此这非常容易。实际上,为了使您的BinaryTree类不与任何数据类型一起使用,您需要额外工作。

例如,如果您希望将用于将对象放置在树中的键值从key()方法中可用于对象中,只需在对象上调用key()即可。例如:

class BinaryTree(object):

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

请注意,您不需要定义对象object_to_insert的类型。只要它有一个key()方法,它就可以工作。
例外情况是如果您想让它与基本数据类型(如字符串或整数)一起使用。您将不得不将它们包装在一个类中,以使它们与您的通用二叉树一起使用。如果这听起来太重了,并且您希望实际存储字符串的额外效率,抱歉,Python不擅长这样做。

9
相反地,Python 中所有数据类型都是对象。它们不需要像 Java 中的 Integer 包装/取消包装那样进行处理。 - George Hilliard

3
如果你使用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支持
  • 增加?支持
  • 清理代码

2

幸运的是,Python中已经有一些通用编程方面的努力。这里有一个库:generic

这是它的文档:http://generic.readthedocs.org/en/latest/

虽然它多年来没有进展,但您可以大致了解如何使用它并创建自己的库。

祝贺你


2

看看内置的容器是如何做到的。 dictlist 等包含各种类型的异构元素。如果你为树定义了一个 insert(val) 函数,它在某个时刻会执行类似于 node.value = val 的操作,Python 会处理其余部分。


1
现在可以从Python 3.12开始使用泛型类型和类型别名了!(今天发布!)
根据PEP-695的指导,现在可以编写如下代码:
def max[T](args: Iterable[T]) -> T:
    ...

class list[T]:
    def __getitem__(self, index: int, /) -> T:
        ...

    def append(self, element: T) -> None:
        ...

现在也可以使用类型别名了!甚至可以使用泛型
type Point = tuple[float, float]

最后一个官方指南的示例:
ype IntFunc[**P] = Callable[P, int]  # ParamSpec
type LabeledTuple[*Ts] = tuple[str, *Ts]  # TypeVarTuple
type HashableSequence[T: Hashable] = Sequence[T]  # TypeVar with bound
type IntOrStrSequence[T: (int, str)] = Sequence[T]  # TypeVar with constraints

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接