Python中的私有构造函数

107

如何创建一个私有构造函数,该构造函数只能被类的静态函数调用,而不能从其他地方调用?


10
Python 中没有私有和公有方法的概念。 - Fred Foo
11
Python不是Java。 - Sven Marnach
8
@Tadeck,请停止错误地纠正他人。错误拼写姓名并不能使其变得私密。 - Daniel Roseman
12
@DanielRoseman说larsmans说“Python中没有私有方法和公共方法的概念”。我认为他是错的。文档上说:“既然存在类私有成员的有效用例(...),因此对于这种机制提供了有限的支持,称为名称修饰。”请不要在没有证明你正确的情况下继续争论。 - Tadeck
2
@Tadeck,重复我的答案:标记为__的方法仍然可以被称为obj._ClassName__Method()。现在,请您解释一下,这是如何实现私有的? - Zaur Nasibov
4
请先完全阅读我的评论。名称修饰为您提供了关于私有类的(部分)概念。请不要说在ClassName之外调用obj._ClassName__Method()是正确的行为。 - Tadeck
13个回答

75

___前缀并不能提供一种将对象实例化限制在特定“工厂”的解决方案,但是Python是一个强大的工具箱,可以以多种方式实现所需的行为(正如@Jesse W at Z已经展示的那样)。 以下是一种可能的解决方案,它保持类的公开可见性(允许使用isinstance等),但确保只能通过类方法进行构造:

class OnlyCreatable(object):

    __create_key = object()

    @classmethod
    def create(cls, value):
        return OnlyCreatable(cls.__create_key, value)

    def __init__(self, create_key, value):
        assert(create_key == OnlyCreatable.__create_key), \
            "OnlyCreatable objects must be created using OnlyCreatable.create"
        self.value = value

使用create类方法构建对象:

>>> OnlyCreatable.create("I'm a test") 
<__main__.OnlyCreatable object at 0x1023a6f60>
尝试构造一个对象而不使用create类方法时,由于断言的存在,创建会失败。
>>> OnlyCreatable(0, "I'm a test")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in __init__
AssertionError: OnlyCreatable objects can only be created using OnlyCreatable.create
如果尝试通过模仿create类方法创建对象失败,原因是编译器混淆了OnlyCreatable.__createKey
>>> OnlyCreatable(OnlyCreatable.__createKey, "I'm a test")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'OnlyCreatable' has no attribute '__createKey'

构造OnlyCreatable类外的对象,唯一的方法是知道OnlyCreatable.__create_key的值。由于这个类属性的值是在运行时生成的,并且它的名称以双下划线为前缀标记为不可访问,因此实际上几乎“不可能”获取该值和/或构造对象。


5
可以访问,只需使用OnlyCreatable._OnlyCreatable__create_key。或者您可以使用-O标志或PYTHONOPTIMIZE环境变量禁用Python中的断言来运行代码。 - jonrsharpe
1
这与Python拥有“强大的工具箱”无关。任何语言都可以实现这一点。之所以在任何真正的OOP中被认为是可怕的做法,是因为没有办法确定实例化一个类是否会抛出异常,直到你实际运行代码。因此,在这种用例中,“最小惊讶原则”被严重破坏了。 - Tarynn
__init__ 中,将其改为 if 语句而不是断言 -- 断言很容易被禁用,在某些环境中也是如此。此外,您可以使用 is 替换 == 相等测试,以使程序员的意图清晰明了。 - amka66
@Tarynn 我想知道你认为哪些语言是“真正的面向对象编程语言”。我使用过很多语言,惊讶地发现它们在最基本的面向对象方面与Python相比差距很大。在Python中,我确实看到了“一切皆为对象”的真谛。但在Java、C#、C++中,这种说法显然是错误的。函数不是对象,类也不是对象,方法也不是对象。等等。 - Ark-kun
为了扩展@amka66的评论,您可以使用关键字class Hack: def __eq__(self, other): return True来进行黑客攻击。如果您想让它适用于恶意对象,请使用is - joel

40

我如何创建私有构造函数?

