【问题标题】:Custom IDE-compatible static-types in PythonPython 中与 IDE 兼容的自定义静态类型
【发布时间】:2020-07-31 00:48:19
【问题描述】:

为了更好的设计和 OOP,我想创建一个自定义的 IDE 兼容的静态类型。例如,考虑以下理想化类:

class IntOrIntString(Union[int, str]):

    @staticmethod
    def is_int_string(item):
        try:
            int(item)
            return True
        except:
            return False

    def __instancecheck__(self, instance):
        # I know __instacecheck__ is declared in the metaclass. It's written here for the sake of the argument.
        return isinstance(instance, int) or (isinstance(instance, str) and self.is_int_string(instance))

    @staticmethod
    def as_integer(item):
        return int(item)

现在,我知道这是一个愚蠢的课程,但它只是一个简单的例子。定义这样的类有以下好处:

  1. 它允许在 IDE 中进行静态类型检查(例如 def parse(s: IntOrIntString): ...)。
  2. 它允许动态类型检查(例如isinstance(item, IntOrIntString))。
  3. 它可以用来更好地封装类型相关的静态函数(例如inetger = IntOrIntString.as_integer(item))。

但是,此代码不会运行,因为 Union[int, str] 不能被子类化 - 我明白了:

TypeError: 不能子类化 typing.Union

所以,我试图通过创建这个“类型”来解决这个问题,将它称为Union 的实例(实际上是)。含义:

IntOrIntString = Union[int, str]
IntOrIntString.as_integer = lambda item: int(item)
...

但是当我收到错误消息时这也不起作用

AttributeError: '_Union' 对象没有属性 'as_integer'

关于如何实现这一点的任何想法,或者,也许,为什么它不应该实现的理由?

我使用 python 3.6,但这并不是一成不变的,因为如果需要我可以更改版本。我使用的 IDE 是 PyCharm。

谢谢

编辑:另外两个有用的例子:

  1. AnyNumber 类型可以接受任何我想要的数字。也许从 floatint 开始,但可以扩展为支持我想要的任何类似数字的类型,例如 int-strings 或单项迭代。这种扩展立即在系统范围内,这是一个巨大的好处。例如,考虑函数
def func(n: AnyNumber):
    n = AnyNumber.get_as_float()
    # The rest of the function is implemented just for float.
    ...
  1. 使用pandas,您通常可以在SeriesDataFrameIndex 上执行类似的操作,因此假设有一个类似于上面称为SeriesContainer 的“类型类”,它简化了使用 - 允许我通过调用SeriesContainer.as_series_collection(...)SeriesContainer.as_data_frame(...) 来统一处理所有数据类型,具体取决于使用情况。

