如何在Python中创建一个FractionEnum而不引发元类冲突?

huangapple go评论89阅读模式
英文:

How can I create a FractionEnum in Python without a metaclass conflict?

问题

I am trying to create a FractionEnum similar to StrEnum or IntEnum. My first attempt resulted in a metaclass conflict:

class FractionEnum(fractions.Fraction, Enum):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

I followed the suggestion from this answer https://stackoverflow.com/questions/69005034/multiple-inheritance-metaclass-conflict-involving-enum and created a new metaclass:

class FractionEnumMeta(type(Enum), type(fractions.Fraction)):
    pass

class FractionEnum(fractions.Fraction, Enum, metaclass=FractionEnumMeta):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9

This solved the above error but now I get:

  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/enum.py", line 289, in __new__
    enum_member = __new__(enum_class, *args)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/fractions.py", line 93, in __new__
    self = super(Fraction, cls).__new__(cls)

TypeError: Enum.__new__() missing 1 required positional argument: 'value'

The issue seems to be that the __new__ call inside Fraction is trying to create an enum, from the call inside the EnumMeta metaclass:

            else:
                enum_member = __new__(enum_class, *args)

I'm misunderstanding how the metaclasses can work together to create an object that is both a fraction and an Enum - it seems to work out of the box with int or str or classes that don't define a metaclass.

Update:

I was able to use the code below to have the enumeration replace the Fraction's new method, but I am getting an error if I try deepcopy a class that has the enum as a member:

/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/enum.py:497: in _create_
    _, first_enum = cls._get_mixins_(cls, bases)

in the Enum code:

        # ensure final parent class is an Enum derivative, find any concrete
        # data type, and check that Enum has no members
        first_enum = bases[-1]
        if not issubclass(first_enum, Enum):
            raise TypeError("new enumerations should be created as "
                    "`EnumName([mixin_type, ...] [data_type,] enum_type)`")
        member_type = _find_data_type(bases) or object
        if first_enum._member_names_:
>           raise TypeError("Cannot extend enumerations")
E           TypeError: Cannot extend enumerations

Sample to reproduce:

class TestFractionEnum(FractionEnum):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9

class C:
    def __init__(self):
        self.fraction_enum = TestFractionEnum.VALUE_1

c = C()
print(c)
print(c.fraction_enum)
d = copy.copy(c)
print(d)
e = copy.deepcopy(c)
print(e)

Update 2:

Overriding deepcopy on the enum seems to work:

def __deepcopy__(self, memo):
        if type(self) == Fraction:
            return self
        for item in self.__class__:
            if self == item:
                return item
        assert f'Invalid enum: {self}'
英文:

I am trying to create a FractionEnum similar to StrEnum or IntEnum. My first attempt resulted in a metaclass conflict:

class FractionEnum(fractions.Fraction, Enum):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

I followed the suggestion from this answer https://stackoverflow.com/questions/69005034/multiple-inheritance-metaclass-conflict-involving-enum and created a new metaclass:

class FractionEnumMeta(type(Enum), type(fractions.Fraction)):
    pass

class FractionEnum(fractions.Fraction, Enum, metaclass=FractionEnumMeta):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9

This solved the above error but now I get:

  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/enum.py", line 289, in __new__
    enum_member = __new__(enum_class, *args)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/fractions.py", line 93, in __new__
    self = super(Fraction, cls).__new__(cls)

TypeError: Enum.__new__() missing 1 required positional argument: 'value'

The issue seems to be that the __new__ call inside Fraction is trying to create an enum, from the call inside the EnumMeta metaclass:

            else:
                enum_member = __new__(enum_class, *args)

I'm misunderstanding how the metaclasses can work together to create an object that is both a fraction and an Enum - it seems to work out of the box with int or str or classes that don't define a metaclass.

Update:

I was able to use the code below to have the enumeration replace the Fraction's new method, but I am getting an error if I try deepcopy a class that has the enum as a member:

/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/enum.py:497: in _create_
    _, first_enum = cls._get_mixins_(cls, bases)

in the Enum code:

        # ensure final parent class is an Enum derivative, find any concrete
        # data type, and check that Enum has no members
        first_enum = bases[-1]
        if not issubclass(first_enum, Enum):
            raise TypeError("new enumerations should be created as "
                    "`EnumName([mixin_type, ...] [data_type,] enum_type)`")
        member_type = _find_data_type(bases) or object
        if first_enum._member_names_:
>           raise TypeError("Cannot extend enumerations")
E           TypeError: Cannot extend enumerations

Sample to reproduce:

class TestFractionEnum(FractionEnum):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9

class C:
    def __init__(self):
        self.fraction_enum = TestFractionEnum.VALUE_1

c = C()
print(c)
print(c.fraction_enum)
d = copy.copy(c)
print(d)
e = copy.deepcopy(c)
print(e)

Update 2:

Overriding deepcopy on the enum seems to work:

def __deepcopy__(self, memo):
        if type(self) == Fraction:
            return self
        for item in self.__class__:
            if self == item:
                return item
        assert f'Invalid enum: {self}'

答案1

得分: 3

正如 @jsbueno 所说,您需要编写自己的 __new__

from enum import EnumType, Enum
from fractions import Fraction
import math

class FractionEnumMeta(type(Fraction), EnumType):
    pass