实际上,这是不可能的,因为 Python 并不像其他面向对象语言那样使用构造函数,并且 Python 并没有强制执行隐私,它只有一种特定的语法来建议一个给定的方法/属性应该被视为私有。让我详细说明一下...

首先:在 Python 中最接近构造函数的是 __new__ 方法,但这很少用到(通常使用 __init__,它修改刚创建的对象(实际上它已经有 self 作为第一个参数了)。

尽管如此,Python 基于每个人都是“成年人”的假设,因此私有/公共并不像其他语言那样被强制执行。

如其他回答者所提到的,一般将需要“私有”的方法前加一个或两个下划线:_private__private。两者之间的区别在于后者会将方法名混淆,因此您将无法从对象实例化外部调用它,而前者则没有。

因此,例如,如果您的类 A 定义了 _private(self)__private(self) 两个方法:

>>> a = A()
>>> a._private()   # will work
>>> a.__private()  # will raise an exception

通常情况下,你应该使用单个下划线。双下划线会让事情变得非常棘手,特别是在单元测试中。

希望对你有所帮助!


2
+1 这是正确的答案! :) 实际上,你可以从类外部调用私有方法,但你不应该这样做,所以你的答案是正确的。谢谢。 - Tadeck
16
问题是关于私有构造函数,而不是私有成员函数。您只是在解释如何拥有私有函数。 - Joshua Chia
1
@Syncopated - 众所周知,根据问题的所有答案和评论,Python 中并不存在“私有构造函数”。 - mac
4
如果是这样的话,那么你应该回答“不可能”,而不是你写的内容,因为问题明显是关于构造函数而不是函数。或者,如果你能想到一种实现所需效果的方法,那就是答案。 - Joshua Chia
2
整个“成年人同意”的梗需要消失。私有修饰符存在的原因是因为对象旨在成为一种状态机。在面向对象编程中,方法和成员紧密耦合。将成员设置为私有的原因是因为在公共接口之外更改它可能会导致非明显的副作用,从而导致难以跟踪的错误。 - Tarynn
显示剩余3条评论

17

您可以在不同的作用域中控制哪些名称可见,而且有许多可用的作用域。以下是将类的构造限制为工厂方法的两种三种其他方法:

#Define the class within the factory method
def factory():
  class Foo:
    pass
  return Foo()

或者

#Assign the class as an attribute of the factory method
def factory():
  return factory.Foo()
class Foo:
  pass
factory.Foo = Foo
del Foo

(注:这仍然允许从外部引用该类(例如进行isinstance检查),但它明显表明您不应直接实例化它。)
或者
#Assign the class to a local variable of an outer function
class Foo:
  pass
def factory_maker():
  inner_Foo=Foo
  def factory():
    return inner_Foo()
  return factory
factory = factory_maker()
del Foo
del factory_maker

这使得访问Foo类变得不可能(至少,不使用至少一个魔术(双下划线)属性),但仍允许多个函数使用它(通过在删除全局Foo名称之前定义它们)。

1
+1. 我想补充一点,在这种情况下,isinstance 检查会不可能,因为这个类是不可访问的(或者至少不应该被访问)。这可以通过让该类继承自一个可见的抽象基类来解决。 - Fred Foo
2
第二个选项允许使用isinstance检查,通过将类引用为factory.Foo。 - Jesse W at Z - Given up on SE
1
真的,但我假设客户端代码不能从外部看到这个类? - Fred Foo
1
我本意是让通过调用工厂来实例化类变得更加_简单_,而不是让直接实例化类变得不可能(好吧,大多数情况下是这样)。如果要直接实例化类,请使用第一种形式。 - Jesse W at Z - Given up on SE
太棒了!谢谢分享!这应该是被采纳的答案! - Michael Dorner

17

虽然在Python中严格来说不存在私有属性,但是你可以使用元类来防止使用MyClass()语法创建MyClass对象。

下面是从Trio项目中改编的示例:

from typing import Type, Any, TypeVar


T = TypeVar("T")


