是否可以在 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
所以我们来开发一个普通的Attribute
class,它也是一个基于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
现在是时候构建一个继承自 MyBase
和 ArgParseMixin
的应用程序 class。 ArgParseMixin
稍后再讨论。 class 有一个普通的构造函数,它调用两个 base-class 构造函数。它还向主解析器添加了 verbose 和 debug 的 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 存储库提供我的代码和示例。
我是 .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
所以我们来开发一个普通的Attribute
class,它也是一个基于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
现在是时候构建一个继承自 MyBase
和 ArgParseMixin
的应用程序 class。 ArgParseMixin
稍后再讨论。 class 有一个普通的构造函数,它调用两个 base-class 构造函数。它还向主解析器添加了 verbose 和 debug 的 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 存储库提供我的代码和示例。