class FractionEnum(Fraction, Enum, metaclass=FractionEnumMeta):
    def __new__(cls, numerator=0, denominator=None):
        #
        # 这是与 Fraction.__new__ 唯一不同的一行
        self = object.__new__(cls)
        #
        #
        if denominator is None:
            if type(numerator) is int:
                self._numerator = numerator
                self._denominator = 1
                return self
            elif isinstance(numerator, numbers.Rational):
                self._numerator = numerator.numerator
                self._denominator = numerator.denominator
                return self
            elif isinstance(numerator, (float, Decimal)):
                # 精确转换
                self._numerator, self._denominator = numerator.as_integer_ratio()
                return self
            elif isinstance(numerator, str):
                # 处理从字符串构造的情况。
                m = _RATIONAL_FORMAT.match(numerator)
                if m is None:
                    raise ValueError('Invalid literal for Fraction: %r' %
                                     numerator)
                numerator = int(m.group('num') or '0')
                denom = m.group('denom')
                if denom:
                    denominator = int(denom)
                else:
                    denominator = 1
                    decimal = m.group('decimal')
                    if decimal:
                        decimal = decimal.replace('_', '')
                        scale = 10**len(decimal)
                        numerator = numerator * scale + int(decimal)
                        denominator *= scale
                    exp = m.group('exp')
                    if exp:
                        exp = int(exp)
                        if exp >= 0:
                            numerator *= 10**exp
                        else:
                            denominator *= 10**-exp
                if m.group('sign') == '-':
                    numerator = -numerator
            else:
                raise TypeError("argument should be a string "
                                "or a Rational instance")
        elif type(numerator) is int is type(denominator):
            pass  # *very* normal case
        elif (isinstance(numerator, numbers.Rational) and
            isinstance(denominator, numbers.Rational)):
            numerator, denominator = (
                numerator.numerator * denominator.denominator,
                denominator.numerator * numerator.denominator
                )
        else:
            raise TypeError("both arguments should be "
                            "Rational instances")
        if denominator == 0:
            raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
        g = math.gcd(numerator, denominator)
        if denominator < 0:
            g = -g
        numerator //= g
        denominator //= g
        self._numerator = numerator
        self._denominator = denominator
        return self

class TestFractionEnum(FractionEnum):
    VALUE_1 = 1, 1
    VALUE_2 = 8, 9

在这种情况下,我们只是重用了 Fraction.__new__,但删除了 super() 调用并直接创建了新的分数实例。

英文:

As @jsbueno said, you have to write your own __new__:

from enum import EnumType, Enum
from fractions import Fraction
import math
class FractionEnumMeta(type(Fraction), EnumType):
pass
class FractionEnum(Fraction, Enum, metaclass=FractionEnumMeta):
def __new__(cls, numerator=0, denominator=None):
#
# this is the only line different from Fraction.__new__
self = object.__new__(cls)
#
#
if denominator is None:
if type(numerator) is int:
self._numerator = numerator
self._denominator = 1
return self
elif isinstance(numerator, numbers.Rational):
self._numerator = numerator.numerator
self._denominator = numerator.denominator
return self
elif isinstance(numerator, (float, Decimal)):
# Exact conversion
self._numerator, self._denominator = numerator.as_integer_ratio()
return self
elif isinstance(numerator, str):
# Handle construction from strings.
m = _RATIONAL_FORMAT.match(numerator)
if m is None:
raise ValueError(&#39;Invalid literal for Fraction: %r&#39; %
numerator)
numerator = int(m.group(&#39;num&#39;) or &#39;0&#39;)
denom = m.group(&#39;denom&#39;)
if denom:
denominator = int(denom)
else:
denominator = 1
decimal = m.group(&#39;decimal&#39;)
if decimal:
decimal = decimal.replace(&#39;_&#39;, &#39;&#39;)
scale = 10**len(decimal)
numerator = numerator * scale + int(decimal)
denominator *= scale
exp = m.group(&#39;exp&#39;)
if exp:
exp = int(exp)
if exp &gt;= 0:
numerator *= 10**exp
else:
denominator *= 10**-exp
if m.group(&#39;sign&#39;) == &#39;-&#39;:
numerator = -numerator
else:
raise TypeError(&quot;argument should be a string &quot;
&quot;or a Rational instance&quot;)
elif type(numerator) is int is type(denominator):
pass # *very* normal case
elif (isinstance(numerator, numbers.Rational) and
isinstance(denominator, numbers.Rational)):
numerator, denominator = (
numerator.numerator * denominator.denominator,
denominator.numerator * numerator.denominator
)
else:
raise TypeError(&quot;both arguments should be &quot;
&quot;Rational instances&quot;)
if denominator == 0:
raise ZeroDivisionError(&#39;Fraction(%s, 0)&#39; % numerator)
g = math.gcd(numerator, denominator)
if denominator &lt; 0:
g = -g
numerator //= g
denominator //= g
self._numerator = numerator
self._denominator = denominator
return self
class TestFractionEnum(FractionEnum):
VALUE_1 = 1, 1
VALUE_2 = 8, 9

In this case, we just reuse Fraction.__new__, but remove the super() call and create the new fraction instance directly.


Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

huangapple
  • 本文由 发表于 2023年7月7日 00:55:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76631028.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定