【问题讨论】:

  • 你能试着用常规的打字术语来澄清你想要达到的目标吗?这些示例在构造上毫无意义。例如,IntOrIntString 之类的类型要么是 int/string 的联合,要么具有附加方法——因为 int 和 str 没有该方法。 AnyNumbernumber pyramid 冲突 - 例如,复数是一个数字,但不能唯一地表示为浮点数。您是否正在寻找类型类或特征?你考虑过 ABC、singledispatch 还是第三方的 multipledispatch?
  • 不要迷失在细节中。如果我愿意,我可以改用AnyNumber.get_complexAnyNumber.get_quaternion。我也可以使用AnyRealNumberget_float。上述示例中的所有共同点是它们的功能不是与类型匹配,而是与类型的某个方面匹配(与使用MappingIterable 等python 抽象不同。在所有上述情况下,我可以轻松编写代码用于数据的“标准化”形式,但以可扩展的方式支持许多其他类型。
  • 正如一些答案和 cmets 正确显示的那样 - 我可以轻松地将上面给出的三个“优势”分开:我可以使用 Union[Int, String] 进行静态类型检查,intStr_isinstance() 用于动态类型-检查并intStr_get_number() 进行标准化。然而,我的问题是将所有三个组合到同一个类中。
  • 这似乎是您提出的一个非常复杂的要求。提供答案将涉及一些工作。请提供清晰的问题描述,这样人们就不会浪费时间在以后找出哪些细节是重要的,哪些不重要。重申一下,当前的要求不能始终如一地满足。例如,a: IntOrIntString 将允许a = 3(因为IntOrIntStringUnion[int, str])和a.as_integer(a)(因为IntOrIntString 定义as_integer)。这是错误的,因为没有 (3).as_integer("12") 这样的东西,例如。
  • 你是正确的,因为没有(3).as_integer()。但是,在编写代码时,它并不意味着这样使用,因为这是一个静态函数

标签: python python-3.x type-hinting python-internals


【解决方案1】:

如果我是你,我会避免创建这样的类,因为它们会造成不必要的类型歧义。相反,以您的示例为例,为了实现区分常规字符串和 int 字符串的目标,我将这样做。首先,创建一个(非静态的)intString 类:

from typing import Union
class intString(object):
    def __init__(self, item: str):
        try:
            int(item)
        except ValueError:
            print("error message")
            exit(1)
        self.val = item

    def __int__(self):
        return int(self.val)

(从str继承可能会更好,但我不确定如何正确地做到这一点,这对问题并不重要)。

假设我们有以下三个变量:

regular_string = "3"
int_string = intString(regular_string)
int_literal = 3

现在我们可以使用内置的python工具来实现我们的三个目标:

  1. 静态类型检查:
def foo(f: Union[int, intString]):
    pass

foo(regular_string)      # Warning
foo(3)                   # No warnings
foo(int_string)          # No warnings

您会注意到,在这里我们有比您建议的更严格的类型检查 - 即使第一个字符串可以转换为 intString,IDE 也会在运行时识别它不是一个并警告您。

  1. 动态类型检查:
print(isinstance(regular_string, (intString, int)))  # <<False
print(isinstance(int_string, (intString, int)))      # <<True
print(isinstance(int_literal, (intString, int)))     # <<True

请注意,如果元组中的任何项匹配其任何父类或它自己的类,则 isinstance 返回 true。

  1. 我不确定我是否真正理解这与封装的关系。但是由于我们在 IntString 类中定义了 int 运算符,因此我们可以根据需要使用 int 的多态性:
for i in [intString("4"), 5, intString("77"), "5"]:
    print(int(i))

将按预期打印 4,5,77。

很抱歉,如果我对这个具体的例子过于执着,但我发现很难想象像这样合并不同类型会有用的情况,因为我相信你提出的三个优点可以是以更 Pythonic 的方式实现。

我建议您查看https://docs.python.org/3/library/typing.html#newtype,了解与定义新类型相关的更多基本功能。

【讨论】:

  • 感谢您的回答,但这并不是我想要的。如果有人想在整个系统中扩展对不同参数类型的支持,这将很有用。例如,假设一个函数接受类型AnyNumber,它支持从单项列表到复数的任何内容,那么使用AnyNumber.get_as_float() 会更简单。 AnyNumber 然后很容易扩展到更多类型。与OOP的关系是将相关函数封装在AnyNumber内,而不是封装在一些外部函数AnyNumber__get_as_float()内。
  • 顺便说一句,如果你想从str继承,你通常应该继承collections.UserString
【解决方案2】:

几个想法。首先,Union[int, str] 包括所有字符串,甚至像 "9.3" 和 "cat" 这样的字符串,它们看起来不像 int

如果您对此没问题,您可以执行以下操作:

intStr = Union[int, str]

isinstance(5, intStr.__args__) # True
isinstance(5.3, intStr.__args__) # False
isinstance("5.3", intStr.__args__) # True
isinstance("howdy", intStr.__args__) # True

请注意,当使用Union 类型或起源为Union 的类型时,您必须使用.__args__ 才能使isinstance() 工作,因为isinstance() 不能与直接使用@ 987654329@s。它无法区分 Unions 和泛型类型。

不过,我假设intStr 不应包含所有字符串,而应仅包含字符串的子集。在这种情况下,为什么不将类型检查方法与类型提示分开呢?

def intStr_check(x):
    "checks if x is an instance of intStr"
    if isinstance(x, int):
        return True
    elif isinstance(x, str):
        try:
            x = int(x)
            return True
        except:
            return False
    else:
        return False

然后在检查类型是否为intStr 时,只需使用该函数代替isinstance()

请注意,您的原始方法有错误,因为 int(3.14) 不会引发错误并且会通过您的检查。

现在我们已经排除了isinstance(),如果出于解析目的,您需要区分intStr 对象和Union[int,str] 对象,您可以使用NewTypetyping,如下所示:

from typing import NewType

IntStr = NewType("IntStr", Union[int,str])

def some_func(a: IntStr):
    if intStr_check(a):
        return int(a) + 1
    else:
        raise ValueError("Argument must be an intStr (an int or string of an int)")


some_num = IntStr("9")

print(some_func(some_num)) # 10

无需创建as_integer()函数或方法,因为它与int()完全相同,更简洁易读。


我对风格的看法:不应该仅仅为了 OOP 而做任何事情。当然,有时您需要存储状态和更新参数,但在不必要的情况下,我相信 OOP 往往会导致更冗长的代码,并且可能更令人头疼的是维护可变状态和避免意外的副作用。因此,我更喜欢仅在必要时声明新类。


编辑:由于您坚持重复使用函数名称isinstance,您可以覆盖isinstance 以添加额外的功能,如下所示:

from typing import NewType, Union, _GenericAlias

isinstance_original = isinstance

def intStr_check(x):
    "checks if x is an instance of intStr"
    if isinstance_original(x, int):
        return True
    elif isinstance_original(x, str):
        try:
            x = int(x)
            return True
        except:
            return False
    else:
        return False

def isinstance(x, t):
    if (t == 'IntStr'): # run intStr_check
        return intStr_check(x)
    elif (type(t) == _GenericAlias): # check Union types
        try:
            check = False
            for i in t.__args__:
                check = check or isinstance_original(x,i)
                if check == True: break
            return check
        except:
            return isinstance_original(x,t)
    else: # regular isinstance
        return isinstance_original(x, t)

# Some tests
assert isinstance("4", 'IntStr') == True
assert isinstance("4.2", 'IntStr') == False
assert isinstance("4h", 'IntStr') == False
assert isinstance(4, 'IntStr') == True
assert isinstance(4.2, int) == False
assert isinstance(4, int) == True
assert isinstance("4", int) == False
assert isinstance("4", str) == True
assert isinstance(4, Union[str,int]) == True
assert isinstance(4, Union[str,float]) == False

注意不要多次运行isinstance_original = isinstance

您仍然可以使用IntStr = NewType("IntStr", Union[int,str]) 进行静态类型检查,但由于您喜欢 OOP,您还可以执行以下操作:

class IntStr:
    "an integer or a string of an integer"
    def __init__(self, value):
        self.value = value
        if not (isinstance(self.value, 'IntStr')):
            raise ValueError(f"could not convert {type(self.value)} to IntStr (an int or string of int): {self.value}")

    def check(self):
        return isinstance(self.value, 'IntStr')

    def as_integer(self):
        return int(self.value)

    def __call__(self):
        return self.value

# Some tests
try:
    a = IntStr("4.2")
except ValueError:
    print("it works")

a = IntStr("4")

print(f"a == {a()}")

assert a.as_integer() + 1 == 5
assert isinstance(a, IntStr) == True
assert isinstance(a(), str) == True
assert a.check() == True

a.value = 4.2

assert a.check() == False

【讨论】:

  • 你也太沉迷于这个具体的例子——这几乎不是我想要的。阅读我对@mattan 的回答发表评论的示例。但是,你给了我一些我可以尝试和使用的想法。感谢那。另外,“为了OOP”是我不会声明IntStr = NewType("IntStr", Union[int,str]),然后每次都必须使用intStr_check,而是将这个功能封装在同一个地方——只需使用IntStr.check,例如,或者更好 - isinstance(x, IntStr).
  • 也许您可以提供更多示例来澄清您的问题?在我看来,您的具体示例可以而且可能应该在没有 OOP 的情况下完成,因为 Python 中类型系统的所有内容都有些混乱。也许您可能对使用 Julia 等语言更感兴趣?
  • 我想如果你想把所有东西都放在同一个地方,你可以制作 isinstance() 的修改版本,但名称不同,只要你测试时使用常规的 isinstance() 方法除了'IntStr'
  • 现在不能选择搬到 Julia。我在原始问题中包含了更多示例。关于修改isinstance,我宁愿插入现有的语法/实现,而不是重写我所有的使用my_isinstance
  • 我会给你赏金,因为你的答案比另一个好一点(对不起@mattan),我不想看到代表浪费了,但它仍然没有回答我的问题。
猜你喜欢
  • 1970-01-01
  • 2023-02-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-15
  • 2015-11-09
  • 2018-06-21
相关资源
最近更新 更多