如何使python“constructor”“private”,以便只能通过调用静态方法创建其类的对象?我知道there are no C++/Java like private methods in Python,但我正在寻找另一种方法来阻止其他人调用我的构造函数(或其他方法)。
我有类似的东西:
class Response(object):
@staticmethod
def from_xml(source):
ret = Response()
# parse xml into ret
return ret
@staticmethod
def from_json(source):
# parse json
pass
并希望以下行为:
r = Response() # should fail
r = Response.from_json(source) # should be allowed
使用静态方法的原因是我总是忘记构造函数采用的参数 - 比如JSON或已经解析过的对象。即便如此,我有时会忘记静态方法并直接调用构造函数(更不用说使用我的代码的其他人)。记录这份合同对我的遗忘无益。我宁愿用断言强制执行它。
与一些评论者相反,我不认为这是非语言的 - “明确胜于隐性”,“应该只有一种方法”。
如果我做错了怎么能得到温柔的提醒?我更喜欢一个解决方案,我不需要改变静态方法,只需一个装饰器或构造函数的单行插件就可以了。拉:
class Response(object):
def __init__(self):
assert not called_from_outside()
答案 0 :(得分:14)
我认为这正是你所寻求的 - 但就我而言,这是一种单声道。
class Foo(object):
def __init__(self):
raise NotImplementedError()
def __new__(cls):
bare_instance = object.__new__(cls)
# you may want to have some common initialisation code here
return bare_instance
@classmethod
def from_whatever(cls, arg):
instance = cls.__new__(cls)
instance.arg = arg
return instance
鉴于您的示例(from_json
和from_xml
),我假设您正在从json或xml源中检索属性值。在这种情况下,pythonic解决方案将有一个普通的初始化程序,并从你的替代构造函数调用它,即:
class Foo(object):
def __init__(self, arg):
self.arg = arg
@classmethod
def from_json(cls, source):
arg = get_arg_value_from_json_source(source)
return cls(arg)
@classmethod
def from_xml(cls, source):
arg = get_arg_value_from_xml_source(source)
return cls(arg)
哦,是的,关于第一个例子:它会阻止你的类以通常的方式实例化(调用类),但客户端代码仍然能够调用{{ 1}},所以这真的是浪费时间。如果你不能以最普通的方式实例化你的课程,它也会使单元测试变得更难......而且我们中的很多人会因此而讨厌你。
答案 1 :(得分:12)
我建议将工厂方法转换为模块级工厂函数,然后将类本身隐藏在模块用户之外。
def one_constructor(source):
return _Response(...)
def another_constructor(source):
return _Response(...)
class _Response(object):
...
您可以在re
等模块中看到此方法,其中匹配对象仅通过match
和search
等函数构建,文档实际上并未命名匹配对象类型。 (至少,3.4 documentation没有。2.7 documentation错误地引用re.MatchObject
,它不存在。)匹配对象类型也抵制直接构造:< / p>
>>> type(re.match('',''))()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create '_sre.SRE_Match' instances
但遗憾的是,它的使用方式依赖于C API,因此普通的Python代码无法使用它。
答案 2 :(得分:1)
评论中的讨论很好。
对于您描述的最小用例,
class Response(object):
def __init__(self, construct_info = None):
if construct_info is None: raise ValueError, "must create instance using from_xml or from_json"
# etc
@staticmethod
def from_xml(source):
info = {} # parse info into here
return Response(info)
@staticmethod
def from_json(source):
info = {} # parse info into here
return Response(info)
传递手工构造的信息的用户可以使用它,但是在那时他们仍然必须阅读代码,静态方法将提供阻力最小的路径。你不能阻止他们,但你可以轻轻地阻止他们。毕竟这是Python。
答案 3 :(得分:1)
另一种API(以及我在Python API中看到的更多)如果你想为用户保持明确,那就是使用关键字参数:
class Foo(object):
def __init__(self, *, xml_source=None, json_source=None):
if xml_source and json_source:
raise ValueError("Only one source can be given.")
elif xml_source:
from_xml(xml_source)
elif json_source:
from_json(json_source)
else:
raise ValueError("One source must be given.")
此处使用3.x&#39; *
来表示keyword-only arguments,这有助于强制执行显式API。在2.x中,这可以通过kwargs
进行修复。
当然,这并不能很好地扩展到许多参数或选项,但肯定有这种风格有意义的情况。 (根据我们的知识,我认为bruno desthuilliers对于这种情况可能是正确的,但我会将此作为其他人的选择。
答案 4 :(得分:0)
这可以通过元类实现,但在Python中不鼓励 。 Python is not Java。 Python中没有公共与私有的一流概念;这个想法是该语言的用户是“同意成年人”,并且可以使用他们喜欢的方法。通常,旨在“私有”的功能(不是API的一部分)由单个前导下划线表示;然而,这主要是惯例,没有什么能阻止用户使用这些功能。
在你的情况下,Pythonic要做的是将构造函数默认为一个可用的from_foo方法,或者甚至创建一个“智能构造函数”,它可以在大多数情况下找到合适的解析器。或者,向__init__
方法添加可选关键字arg,以确定要使用的解析器。
答案 5 :(得分:0)
以下内容类似于我最终做的事情。它比问题中的问题更为通用。
我创建了一个名为guard_call
的函数,检查当前方法是否是从某个类的方法调用的。
这有多种用途。例如,我使用Command Pattern来实现撤销和重做,并使用它来确保我的对象只被命令对象修改过,而不是随机的其他代码(这会使撤销变得不可能)。
在这个具体案例中,我在构造函数中放置一个保护,确保只有Response
方法可以调用它:
class Response(object):
def __init__(self):
guard_call([Response])
pass
@staticmethod
def from_xml(source):
ret = Response()
# parse xml into ret
return ret
对于这个特定情况,你可能会把它变成装饰器并删除参数,但我没有在这里这样做。
这是代码的其余部分。我测试它已经很长时间了,并且不能保证它适用于所有边缘情况,所以要小心。它仍然是Python 2.另一个警告是它很慢,因为它使用inspect
。所以不要在紧凑的循环中使用它,并且当速度是一个问题时,但是当正确性比速度更重要时它可能是有用的。
有一天,我可能会清理它并将其作为库发布 - 我还有一些这样的函数,包括一个断言你在特定线程上运行的函数。你可能会嘲笑hackyness(它很hacky),但我确实发现这种技术对于抽出一些难以发现的bug很有用,并确保我的代码在重构过程中仍能表现出来。例如。
from __future__ import print_function
import inspect
# http://stackoverflow.com/a/2220759/143091
def get_class_from_frame(fr):
args, _, _, value_dict = inspect.getargvalues(fr)
# we check the first parameter for the frame function is
# named 'self'
if len(args) and args[0] == 'self':
# in that case, 'self' will be referenced in value_dict
instance = value_dict.get('self', None)
if instance:
# return its class
return getattr(instance, '__class__', None)
# return None otherwise
return None
def guard_call(allowed_classes, level=1):
stack_info = inspect.stack()[level + 1]
frame = stack_info[0]
method = stack_info[3]
calling_class = get_class_from_frame(frame)
# print ("calling class:", calling_class)
if calling_class:
for klass in allowed_classes:
if issubclass(calling_class, klass):
return
allowed_str = ", ".join(klass.__name__ for klass in allowed_classes)
filename = stack_info[1]
line = stack_info[2]
stack_info_2 = inspect.stack()[level]
protected_method = stack_info_2[3]
protected_frame = stack_info_2[0]
protected_class = get_class_from_frame(protected_frame)
if calling_class:
origin = "%s:%s" % (calling_class.__name__, method)
else:
origin = method
print ()
print ("In %s, line %d:" % (filename, line))
print ("Warning, call to %s:%s was not made from %s, but from %s!" %
(protected_class.__name__, protected_method, allowed_str, origin))
assert False
r = Response() # should fail
r = Response.from_json("...") # should be allowed