英文:
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('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
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论