Python中的__pre_init__功能是什么?

5
我想要实现字符串比较时不区分大小写。 为此,我想创建一个只有一个字符串字段的不可变类。 在构造函数中,我希望在将值赋给字段之前调用lower()方法。
我希望尽可能多地使用标准类,如namedtuple或dataclass。 使用__post_init__函数(参见例如如何在Python的Dataclasses中使用__post_init__方法)感觉像是一种技巧。 这也让我想知道,在__post_init__函数中修改字段后,该字段是否仍然是不可变的。
然而,我找不到__pre_init__函数。 有没有更好的方法?

2
“在将值分配给字段之前,我想调用lower()方法。”-- 那么,有什么阻止你这样做吗?如果有,请详细说明。如果没有,那还有什么问题呢? - fountainhead
1
为什么在你的常规初始化中不使用.lower()就不够了呢?作为一个业余爱好者,也许我缺乏一些SE知识... - Kraay89
1
一个 pre_init - 一种在 __init____new__ 之前被调用的方法。请查看此处以获取更多信息: https://spyhce.com/blog/understanding-new-and-init - balderman
例如,覆盖NamedTuple的__init__如下所示是不允许的:class Name(NamedTuple): name: str def __init__(self, name: str) -> None: def canonical_representation() -> str: return name.lower() self.name: str = canonical_representation()会出现以下错误:File "C:\Users\laarpjljvd\AppData\Local\Programs\Python\Python39\lib\typing.py", line 1775, in __new__ raise AttributeError("Cannot overwrite NamedTuple attribute " + key) - Pierre van de Laar
@PierrevandeLaar,我不确定你在问什么,但是我猜测了一下并留下了我的答案。如果这不是你想要的,请更新你的问题以澄清。谢谢! - Mack
显示剩余2条评论
5个回答

5
原来,dataclasses 没有提供我所需要的功能。而Attrs则提供了:
from attr import attrs, attrib


@attrs(frozen=True)
class Name:
    name: str = attrib(converter=str.lower)

3

澄清:如果我理解问题正确,您特别想使用带有stdlib注释的类dataclasses.dataclass(frozen=True)。具体而言,对于您的情况,您可能会有以下内容:

from dataclasses import dataclass


@dataclass(frozen=True)
class HasStringField:
    some_string: str


instance = HasStringField("SOME_STRING")
instance.some_string  # value: "SOME_STRING"

我的理解是您想实现一些东西,以便上面的instance.some_string中的值实际上是"some_string"

答案:没有“内置”的方法可以做到您想要的,但有两个选项:在__post_init__中使用object.__setattr__或使用另一种提供自定义构造函数的替代装饰器,该装饰器仍然调用由dataclass创建的构造函数。

选项1:在__post_init__中使用object.__setattr__

@dataclasses.dataclass(frozen=True)
class HasStringField:
    some_string: str

    def __post_init__(self):
        object.__setattr__(self, "some_string", self.some_string.lower())


inst = HasStringField("SOME_STRING")
inst.some_string  # value: "some_string"

选项2:自定义装饰器

dataclass_with_default_init 装饰器在类似问题的答案中提供了一个这样的装饰器。

使用 dataclass_with_default_init 装饰器,您可以这样做:

# Import / implement the decorator

@dataclass_with_default_init(frozen=True)
class HasStringField:
    some_string: str

    def __init__(self, some_string: str):
        self.__default_init__(some_string=some_string.lower())


instance = HasStringField("SOME_STRING")
instance.some_string  # value: "some_string"

...而此类的其他所有内容仍然与使用dataclass装饰器时相同。


0

Python的预初始化方法是__new__

无法在冻结的数据类上设置属性,因此我在这里使用了一个命名元组。

from collections import namedtuple

_WithString = namedtuple("WithString", ["stringattr"])


class WithString:
    def __new__(cls, input_string):
        return _WithString(input_string.lower())


