是否可以在 Python 中实现类似 .NET 的属性?

Is it possible to implement .NET-like attributes in Python?

我是 .NET 属性的忠实粉丝 - 预定义和用户定义的属性。属性 classes 继承自 Attribute。大多数 .NET 中的所有内容(classes、方法、成员(属性、字段、枚举值))都可以 'decorated'/配备属性。该属性可以通过例如编译器提取编译器提示或由用户作为一种元编程。

C# 示例:

[System.Serializable]
public class SampleClass {
  // Objects of this type can be serialized.
}

VB 示例:

<System.Serializable()>
Public Class SampleClass
  ' Objects of this type can be serialized.
End Class

在我的示例中,Serializable 标记了一个 class 用于序列化。序列化程序现在可以检索 class' 实例的所有成员,并将实例数据 assemble 检索到序列化对象。也可以标记单个字段是否序列化。

用户可以在反射的帮助下从 class 中获取定义的属性:System.Attribute.GetCustomAttributes(...)

进一步阅读(MSDN 文档):
- Writing Custom Attributes
- Retrieving Information Stored in Attributes

我也是 Python 和装饰器的忠实粉丝。是否可以在装饰器的帮助下在 Python 中实现类似 .NET 的属性?在 Python 中会是什么样子?

@Serializable
class SampleClass():
  # Objects of this type can be serialized.

另一个用例可能是 Python argparse 库。可以注册回调函数,如果输入包含正确的子命令,则由子解析器调用。定义此类命令行参数语法的一种更自然的方法可能是使用装饰器。

这个问题不是关于序列化的——它只是一个用法示例。

鉴于此 Serializable 属性,您可能希望有一个简单的解决方案。

class C:
    b = 2

    def __init__(self):
        self.a = 1

    def f(self):
        pass


>>> c = C()
>>> c.__dict__
{'a': 1}

80% 的工作已经在 __dict__ 每个对象都可用的魔法属性中完成。您可能想要一个 class 级别的可序列化成员列表,并使用 __getattribute__ 魔术方法来修改您的 __dict__ 属性将 return 用于您的 class.

这同样适用于您要移植的其余 C# 属性。我不认为有一种通用的方法可以在不编写大量代码的情况下将随机属性移植到装饰器语法。所以为了简单起见,我的建议是不要拘泥于装饰器,寻找短小精悍的方式。

我玩过一些基于 class 的装饰器和 我可以说在 [=109= 中实现类似 .NET 的属性是可能的].

所以首先让我们开发一个有意义的用例:
我们大多数人都知道 Python argparse 命令行参数解析器。这个解析器可以处理像 git commit -m "message" 这样的子命令,其中 commit 是一个子命令,-m <message> 是这个子命令解析器的一个参数。可以为每个子命令解析器分配一个回调函数。

Python 3.4.2 for Windows has a bug in handling callback functions. It's fixed in 3.5.0 (I haven't tested other 3.4.x versions).

这是一个 classic argparse 示例:

class MyProg():

  def Run(self):
    # create a commandline argument parser
    MainParser = argparse.ArgumentParser(
      description = textwrap.dedent('''This is the User Service Tool.'''),
      formatter_class = argparse.RawDescriptionHelpFormatter,
      add_help=False)

    MainParser.add_argument('-v', '--verbose', dest="verbose", help='print out detailed messages', action='store_const', const=True, default=False)
    MainParser.add_argument('-d', '--debug', dest="debug", help='enable debug mode', action='store_const', const=True, default=False)
    MainParser.set_defaults(func=self.HandleDefault)
    subParsers = MainParser.add_subparsers(help='sub-command help')

    # UserManagement commads
    # create the sub-parser for the "create-user" command
    CreateUserParser = subParsers.add_parser('create-user', help='create-user help')
    CreateUserParser.add_argument(metavar='<Username>', dest="Users", type=str, nargs='+', help='todo help')
    CreateUserParser.set_defaults(func=self.HandleCreateUser)

    # create the sub-parser for the "remove-user" command
    RemoveUserParser = subParsers.add_parser('remove-user', help='remove-user help')
    RemoveUserParser.add_argument(metavar='<UserID>', dest="UserIDs", type=str, nargs='+', help='todo help')
    RemoveUserParser.set_defaults(func=self.HandleRemoveUser)

  def HandleDefault(self, args):
    print("HandleDefault:")

  def HandleCreateUser(self, args):
    print("HandleCreateUser: {0}".format(str(args.Users)))

  def HandleRemoveUser(self, args):
    print("HandleRemoveUser: {0}".format(str(args.UserIDs)))


