有没有一个Python类/枚举用于标志/位掩码操作?

31

我知道基类EnumIntEnum,它们都非常有用,但我想要一些支持标志位操作的功能。我不指望这两个类能够实现我所需的功能。

让我们构建一个例子:

class NetlistKind(IntEnum):
  Unknown = 0
  LatticeNetlist = 1
  QuartusNetlist = 2
  XSTNetlist = 4
  CoreGenNetlist = 8
  All = 15

正如您所看到的,我已经使用 IntEnum 为此枚举获取算术特性。如果有类似于 @unique 的东西来确保所有值都是二的幂次方就太好了。 我可以通过针对我的需求分叉 enum.unique 来实现这一点。(我知道 All 是该规则的例外。)

这样的枚举类型如何使用?

filter = NetlistKind.LatticeNetlist | NetlistKind.QuartusNetlist

由于底层 int 位运算的支持,可以进行一些操作,并且 filter 具有内部值为 3。

如果有一个“在过滤器 Y 中设置标志 X”函数,甚至更好的是一个运算符,这将会很不错。我会添加一个魔法函数 x in y

@unique
class NetlistKind(IntEnum):
  Unknown = 0
  LatticeNetlist = 1
  QuartusNetlist = 2
  XSTNetlist = 4
  CoreGenNetlist = 8
  All = 15

  def __contains__(self, item):
    return  (self.value & item.value) == item.value

使用示例:

....
  def GetNetlists(self, filter=NetlistKind.All):
    for entity in self._entities:
      for nl in entity.GetNetlists():
        if (nl.kind in filter):
          yield nl

  def GetXilinxNetlists(self):
    return self.GetNetlists(NetlistKind.XSTNetlist | NetlistKind.CoreGenNetlist)

所以问题是:

  • 是否有更好的实现位域的方法?
  • 是否有更好的实现这种一维滤波器的方法?我不想为如此简单的过滤条件使用lambda表达式?
  • Python标准库中已经包含了这样的解决方案吗?
  • 如何将此枚举扩展添加到下一个Python版本中? :)

开放特性:

  • __str__中返回所有活动标志的列表
  • ...?

(T)

1
我最近给我的标志库添加了单元测试,并将其发布到了pypi上。接下来几天,我将完成它的README.rst并添加一些额外的功能。它的接口受到python3标准enum模块的很大影响。如果您有兴趣,请看一下:https://pypi.python.org/pypi/py-flags 我已经看到有关于是否使用标志作为Pythonic方式的讨论。我的未来更新到README.rst将包括一个部分,讨论使用多个布尔值作为函数参数或将布尔值存储在对象或字典中与使用集合VS使用标志的利弊。 - pasztorpisti
1
请将您的评论发布为答案,以便我可以点赞!看起来非常不错和成熟。只有一个问题:为什么我需要为枚举提供完全限定名(FQN)?例如:TextStyle('TextStyle.bold')。我认为 bold 就足够了,因为命名空间已经限制为 TextStyle,因为您将其传递给它的构造函数。 - Paebbels
1
很抱歉,仅提供链接的答案在SO上是不被欢迎的。枚举的str()可以在其他情况下使用,而不仅仅是在序列化的情况下,这就是为什么__str__返回fqdn的原因。我认为即使在没有标志类的情况下,str()也应该是可解释的。实际上,为了进行自定义序列化,我除了标准的__str__之外还提供了一个to_simple_str()。在这种情况下,to_simple_str()将只发出'bold',而TextStyle('bold')也可以工作。实际上,pickle序列化器支持标志仅保存标志类名称和to_simple_str()的输出。 - pasztorpisti
1
我其实一直在思考是否在 str() 中返回 fqdn。我曾经想过在 str() 中返回非 fqdn,只在 repr() 中返回 fqdn,或者使用一个实用函数来返回 fqdn,但最终我决定采用 fqdn 的方式,以与标准 enum 模块的 str() 行为相同。 - pasztorpisti
1
关于仅提供链接的答案:请添加一个使用您的类/语法/...替换我的初始示例的py-flags库示例。然后添加到PyPI(文档,下载)的链接。我对我的库也是这样做的,从未在SO上遇到过仅链接帖子或垃圾邮件的问题。如果您解释了您的解决方案如何解决我的问题,然后链接到常见的下载平台,那么这是完全有效的。如果它是开源而不是商业产品,则规则会放松 :) - Paebbels
3个回答

