【问题标题】:Using __new__ in inherited dataclasses在继承的数据类中使用 __new__
【发布时间】:2021-10-12 15:44:47
【问题描述】:

假设我有以下代码用于处理个人和国家之间的链接:

from dataclasses import dataclass

@dataclass
class Country:
    iso2 : str
    iso3 : str
    name : str

countries = [ Country('AW','ABW','Aruba'),
              Country('AF','AFG','Afghanistan'),
              Country('AO','AGO','Angola')]
countries_by_iso2 = {c.iso2 : c for c in countries}
countries_by_iso3 = {c.iso3 : c for c in countries}

@dataclass
class CountryLink:
    person_id : int
    country : Country

country_links = [ CountryLink(123, countries_by_iso2['AW']),
                  CountryLink(456, countries_by_iso3['AFG']),
                  CountryLink(789, countries_by_iso2['AO'])]

print(country_links[0].country.name)

这一切都很好,但我决定我想让它变得不那么笨重,以便能够处理不同形式的输入。我还想使用__new__ 来确保我们每次都获得有效的 ISO 代码,并且我想反对在这种情况下无法创建。因此,我添加了几个继承自此的新类:

@dataclass
class CountryLinkFromISO2(CountryLink):
    def __new__(cls, person_id : int, iso2 : str):
        if iso2 not in countries_by_iso2:
            return None
        new_obj = super().__new__(cls)
        new_obj.country = countries_by_iso2[iso2]
        return new_obj

@dataclass
class CountryLinkFromISO3(CountryLink):
    def __new__(cls, person_id : int, iso3 : str):
        if iso3 not in countries_by_iso3:
            return None
        new_obj = super().__new__(cls)
        new_obj.country = countries_by_iso3[iso3]
        return new_obj

country_links = [ CountryLinkFromISO2(123, 'AW'),
                  CountryLinkFromISO3(456, 'AFG'),
                  CountryLinkFromISO2(789, 'AO')]

乍一看似乎可行,但后来我遇到了问题:

a = CountryLinkFromISO2(123, 'AW')
print(type(a))
print(a.country)
print(type(a.country))

返回:

<class '__main__.CountryLinkFromISO2'>
AW
<class 'str'>

继承的对象具有正确的类型,但它的属性country 只是一个字符串,而不是我期望的Country 类型。我在__new__ 中放入了打印语句,用于检查new_obj.country 的类型,并且在return 行之前是正确的。

我想要实现的是让a 成为CountryLinkFromISO2 类型的对象,它将继承我对CountryLink 所做的更改,并使其具有从字典@ 中获取的属性country 987654336@。我怎样才能做到这一点?

【问题讨论】:

  • 您确定要覆盖__new__ 而不是__init__?你也可以考虑使用像 attrs 这样的库。
  • @NathanielFord __init__ 将始终返回该类的一个实例,如果输入无效,我不希望发生这种情况。我可以有一个引发异常的__init__,但这意味着每次我尝试调用我的代码时,我都必须将它放在一个 try/except 块中,这很笨拙并且可能导致性能问题。
  • 我认为 Mark 提供了正确的课程(工厂方法),但您应该查看 attrs 或类似库的验证器。

标签: python python-3.x inheritance python-dataclasses


【解决方案1】:

仅仅因为数据类在幕后进行,并不意味着您的类没有__init__()。他们这样做了,看起来像:

def __init__(self, person_id: int, country: Country):
    self.person_id = person_id
    self.country = country

当你创建类时:

CountryLinkFromISO2(123, 'AW')

"AW" 字符串被传递给__init__() 并将值设置为字符串。

以这种方式使用__new__() 是脆弱的,并且从构造函数返回 None 是相当不符合 Python 的 (imo)。也许您最好创建一个返回None 或您想要的类的实际工厂函数。那你就完全不用惹__new__()了。

@dataclass
class CountryLinkFromISO2(CountryLink):
    @classmethod
    def from_country_code(cls, person_id : int, iso2 : str):
        if iso2 not in countries_by_iso2:
            return None
        return cls(person_id, countries_by_iso2[iso2])

a = CountryLinkFromISO2.from_country_code(123, 'AW')

如果出于某种原因需要使用__new__(),您可以在没有匹配时从新返回None,并将国家/地区设置为__post_init__()

@dataclass
class CountryLinkFromISO2(CountryLink):
    def __new__(cls, person_id : int, iso2 : str):
        if iso2 not in countries_by_iso2:
            return None
        return super().__new__(cls)
    
    def __post_init__(self):        
        self.country = countries_by_iso2[self.country]

【讨论】:

  • 这是否意味着由数据类装饰器隐式创建的__init__ 在位置上具有相同参数的__new__ 之后运行,因此覆盖了@​​987654337@ 的先前值?从构造函数返回None 可能是非pythonic,但最好的选择是try/except 块,我知道这会导致性能问题。
  • 是的 __init__() 是由数据类装饰器创建的。它是documented here,是使用数据类的原因之一。在__new__() 之后调用__init__()。这并不特定于数据类。
  • 因此,如果我想避免 __init__ 替换 __new__ 中设置的 country 的值,我是否需要手动指定 __init____new__所以它会采用相同的位置参数但故意不覆盖?
【解决方案2】:

您看到的行为是因为数据类在__init__ 中设置了它们的字段,这发生在__new__ 运行之后。

解决这个问题的 Pythonic 方法是提供一个替代构造函数。我不会做子类,因为它们只用于它们的构造函数。

例如:

@dataclass
class CountryLink:
    person_id: int
    country: Country

    @classmethod
    def from_iso2(cls, person_id: int, country_code: str):
        try:
            return cls(person_id, countries_by_iso2[country_code])
        except KeyError:
            raise ValueError(f'invalid ISO2 country code {country_code!r}') from None

    @classmethod
    def from_iso3(cls, person_id: int, country_code: str):
        try:
            return cls(person_id, countries_by_iso3[country_code])
        except KeyError:
            raise ValueError(f'invalid ISO3 country code {country_code!r}') from None

country_links = [ CountryLink.from_iso2(123, 'AW'),
                  CountryLink.from_iso3(456, 'AFG'),
                  CountryLink.from_iso2(789, 'AO')]

【讨论】:

  • 这种方法意味着我每次创建类时都必须使用 try/except 块。这不会影响性能吗?
  • 如果没有引发异常,Try/except 块对性能的影响最小。它甚至可能更快,因为没有in 检查和if-branch。无论哪种方式,我都不认为这可能是您代码中的瓶颈。
猜你喜欢
  • 2014-05-22
  • 2011-07-06
  • 2011-10-06
  • 2021-07-10
  • 2023-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多