my = MyProg()
my.Run()

一个更好、更具描述性的解决方案可能如下所示:

class MyProg():
  def __init__(self):
    self.BuildParser()
    # ...
  def BuiltParser(self):
    # 1. search self for methods (potential handlers)
    # 2. search this methods for attributes
    # 3. extract Command and Argument attributes
    # 4. create the parser with that provided metadata

  # UserManagement commads
  @CommandAttribute('create-user', help="create-user help")
  @ArgumentAttribute(metavar='<Username>', dest="Users", type=str, nargs='+', help='todo help')
  def HandleCreateUser(self, args):
    print("HandleCreateUser: {0}".format(str(args.Users)))

  @CommandAttribute('remove-user',help="remove-user help")
  @ArgumentAttribute(metavar='<UserID>', dest="UserIDs", type=str, nargs='+', help='todo help')
  def HandleRemoveUser(self, args):
    print("HandleRemoveUser: {0}".format(str(args.UserIDs)))

第 1 步 - 普通 Attribute class

所以我们来开发一个普通的Attributeclass,它也是一个基于class的装饰器。这个装饰器将自己添加到一个名为 __attributes__ 的列表中,该列表已在要装饰的函数上注册。

class Attribute():
  AttributesMemberName =  "__attributes__"
  _debug =                False

  def __call__(self, func):
    # inherit attributes and append myself or create a new attributes list
    if (func.__dict__.__contains__(Attribute.AttributesMemberName)):
      func.__dict__[Attribute.AttributesMemberName].append(self)
    else:
      func.__setattr__(Attribute.AttributesMemberName, [self])
    return func

  def __str__(self):
    return self.__name__

  @classmethod
  def GetAttributes(self, method):
    if method.__dict__.__contains__(Attribute.AttributesMemberName):
      attributes = method.__dict__[Attribute.AttributesMemberName]
      if isinstance(attributes, list):
        return [attribute for attribute in attributes if isinstance(attribute, self)]
    return list()

步骤 2 - 用户定义的属性

现在我们可以创建继承自 Attribute 的基本装饰功能的自定义属性。我将声明 3 个属性:

  • DefaultAttribute - 如果没有子命令解析器识别命令,则此修饰方法将作为回退处理程序。
  • CommandAttribute - 定义子命令并将装饰函数注册为回调。
  • ArgumentAttribute - 添加参数到子命令解析器。
class DefaultAttribute(Attribute):
  __handler = None

  def __call__(self, func):
    self.__handler = func
    return super().__call__(func)

  @property
  def Handler(self):
    return self.__handler

class CommandAttribute(Attribute):
  __command = ""
  __handler = None
  __kwargs =  None

  def __init__(self, command, **kwargs):
    super().__init__()
    self.__command =  command
    self.__kwargs =   kwargs

  def __call__(self, func):
    self.__handler = func
    return super().__call__(func)

  @property
  def Command(self):
    return self.__command

  @property
  def Handler(self):
    return self.__handler

  @property
  def KWArgs(self):
    return self.__kwargs

class ArgumentAttribute(Attribute):
  __args =   None
  __kwargs = None

  def __init__(self, *args, **kwargs):
    super().__init__()
    self.__args =   args
    self.__kwargs = kwargs

  @property
  def Args(self):
    return self.__args

  @property
  def KWArgs(self):
    return self.__kwargs

第 3 步 - 构建辅助混合 class 来处理方法上的属性

为了简化使用属性的工作,我实现了 AttributeHelperMixin class,它可以:

  • 检索class
  • 的所有方法
  • 检查一个方法是否有属性并且
  • return 给定方法的属性列表。