57
Python 3.6增加了FlagIntFlag,支持通常的位运算。作为奖励,位运算的结果值仍然是原始标志类的成员,并且是单例[1]。 aenum库也具有此功能,并可在Python 2.7中使用。
[1] 3.6.0存在一个错误:如果伪标志成员在线程中被创建,则可能会出现重复;这在3.6.1中得到修复(并且在aenum中从未存在)。

谢谢Ethan。@pasztorpisti提供的py-flags模块非常强大。也许Python 3.6应该研究一下他的模块(基于元类),并且整合一些功能。=> https://github.com/pasztorpisti/py-flags - Paebbels
1
@Paebbels:看起来它与stdlib版本非常相似。 - Ethan Furman
3
对于基本功能而言,它们几乎是相当的,所以使用Python3.6+的用户应该考虑使用标准库版本。py-flags 包含了一些额外的功能,这些功能可能被认为是好的或者不好的,如果需要,开发者实际上可以使用继承将这些功能添加到标准库版本中。作为设计决策,py-flags 故意避免将 Flags 派生自 Enum。我认为枚举和标志(如果需要)之间的正确关系应该像Pascal语言中枚举和"枚举集合"类型之间的关系:http://wiki.freepascal.org/Set - pasztorpisti
aenum库非常棒,正是我在发现只有3.6版本才有新的Flag功能时所需要的。我无法确定它是否只是将3.6代码拿来回溯移植,但它运行良好。 - mbrig

23

我最近发布了一个开源软件包py-flags,旨在解决这个问题。该库具有完全相同的功能,并且其设计受到Python3枚举模块的重大影响。

关于是否足够符合Python风格来实现这样的flags类存在争议,因为它的功能与语言提供的其他方法有很大重叠(布尔变量集合、带布尔属性的对象或带布尔项的字典等)。因此,我认为flags类过于狭隘和/或冗余,不应该成为标准库的一部分,但在某些情况下,它比前面列出的解决方案更好,因此安装“pip”的库可以派上用场。

使用py-flags模块,您的示例将如下所示:

from flags import Flags

class NetlistKind(Flags):
    Unknown = 0
    LatticeNetlist = 1
    QuartusNetlist = 2
    XSTNetlist = 4
    CoreGenNetlist = 8
    All = 15
上述内容可以进一步调整一下,因为库中声明的标志类会自动提供两个“虚拟”标志:NetlistKind.no_flagsNetlistKind.all_flags。这使已经声明的NetlistKind.UnknownNetlistKind.All变得多余,因此我们可以在声明中将它们省略,但问题在于no_flagsall_flags与您的命名约定不符。为了解决这个问题,我们在您的项目中声明了一个标志基类,而不是使用 flags.Flags,您在项目的其余部分中必须使用它:
from flags import Flags

class BaseFlags(Flags):
    __no_flags_name__ = 'Unknown'
    __all_flags_name__ = 'All'

根据之前声明的基类,可以由您项目中的任何标志进行子类化,因此我们可以更改您的标志声明为:

class NetlistKind(BaseFlags):
    LatticeNetlist = 1
    QuartusNetlist = 2
    XSTNetlist = 4
    CoreGenNetlist = 8

这样 NetlistKind.Unknown 就会自动声明一个值为零的枚举成员。 NetlistKind.All 也存在,并且它自动包含您声明的所有标志的组合。可以使用/不使用这些虚拟标志来迭代枚举成员。还可以声明别名(与先前声明的其他标志具有相同的值的标志)。

