防止其他课程'调用我的构造函数的方法

时间:2014-07-30 15:27:53

标签: python python-2.7

如何使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()

6 个答案:

答案 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_jsonfrom_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等模块中看到此方法,其中匹配对象仅通过matchsearch等函数构建,文档实际上并未命名匹配对象类型。 (至少,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