string_1 = WithString("TEST")
string_2 = WithString("test")
print("instances:", string_1, string_2)
print("attributes:", string_1.stringattr, string_2.stringattr)
print("compare instances:", string_1 == string_2)
print("compare attributes:", string_1.stringattr == string_2.stringattr)

输出:

instances: WithString(stringattr='test') WithString(stringattr='test')
compare instances: True

你也可以在 __init__ 中这样做:

相同的行为,但这次使用数据类。你需要绕过冻结属性

from dataclasses import dataclass


@dataclass(frozen=True)
class WithString:
    stringattr: str

    def __init__(self, input_string):
        object.__setattr__(self, "stringattr", input_string.lower())


string_1 = WithString("TEST")
string_2 = WithString("test")
print("instances:", string_1, string_2)
print("compare instances:", string_1 == string_2)

输出:

instances: WithString(stringattr='test') WithString(stringattr='test')
compare instances: True

或者只需覆盖str

生成的对象的行为很像字符串,也可以与字符串进行比较。

class CaseInsensitiveString(str):
    def __eq__(self, other):
        try:
            return self.lower() == other.lower()
        except AttributeError:
            return NotImplemented


string_1 = CaseInsensitiveString("TEST")
string_2 = CaseInsensitiveString("test")
actual_string = "Test"
print("instances:", string_1, string_2)
print("compare instances:", string_1 == string_2)
print("compare left:", string_1 == actual_string)
print("compare right:", actual_string == string_1)

输出:

instances: TEST test
compare instances: True
compare left: True
compare right: True

0
注意:我仍然认为最好的方法是使用上面提到的__post_init__ / object.__set_attribute__或者使用外部库,比如https://www.attrs.org/en/stable/)

我在阅读数据类本身的Python代码时看到的一种模式是使用相同名称但采用蛇形命名法的函数来进行这些更改。例如:

@dataclass(frozen=True)
class HasStringField:
  some_string: str


def has_string_field(some_string: str) -> HasStringField:
  return HasStringField(some_string.lower())

那么你总是导入并使用 has_string_field() 而不是 HasStringField()。(此模式的一个示例是 dataclasses 中的 field vs Field):

它的缺点是冗长,使类不变量可被绕过。


0

这应该是最简单的方法之一,您提到的字段是内部data(继承自UserString类):

from collections import UserString

class LowerStr(UserString):
    def __init__(self, value):
        super().__init__(value)
        self.data = self.data.lower()

s1 = LowerStr("ABC")
print("s1: {}".format(s1))
print("s1 class: {}".format(s1.__class__))
print("s1 class mro: {}".format(s1.__class__.mro()))

输出:

s1: abc
s1 class: <class '__main__.LowerStr'>
s1 class mro: [<class '__main__.LowerStr'>, <class 'collections.UserString'>, <class 'collections.abc.Sequence'>, <class 'collections.abc.Reversible'>, <class 'collections.abc.Collection'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>, <class 'object'>]

这将为您提供所有来自str的方法,并且您可以随意自定义它们。

如果您喜欢子类化,那么就没有内部data属性:

class InsensitiveStr(str):
    def __new__(cls, value):
        if isinstance(value, str):
            return super().__new__(cls, value.lower())
        else:
            raise ValueError

s1 = InsensitiveStr("ABC")
print(s1)
print(type(s1))
print(s1.__class__)
print(s1.__class__.__mro__)

注意mro中的差异。
输出
abc
<class '__main__.InsensitiveStr'>
<class '__main__.InsensitiveStr'>
(<class '__main__.InsensitiveStr'>, <class 'str'>, <class 'object'>)

这个类不是不可变的。可以更改数据字段的值。 - Pierre van de Laar
s1 就像一个不可变的字符串。当然,除了 s1.data 以外。您也可以使用相同的想法进行子类化。请参见我的编辑。 - progmatico
请参见此处以获取类似的讨论。 - progmatico

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