class NoPublicConstructor(type):
    """Metaclass that ensures a private constructor

    If a class uses this metaclass like this:

        class SomeClass(metaclass=NoPublicConstructor):
            pass

    If you try to instantiate your class (`SomeClass()`),
    a `TypeError` will be thrown.
    """

    def __call__(cls, *args, **kwargs):
        raise TypeError(
            f"{cls.__module__}.{cls.__qualname__} has no public constructor"
        )

    def _create(cls: Type[T], *args: Any, **kwargs: Any) -> T:
        return super().__call__(*args, **kwargs)  # type: ignore

以下是一个使用示例:

from math import cos, sin


class Point(metaclass=NoPublicConstructor):
     def __init__(self, x, y):
         self.x = x
         self.y = y

     @classmethod
     def from_cartesian(cls, x, y):
         return cls._create(x, y)
     
     @classmethod
     def from_polar(cls, rho, phi):
         return cls._create(rho * cos(phi), rho * sin(phi))

Point(1, 2) # raises a type error
Point.from_cartesian(1, 2) # OK
Point.from_polar(1, 2) # OK

1
这个类的使用者在运行代码之前如何知道没有公共构造函数存在? - Tarynn
1
@Tarynn 添加一个文档字符串吧。 - Noé Rubinstein

5

引用Python风格指南(PEP 8)

此外,以下使用前导或尾随下划线的特殊形式也被认可(这些通常可以与任何大小写约定相结合): - `_single_leading_underscore`:弱的“内部使用”指示器。例如,“from M import *”不会导入名称以下划线开头的对象。 - `single_trailing_underscore_`:按照惯例用于避免与Python关键字冲突,例如,`Tkinter.Toplevel(master, class_='ClassName')` - `__double_leading_underscore`:在命名类属性时,调用名称混淆(在类FooBar中,`__boo`变成`_FooBar__boo`;请参见下文)。 - `__double_leading_and_trailing_underscore__`:“魔术”对象或属性,存在于用户控制的命名空间中。例如,`__init__`、`__import__`或`__file__`。永远不要发明这样的名称;只使用它们如文档所述。

17
我不明白这个回答和问题有什么关联。它没有提到构造函数。 - Fred Foo
@larsmans - 通过风格指南回答“私有”和“不在其他地方调用”的问题。 - gimel
11
构造函数有固定的名称,因此您不能仅按照惯例添加下划线。 - Fred Foo

4
如果模块级别的函数可以代替静态方法...
将整个类设为私有,以抽象类形式暴露API,并使用函数实例化私有类。
class Foo(ABC):
    @abstractmethod
    def bar(self) -> int:
        ...


class _SpecificFoo(Foo):
    def __init__(self, bar: int):
        self._bar = bar

    def bar(self) -> int:
        return self._bar


def specific_foo(bar: int) -> Foo:
    return _SpecificFoo(bar)

注意

  • _SpecificFoo.__init__specific_foo 可以有不同的签名。
  • specific_foo 返回一个 Foo,API 中没有提到 _SpecificFoo

这与 Jesse 的第一种方法非常相似,但是每次调用 specific_foo 时类并不会被重新定义,并且类接口是静态类型的。


1
从类型注释的角度来看,这是一个很好的解决方案。我们仍然可以在类型注释中使用 Foo,但 _SpecificFoo 的受保护状态将强烈鼓励我们使用 specific_foo 实例化新实例。这也比那些实际上会在尝试使用构造函数时抛出错误的解决方案更符合 Python 风格。最好只是使用下划线来阻止它。我很惊讶这没有更多的赞成票。 - croncroncron

2

如果你只是想简单地表示一个构造函数不应该直接使用,你可以按照以下模式来使构造函数默认使用时引发异常:

class Foo:
    def __init__(self, arg, __private=False):
        assert __private, "Constructor shouldn't be called directly"
        self.arg = arg

    @classmethod
    def from_bar(cls, bar: Bar):
        return cls(bar.arg, __private=True)

    @classmethod
    def from_baz(cls, baz: Baz):
         return cls(baz.arg, __private=True)

当然,任何人仍然可以使用Foo('abc', __private=True),但这会发出明确的信号,表明不支持这种行为,这符合Python的通用设计。