class AttributeHelperMixin():
  def GetMethods(self):
    return {funcname: func
            for funcname, func in self.__class__.__dict__.items()
            if hasattr(func, '__dict__')
           }.items()

  def HasAttribute(self, method):
    if method.__dict__.__contains__(Attribute.AttributesMemberName):
      attributeList = method.__dict__[Attribute.AttributesMemberName]
      return (isinstance(attributeList, list) and (len(attributeList) != 0))
    else:
      return False

  def GetAttributes(self, method):
    if method.__dict__.__contains__(Attribute.AttributesMemberName):
      attributeList = method.__dict__[Attribute.AttributesMemberName]
      if isinstance(attributeList, list):
        return attributeList
    return list()

第 4 步 - 构建应用程序 class

现在是时候构建一个继承自 MyBaseArgParseMixin 的应用程序 class。 ArgParseMixin 稍后再讨论。 class 有一个普通的构造函数,它调用两个 base-class 构造函数。它还向主解析器添加了 verbosedebug 的 2 个参数。所有回调处理程序都装饰有新的属性。

class MyBase():
  def __init__(self):
    pass

class prog(MyBase, ArgParseMixin):
  def __init__(self):
    import argparse
    import textwrap

    # call constructor of the main interitance tree
    MyBase.__init__(self)

    # Call the constructor of the ArgParseMixin
    ArgParseMixin.__init__(self,
      description = textwrap.dedent('''\
        This is the Admin Service Tool.
        '''),
      formatter_class = argparse.RawDescriptionHelpFormatter,
      add_help=False)

    self.MainParser.add_argument('-v', '--verbose',  dest="verbose",  help='print out detailed messages',  action='store_const', const=True, default=False)
    self.MainParser.add_argument('-d', '--debug',    dest="debug",    help='enable debug mode',            action='store_const', const=True, default=False)

  def Run(self):
    ArgParseMixin.Run(self)

  @DefaultAttribute()
  def HandleDefault(self, args):
    print("DefaultHandler: verbose={0}  debug={1}".format(str(args.verbose), str(args.debug)))

  @CommandAttribute("create-user", help="my new command")
  @ArgumentAttribute(metavar='<Username>', dest="Users", type=str, help='todo help')
  def HandleCreateUser(self, args):
    print("HandleCreateUser: {0}".format(str(args.Users)))

  @CommandAttribute("remove-user", help="my new command")
  @ArgumentAttribute(metavar='<UserID>', dest="UserIDs", type=str, help='todo help')
  def HandleRemoveUser(self, args):
    print("HandleRemoveUser: {0}".format(str(args.UserIDs)))

p = prog()
p.Run()

第 5 步 - ArgParseMixin 助手 class。

此 class 使用属性提供的数据构造基于 argparse 的解析器。解析过程由Run().

调用
class ArgParseMixin(AttributeHelperMixin):
  __mainParser = None
  __subParser =  None
  __subParsers = {}

  def __init__(self, **kwargs):
    super().__init__()

    # create a commandline argument parser
    import argparse
    self.__mainParser = argparse.ArgumentParser(**kwargs)
    self.__subParser =  self.__mainParser.add_subparsers(help='sub-command help')

    for funcname,func in self.GetMethods():
      defAttributes = DefaultAttribute.GetAttributes(func)
      if (len(defAttributes) != 0):
        defAttribute = defAttributes[0]
        self.__mainParser.set_defaults(func=defAttribute.Handler)
        continue

      cmdAttributes = CommandAttribute.GetAttributes(func)
      if (len(cmdAttributes) != 0):
        cmdAttribute = cmdAttributes[0]
        subParser = self.__subParser.add_parser(cmdAttribute.Command, **(cmdAttribute.KWArgs))
        subParser.set_defaults(func=cmdAttribute.Handler)

        for argAttribute in ArgumentAttribute.GetAttributes(func):
          subParser.add_argument(*(argAttribute.Args), **(argAttribute.KWArgs))

        self.__subParsers[cmdAttribute.Command] = subParser
        continue

  def Run(self):
    # parse command line options and process splitted arguments in callback functions
    args = self.__mainParser.parse_args()
    # because func is a function (unbound to an object), it MUST be called with self as a first parameter
    args.func(self, args)

  @property
  def MainParser(self):
    return self.__mainParser

  @property
  def SubParsers(self):
    return self.__subParsers

我将在 GitHub 作为 pyAttribute 存储库提供我的代码和示例。