首页 > 编程语言 >初窥python泛型系统与类型约束

初窥python泛型系统与类型约束

时间:2024-11-14 22:07:35浏览次数:1  
标签:__ .__ python 约束 Generic params 泛型 class cls

对类进行索引
翻阅python源码有时会看到类似这样的实现,class Dataset(Generic[T_co]):Generic是一个类,但是可以直接对其进行索引,这需要归功于魔法方法__class_getitem__

class Box:
    def __class_getitem__(cls, item):
        print(cls, item)


var = Box[int, bool, str]  # 会输出 (<class 'int'>, <class 'bool'>, <class 'str'>)

之后会看到一个更具体且复杂的应用。

使用typing.TypeVar声明类型

通过typing.TypeVar可以声明一种类型,可以将这个类型作为type hint,例如:

_F = typing.TypeVar("_F")

def func():
    return 1

data: _F = func

但是这样的代码并不具有实际意义,我们希望能够对变量进行更好地类型检查并获得更好的提示功能。因此我们可以对类型增加多种约束。但是这些约束不会强制执行,只会得到警告,这是python语言特性决定的。例如:

_F = typing.TypeVar("_F", bound=typing.Callable[..., int])

我们对类型_F增加约束,希望它是一个可以接受任意数量参数,返回值为int类型的可调用对象。再例如:

T = TypeVar("T", int, float)

我们限定T只能是int或是float类型。
实际上typing.TypeVar非常灵活,有非常多可配置项。完整init函数声明如下:

 def __init__(self, name, *constraints, bound=None,
                 covariant=False, contravariant=False):

对于使用TypeVar声明的类型,还可以在运行时获取类型的基本信息,例如:

T = TypeVar("T", int, float)
print(T.__name__)  // T
print(T.__constraints__)  // (<class 'int'>, <class 'float'>)
// ... 更多用法

python的typing库提供了丰富的约束条件,几乎可以代表python中的所有类型特点。例如:

from typing import TypeVar, SupportsRound, SupportsAbs
SR = TypeVar("SR", bound=SupportsRound)  //希望类型SR可以支持round操作
from typing import TypeVar, Awaitable
SW = TypeVar("SW", bound=Awaitable)  // 希望类型SW是可以被await的

此外,typing库还内置了很多基本类型例如List、'Dict'、'Union'等。

T = TypeVar("T", int, float) 
TD = Dict[str, T]

td: TD = {}
td["a"] = 1
td["b"] = "2"  // 会得到一个警告 值的类型不匹配

TD表示一个key类型为字符串,value类型为int或是float类型的字典。
covariant是一个不太直观的编程概念,但是有时会用到这一特性。例如:

T_co = TypeVar("T_co", covariant=True)

__init_subclass__方法
函数名称可能具有一定误导性,这个方法在声明子类时就调用而不需要实例化子类对象。并且可以在定义子类时传递参数。

class Base:
    def __init_subclass__(cls, config=None, **kwargs):
        cls.config = config
        print(f"Subclass {cls.__name__} created with config: {config}")
        super().__init_subclass__(**kwargs)


class Sub1(Base, config="config1"):
    pass


class Sub2(Base, config="config2"):
    pass

Generic使用

T_co = TypeVar("T_co", covariant=True)


class Dataset(Generic[T_co]):
    def __init__(self, data: List[T_co]):
        self.data = data

    def get_data(self) -> List[T_co]:
        return self.data