3
请注意,在某些配置下(例如PYTHONOPTIMIZE=TRUE),Python会跳过检查断言。如果您仍希望进行此检查,可以考虑将assert更改为if not __private: raise RuntimeError("...") - Matthew D. Scholefield
__private 可以通过使用 Unicode 字符使其更加晦涩(但可能不太可移植)。 - Dmytro

2
首先,术语“构造函数”不适用于Python,因为虽然__init__() 方法扮演了一个构造函数的角色,但它只是一个在对象已经被创建并需要初始化时调用的方法。
Python中类的每个方法都是公共的。通常程序员会在方法的名称中使用___标记“私有”方法,例如:
# inheriting from object is relevant for Python 2.x only
class MyClass(object): 
    # kinda "constructor"
    def __init__(self):
        pass

    # here is a "private" method
    def _some_method(self):
        pass

    # ... and a public one
    def another_method(self):
        pass

4
@Tadeck,这不是真的,BasicWolf 是正确的。以双下划线开头的方法并不是私有的——它们会被名称修饰,但仍然可以从类外部访问它们。 - Daniel Roseman
1
@Tadeck,尝试这个:创建一个名为MyClass的类,并在其中添加一个名为__my()的方法。考虑使用mc_obj = MyClass()实例化该类。您将能够通过mc_obj._MyClass__my()访问__my()方法。 - Zaur Nasibov
1
@BasicWolf:我知道你可以做到,但这并不意味着你应该这样做,如果你想遵循Python的规则。如果你不在意,你也可以成为eval()的粉丝或者使用赋值语句,例如vars()['a'] = 10而不是a = 10 - Tadeck
@BasicWolf:你说“用单下划线标记一个方法完全意味着相同”,我完全不同意。请阅读gimel的回答并自行检查,这样你就知道以单下划线和双下划线开头的方法之间的区别了。 - Tadeck
@BasicWolf:请不要说这完全相同或证明它是。你的最后一条评论与主题无关。当我需要它们提供的功能时,我使用__方法。我想指出的是,“_”开头的方法名称与“__”不同,如果你还没有注意到,请参阅@gimel或@mac的答案。 - Tadeck

2
如果可以使用模块级函数而不是静态方法,请参见我的其他答案
您可以通过抽象类实现此效果。需要在“私有构造函数”中定义的任何实例属性都可以是抽象属性。然后,您的工厂类方法通过填充这些抽象属性来构建自己的具体类,以及执行任何其他初始化工作,如数据验证。
from abc import ABC, abstractmethod

class Foo(ABC):
    @property
    @abstractmethod
    def _a(self) -> int:
        pass

    def bar(self) -> int:
        return self._a + 1

    @classmethod
    def from_values(cls, a: int) -> 'Foo':
        class _Foo(cls):
            def __init__(self, __a):
                self.__a = __a

            @property
            def _a(self):
                return self.__a
        
        return _Foo(a)

Foo()  # TypeError: Can't instantiate abstract class ...
Foo.from_values(1).bar()  # 1

如果您发现在Foo上不需要抽象属性,则调用Foo()时将不会出现TypeError。在这种情况下,您可以依赖于从ABC继承的文档,或者为了安全起见定义一个虚拟属性。
  • Need mutable instance attributes? Add setters.
  • Don't care about the difference between class and instance attributes? Simplify with
    class _Foo(cls):
        _a = a
    
    return _Foo()
    

1
另一种解决方案是让__init__请求一种只有工厂方法才知道的私密密码

请参见下面的示例:

from __future__ import annotations

class Foo:
    name: str

    def __init__(self, check_private_constructor: str):
        if check_private_constructor != "from_within":
            raise RuntimeError("Please use named constructors!")

    @staticmethod
    def from_name(name: str) -> Foo:
        r = Foo("from_within")
        r.name = name
        return r

    @staticmethod
    def from_index(index: int) -> Foo:
        r = Foo("from_within")
        r.name = str(index)
        return r
        
    def __str__(self):
        return "Foo.name=" + self.name


# a = Foo("eee")    # will raise an exception
a = Foo.from_name("Bob")
print(a)
b = Foo.from_index(3)
print(b)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接