编辑[BLUF]:@martineau 提供的答案没有问题,这篇文章只是为了跟进完成,讨论在类定义中使用附加关键字时遇到的潜在错误不是由元类管理的。
我想提供一些关于使用__init_subclass__ 以及使用__new__ 作为工厂的附加信息。 @martineau 发布的答案非常有用,我在自己的程序中实现了它的修改版本,因为我更喜欢使用类创建序列而不是向命名空间添加工厂方法;与pathlib.Path 的实现方式非常相似。
为了跟进 @martinaeu 的评论跟踪,我从他的回答中提取了以下 sn-p:
import os
import re
class FileSystem(object):
class NoAccess(Exception): pass
class Unknown(Exception): pass
# Regex for matching "xxx://" where x is any non-whitespace character except for ":".
_PATH_PREFIX_PATTERN = re.compile(r'\s*([^:]+)://')
_registry = {} # Registered subclasses.
@classmethod
def __init_subclass__(cls, /, **kwargs):
path_prefix = kwargs.pop('path_prefix', None)
super().__init_subclass__(**kwargs)
cls._registry[path_prefix] = cls # Add class to registry.
@classmethod
def _get_prefix(cls, s):
""" Extract any file system prefix at beginning of string s and
return a lowercase version of it or None when there isn't one.
"""
match = cls._PATH_PREFIX_PATTERN.match(s)
return match.group(1).lower() if match else None
def __new__(cls, path):
""" Create instance of appropriate subclass. """
path_prefix = cls._get_prefix(path)
subclass = FileSystem._registry.get(path_prefix)
if subclass:
# Using "object" base class method avoids recursion here.
return object.__new__(subclass)
else: # No subclass with matching prefix found (and no default).
raise FileSystem.Unknown(
f'path "{path}" has no known file system prefix')
def count_files(self):
raise NotImplementedError
class Nfs(FileSystem, path_prefix='nfs'):
def __init__ (self, path):
pass
def count_files(self):
pass
class LocalDrive(FileSystem, path_prefix=None): # Default file system.
def __init__(self, path):
if not os.access(path, os.R_OK):
raise FileSystem.NoAccess('Cannot read directory')
self.path = path
def count_files(self):
return sum(os.path.isfile(os.path.join(self.path, filename))
for filename in os.listdir(self.path))
if __name__ == '__main__':
data1 = FileSystem('nfs://192.168.1.18')
data2 = FileSystem('c:/') # Change as necessary for testing.
print(type(data1).__name__) # -> Nfs
print(type(data2).__name__) # -> LocalDrive
print(data2.count_files()) # -> <some number>
try:
data3 = FileSystem('foobar://42') # Unregistered path prefix.
except FileSystem.Unknown as exc:
print(str(exc), '- raised as expected')
else:
raise RuntimeError(
"Unregistered path prefix should have raised Exception!")
此答案是书面作品,但我希望解决其他人可能因缺乏经验或团队要求的代码库标准而遇到的一些问题(潜在的陷阱)。
首先,对于__init_subclass__ 上的装饰器,根据PEP:
可能需要在 __init_subclass__ 装饰器上显式使用 @classmethod。它是隐含的,因为没有合理的解释可以忽略它,并且无论如何都需要检测到这种情况才能给出有用的错误消息。
这不是问题,因为它已经暗示了,而禅宗告诉我们“显性大于隐性”;无论如何,当遵守 PEP 时,就可以了(并且进一步解释了理性)。
在我自己的类似解决方案的实现中,子类没有使用额外的关键字参数定义,例如@martineau 在此处所做的:
class Nfs(FileSystem, path_prefix='nfs'): ...
class LocalDrive(FileSystem, path_prefix=None): ...
浏览PEP时:
作为第二个更改,新的type.__init__ 只是忽略关键字参数。目前,它坚持没有给出关键字参数。如果元类不处理关键字参数给类声明,这会导致(想要的)错误。想要接受关键字参数的元类作者必须通过覆盖 __init__ 来过滤掉它们。
为什么这(可能)有问题?好吧,有几个问题(特别是this)描述了围绕类定义中的附加关键字参数、元类的使用(随后是metaclass= 关键字)和被覆盖的__init_subclass__ 的问题。但是,这并不能解释为什么它在当前给定的解决方案中有效。答案:kwargs.pop()。
如果我们看以下内容:
# code in CPython 3.7
import os
import re
class FileSystem(object):
class NoAccess(Exception): pass
class Unknown(Exception): pass
# Regex for matching "xxx://" where x is any non-whitespace character except for ":".
_PATH_PREFIX_PATTERN = re.compile(r'\s*([^:]+)://')
_registry = {} # Registered subclasses.
def __init_subclass__(cls, **kwargs):
path_prefix = kwargs.pop('path_prefix', None)
super().__init_subclass__(**kwargs)
cls._registry[path_prefix] = cls # Add class to registry.
...
class Nfs(FileSystem, path_prefix='nfs'): ...
这仍然可以正常运行,但如果我们删除 kwargs.pop():
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs) # throws TypeError
cls._registry[path_prefix] = cls # Add class to registry.
抛出的错误是已知的并在 PEP 中描述:
在新代码中,抱怨关键字参数的不是__init__,而是__init_subclass__,其默认实现不带参数。在使用方法解析顺序的经典继承方案中,每个__init_subclass__ 都可以取出它的关键字参数,直到没有留下任何关键字参数,这由__init_subclass__ 的默认实现进行检查。
正在发生的事情是 path_prefix= 关键字正在从 kwargs 中“弹出”,而不仅仅是访问,因此 **kwargs 现在为空并传递了 MRO,因此符合默认实现(接收 no关键字参数)。
为了完全避免这种情况,我建议不要依赖kwargs,而是使用对__init_subclass__ 的调用中已经存在的内容,即cls 参考:
# code in CPython 3.7
import os
import re
class FileSystem(object):
class NoAccess(Exception): pass
class Unknown(Exception): pass
# Regex for matching "xxx://" where x is any non-whitespace character except for ":".
_PATH_PREFIX_PATTERN = re.compile(r'\s*([^:]+)://')
_registry = {} # Registered subclasses.
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._registry[cls._path_prefix] = cls # Add class to registry.
...
class Nfs(FileSystem):
_path_prefix = 'nfs'
...
如果需要引用子类使用的特定前缀(通过self._path_prefix),将prior 关键字添加为类属性还扩展了在以后的方法中的使用。据我所知,您不能再引用定义中提供的关键字(没有一些复杂性),这似乎微不足道且有用。
因此,对于 @martineau,我为我的 cmets 看起来令人困惑而道歉,只有这么多的空间可以输入它们,并且如图所示,它更详细。