d: Dataset[int] = Dataset([1, 2, 3])  # 通过泛型得到类型提示
print(Dataset[int].__origin__)        # 继承自Generic类会获取该属性
print(Dataset[int].__args__)          # 继承自Generic类会获取该属性
print(Dataset[int].__parameters__)    # 继承自Generic类会获取该属性
class Generic:
    """Abstract base class for generic types.

    A generic type is typically declared by inheriting from
    this class parameterized with one or more type variables.
    For example, a generic mapping type might be defined as::

      class Mapping(Generic[KT, VT]):
          def __getitem__(self, key: KT) -> VT:
              ...
          # Etc.

    This class can then be used as follows::

      def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
          try:
              return mapping[key]
          except KeyError:
              return default
    """
    __slots__ = ()
    _is_protocol = False

    @_tp_cache
    def __class_getitem__(cls, params):
        """Parameterizes a generic class.

        At least, parameterizing a generic class is the *main* thing this method
        does. For example, for some generic class `Foo`, this is called when we
        do `Foo[int]` - there, with `cls=Foo` and `params=int`.

        However, note that this method is also called when defining generic
        classes in the first place with `class Foo(Generic[T]): ...`.
        """
        if not isinstance(params, tuple):
            params = (params,)

        params = tuple(_type_convert(p) for p in params)
        if cls in (Generic, Protocol):
            # Generic and Protocol can only be subscripted with unique type variables.
            if not params:
                raise TypeError(
                    f"Parameter list to {cls.__qualname__}[...] cannot be empty"
                )
            if not all(_is_typevar_like(p) for p in params):
                raise TypeError(
                    f"Parameters to {cls.__name__}[...] must all be type variables "
                    f"or parameter specification variables.")
            if len(set(params)) != len(params):
                raise TypeError(
                    f"Parameters to {cls.__name__}[...] must all be unique")
        else:
            # Subscripting a regular Generic subclass.
            for param in cls.__parameters__:
                prepare = getattr(param, '__typing_prepare_subst__', None)
                if prepare is not None:
                    params = prepare(cls, params)
            _check_generic(cls, params, len(cls.__parameters__))

            new_args = []
            for param, new_arg in zip(cls.__parameters__, params):
                if isinstance(param, TypeVarTuple):
                    new_args.extend(new_arg)
                else:
                    new_args.append(new_arg)
            params = tuple(new_args)

        return _GenericAlias(cls, params,
                             _paramspec_tvars=True)

    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        tvars = []
        if '__orig_bases__' in cls.__dict__:
            error = Generic in cls.__orig_bases__
        else:
            error = (Generic in cls.__bases__ and
                        cls.__name__ != 'Protocol' and
                        type(cls) != _TypedDictMeta)
        if error:
            raise TypeError("Cannot inherit from plain Generic")
        if '__orig_bases__' in cls.__dict__:
            tvars = _collect_parameters(cls.__orig_bases__)
            # Look for Generic[T1, ..., Tn].
            # If found, tvars must be a subset of it.
            # If not found, tvars is it.
            # Also check for and reject plain Generic,
            # and reject multiple Generic[...].
            gvars = None
            for base in cls.__orig_bases__:
                if (isinstance(base, _GenericAlias) and
                        base.__origin__ is Generic):
                    if gvars is not None:
                        raise TypeError(
                            "Cannot inherit from Generic[...] multiple types.")
                    gvars = base.__parameters__
            if gvars is not None:
                tvarset = set(tvars)
                gvarset = set(gvars)
                if not tvarset <= gvarset:
                    s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
                    s_args = ', '.join(str(g) for g in gvars)
                    raise TypeError(f"Some type variables ({s_vars}) are"
                                    f" not listed in Generic[{s_args}]")
                tvars = gvars
        cls.__parameters__ = tuple(tvars)

我们可以看到继承了泛型类后我们自定义的Dataset类支持Dataset[int]写法,这得益于Generic类实现了__class_getitem__(cls, params)方法。
但是我们可以注意到一个反常的现象那就是Generic__class_getitem__(cls, params)方法返回了一个_GenericAlias对象,所以Generic[T]的写法应当等价于_GenericAlias(cle, T),不应该继承Generic才对。但是我们用pycharm等工具却会发现Dataset类还是继承了Generic类,这是因为_GenericAlias继承了_BaseGenericAlias类,这个类中有一个关键的魔法方法__mro_entries__,这个类可以动态修改python类的继承关系,充分体现了python编程的灵活性。具体实现如下:

def __mro_entries__(self, bases):
    res = []
    if self.__origin__ not in bases:
        res.append(self.__origin__)
    i = bases.index(self)
    for b in bases[i+1:]:
        if isinstance(b, _BaseGenericAlias) or issubclass(b, Generic):
            break
    else:
        res.append(Generic)
    return tuple(res)

观察这个函数的实现逻辑,显然会判断是否继承自泛型类,没有就在res中添加Generic类。

两类type hint的细微区别:

def add_module(self, name: str, module: Optional['Module']) -> None: 
def add_module(self, name: str, module: Optional[Module]) -> None:

区别只在于一个单引号,大部分场景下两种用法可以等同。前者做法的优点在于可以避免一些作用域带来的问题,例如:

from typing import Union, Optional