作为另一种声明方式,可以使用“函数调用风格”(也由标准的枚举模块提供):

NetlistKind = BaseFlags('NetlistKind', ['LatticeNetlist', 'QuartusNetlist',
                                        'XSTNetlist', 'CoreGenNetlist'])
如果一个标志类声明了一些成员,那么它被认为是final。尝试对其进行子类化将导致错误。在语义上不希望允许为添加新成员或更改功能而对标志类进行子类化。
此外,标志类以类型安全的方式提供您列出的运算符(布尔运算符、in运算符、迭代等等...)。我将在接下来的几天内完成README.rst,并在软件包接口上进行一些小修补,但基本功能已经存在并测试了相当好的覆盖率。

@Paebbels 感谢您的慷慨,但我通常在这里不是为了积分。当我太累做其他事情时,我会访问stackoverflow。:-D 我正在处理lib及其README.rst文件。更详细的文档与更好的接口描述以及一些设计哲学将很快在接下来的几天内提供。当前文档非常基础,如您所见,但即使在较大的文档中,我通常也从类似的TL;DR部分开始...如果您对即将推出的较长版本感兴趣,请查询该库的pypi或github页面。 - pasztorpisti
有时候我会添加赏金以表彰优秀的工作,特别是当它能够节省我自己编写代码的时间 :). 所以你的库现在被用在了“PoC-Library”中。我认为我应该找到一种好的方式来描述PoC的依赖关系。我们没有setup.py,因为PoC没有被安装。Python脚本只是一个后台基础设施... 我应该为此写一个新问题... - Paebbels
1
对于应用程序,人们通常只使用每行1个依赖项的requirements.txt文件和pip install -r requirements.txt命令。每行requirements.txt都包含pip install的参数列表,例如:whatever_libmock>=1.2.0-r inherited_base_requirements.txt - pasztorpisti
1
我认为我发现了一个小问题:None是Python中的关键字,而不是标识符,因此不能用作枚举成员。 NetlistKind.None会报告语法错误。我已经在深层子目录中为Travis-CI准备了requirements.lst ... 我将其移动到根目录中。谢谢。 - Paebbels
1
@Paebbels 在 requirements.txt 中添加您的依赖项及其固定版本,例如:flags==1.0.1 而不是简单地写成 flags。这样,如果 pypi 中的库被更新(或在最坏的情况下多个库被更新),则您的应用程序不会受到影响。最好在需要时手动升级并增加版本号。 - pasztorpisti
显示剩余4条评论

6

我认为在这里举一个Python原生标志和按位运算的例子会很不错。

from enum import Flag, auto


class FoodCategory(Flag):
    FISH = auto()
    SHELLFISH = auto()
    TREE_NUTS = auto()
    PEANUTS = auto()
    WHEAT = auto()
    RICE = auto()
    MILK = auto()
    BUTTER = auto()
    ALLERGENS = TREE_NUTS | PEANUTS | WHEAT | MILK


if __name__ == "__main__":
    DAIRY = FoodCategory.MILK | FoodCategory.BUTTER
    print(f"{FoodCategory.MILK in DAIRY = }")
    print(f"{FoodCategory.SHELLFISH in DAIRY = }")
    print(f"{FoodCategory.WHEAT in FoodCategory.ALLERGENS = }")
    print(f"{FoodCategory.SHELLFISH in FoodCategory.ALLERGENS = }")

输出:

FoodCategory.MILK in DAIRY = True
FoodCategory.SHELLFISH in DAIRY = False
FoodCategory.WHEAT in FoodCategory.ALLERGENS = True
FoodCategory.SHELLFISH in FoodCategory.ALLERGENS = False

这很好,我不知道 Flag 是以这种方式实现 __contains__ 的。感觉比使用 检查是否已设置某些标志更符合惯用法。你是否知道是否还有另一种取消单个标志的替代方法?(例如,禁用牛奶的替代方法是 flags &= ~FoodCategory.MILK - Seb

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