class Module:
    def __init__(self, name: str):
        self.name = name

    def test(self, other: Optional['Module']):
        if isinstance(other, Module):
            print(f"{self.name} and {other.name} are both modules.")


Module("module1").test(Module("module2"))

此时如果去掉单引号程序会报错。

标签:__,.__,python,约束,Generic,params,泛型,class,cls
From: https://www.cnblogs.com/lrj-bupt/p/18545818

相关文章

  • python进阶——快速掌握【文件操作】(内附代码)
    1.文件操作1.0文件操作的重要性和应用场景1.1文件的基本概念1.1.1文件的概念文件是一个存储在某种持久性存储介质【硬盘、光盘、磁盘等】上的数据的结合。文件可包含各种类型的信息:文本、图像、音频、视频、应用程序代码、其他类型的二进制数据。文件通常由数据、元......
  • 带你一起全面了解关于Python网络爬虫的相关知识点!
     成长路上不孤单......
  • Python并行编程1并行编程简介(上)高频面试题:GIL进程线程协程
    1并行编程简介首先,我们将讨论允许在新计算机上并行执行的硬件组件,如CPU和内核,然后讨论操作系统中真正推动并行的实体:进程和线程。随后,将详细说明并行编程模型,介绍并发性、同步性和异步性等基本概念。介绍完这些一般概念后,我们将讨论全局解释器锁(GIL)及其带来的问题,从而了解Py......
  • 快速掌握 python进阶【异常处理】【文件操作】
    一、异常处理机制异常的定义:程序运行时发生的不正常事件。使用异常处理机制,捕获异常,处理异常。异常分为:内置异常、自定义异常。1.1内置异常处理异常处理是对异常进行捕获、抛出、处理,提高程序健壮性的机制。算法的设计要求:正确性、可读性、健壮性、高效率、低存储使用......
  • Python注意力机制Attention下CNN-LSTM-ARIMA混合模型预测中国银行股票价格|附数据代码
    全文链接:https://tecdat.cn/?p=38195原文出处:拓端数据部落公众号 股票市场在经济发展中占据重要地位。由于股票的高回报特性,股票市场吸引了越来越多机构和投资者的关注。然而,由于股票市场的复杂波动性,有时会给机构或投资者带来巨大损失。考虑到股票市场的风险,对股价变动的研究......
  • Python用CEEMDAN-LSTM-VMD金融股价数据预测及SVR、AR、HAR对比可视化
    全文链接:https://tecdat.cn/?p=38224原文出处:拓端数据部落公众号 分析师:Duqiao Han 股票市场是一个复杂的非线性系统,股价受到许多经济和社会因素的影响。因此,传统的线性或近线性预测模型很难有效、准确地预测股票指数的价格趋势。众所周知,深度学习通过逐层特征转换,将原始......
  • Python包和模块管理
    二、模块模块是什么?模块就是一个.py文件,可以定义函数、类和变量,模块内也可能包含可执行的代码。模块的作用代码重用:模块可以将代码划分为更小的单元,方便在其他文件中重复使用。组织和结构化代码:模块帮助将大型代码库分解成逻辑单元,使代码结构更清晰。避免命名冲突:模块引......
  • 自学习python之字符串2
    字符串:格式化format()方法1.位置参数(字段)2.关键字参数(相当于变量赋值)如果位置参数和关键字参数结合使用时,位置参数必须在关键字参数前面,否则报错打印花括号 格式化符号1.字符串格式化符号含义2.格式化操作符辅助命令m.n:主要是.n,m一般没用3.字......
  • K-Means聚类分析以及误差平方和SSE(Python实现)
    K-means聚类的原理。K-Means算法的目标是将原始数据分为K簇,每一簇都有一个中心点,这也是簇中点的均值点,簇中所有的点到所属的簇的中心点的距离都比到其他簇的中心点更近。K-means聚类的算法流程。1、随机确定K个点作为质心(在本次实验中,我在数据中使用随机数选择了K个点作为初始......
  • python+vue基于django/flask新农村综合风貌展示平台java+nodejs+php-计算机毕业设计
    目录技术栈和环境说明具体实现截图预期达到的目标系统设计详细视频演示技术路线解决的思路性能/安全/负载方面可行性分析论证python-flask核心代码部分展示python-django核心代码部分展示研究方法感恩大学老师和同学源码获取技术栈和环境说明本系统以Python开发语言......