【问题标题】:Private Constructor in PythonPython中的私有构造函数
【发布时间】:2012-01-02 23:11:18
【问题描述】:

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

【问题讨论】:

  • Python 中没有私有与公共方法的概念。
  • @Tadeck,请停止错误地纠正人们。修改名称不会使其私有化。
  • @DanielRoseman:larsmans 说“Python 中没有私有与公共方法的概念”。我说他错了。 Documentation 说:“由于类私有成员(...)有一个有效的用例,因此对这种机制的支持有限,称为名称修改。”。请不要在没有证明你是对的情况下争论。
  • @BasicWolf:请先完整阅读我的评论。名称修改为您提供了私有类的(部分)概念。请不要说在ClassName 之外调用obj._ClassName__Method() 是正确的行为。

标签: python static constructor private


【解决方案1】:

首先,术语“构造函数”不适用于 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

【讨论】:

  • @Tadeck,这不是真的,BasicWolf 是正确的。以双下划线为前缀的方法不是私有的 - 它们是名称混乱的,但仍然可以从类外部访问它们。
  • @Tadeck,试试这个:用__my() 方法创建一个类MyClass。考虑mc_obj = MyClass()。您将能够通过mc_obj._MyClass__my() 访问__my()
  • @BasicWolf:我知道你可以做到,但这并不意味着你应该这样做,如果你想遵循 Python 的规则的话。如果您不在乎,您也可以成为eval() 的粉丝,或者使用vars()['a'] = 10 等作业代替a = 10
  • @BasicWolf:你说“用单个下划线标记方法意味着完全相同”,我完全不同意。请阅读 gimel 的答案并自己检查,这样您就知道以单下划线和双下划线开头的方法之间的区别。
  • @BasicWolf:请不要说这完全一样或证明是一样的。你最后的评论是题外话。当我想要它们提供的功能时,我会使用 __ 方法。我想指出的是,以_ 开头的方法名称与__ 不同,如果您还没有注意到,请参阅@gimel 或@mac 的答案。
【解决方案2】:

引用Python style guide (PEP 8)

此外,以下特殊形式使用前导或尾随 下划线被识别(这些通常可以与任何大小写组合 约定):

  • _single_leading_underscore:弱“内部使用”指标。例如。 “from M import *”不导入名称以下划线开头的对象。

  • single_trailing_underscore_:按惯例使用,以避免与 Python 关键字,例如 Tkinter.Toplevel(master, class_='ClassName')

  • __double_leading_underscore:命名类属性时,调用名称 mangling(在 FooBar 类中,__boo 变为 _FooBar__boo;见下文)。

  • __double_leading_and_trailing_underscore__:“魔法”对象或 存在于用户控制的命名空间中的属性。例如。 __init__, __import____file__。永远不要发明这样的名字;只使用它们 记录在案。

【讨论】:

  • 我不明白这是如何回答这个问题的。它没有说明构造函数。
  • @larsmans - 通过样式指南回答“私人”和“未从其他地方调用”。
  • 但是构造函数有固定的名字,所以你不能按照惯例只附加一个下划线。
【解决方案3】:

如何创建私有构造函数?

从本质上讲,不可能既是因为 python 不会像您使用其他 OOP 语言时所认为的那样使用构造函数,而且因为 python 不会强制隐私,它只是有一个特定的语法来建议给定的方法/属性应该被认为是私有的。让我详细说明...

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

无论如何,python 是基于每个人都是同意的成年人的假设,因此私有/公共不像其他语言那样强制执行。

正如其他响应者所提到的,“私有”的方法通常在前面加上一个或两个下划线:_private__private。两者的区别在于后者会打乱方法的名称,所以你将无法从对象实例化之外调用它,而前者则不会。

例如,如果您的班级 A 同时定义了 _private(self)__private(self)

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

您通常希望使用单下划线,因为 - 特别是对于单元测试 - 使用双下划线会使事情变得非常棘手......

HTH!

【讨论】:

  • +1 那是正确的答案! :) 其实你可以从类外调用私有方法,但你不应该这样做,所以你的答案是可以的。谢谢。
  • 问题是关于私有构造函数,而不是私有成员函数。你只是在解释如何拥有私有函数。
  • @Syncopated - 正如该问题的所有答案和 cmets 所指出的,Python 中没有“私有构造函数”之类的东西
  • @mac 如果是这样,那么你的答案应该是“不可能。”,而不是你写的,因为问题显然是关于构造函数,而不是函数。或者,如果您能想出一种达到预期效果的方法,那就是答案。
  • 整个“同意的成年人”模因需要消亡。存在私有修饰符的原因是因为对象旨在成为一种状态机。在 OOP 中,方法和成员是紧密耦合的。创建私有成员的原因是因为在公共接口之外更改它可能会产生不明显的副作用,导致难以追踪错误。
【解决方案4】:

目前还没有人提到这一点——您可以相当程度地控制哪些名称在哪些范围内可见——并且有很多个范围可用。以下是两个 三个 将类的构造限制为工厂方法的其他方法:

#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。我想补充一点,在这种情况下,isinstance 检查将是不可能的,因为该类不可访问(或者至少不应该被访问)。这可以通过让类从可见的抽象基类继承来解决。
  • 第二个选项允许 isinstance 检查,将类称为 factory.Foo
  • 没错,但我认为客户端代码不允许从外部查看课程?
  • 我打算让它更容易调用工厂而不是直接实例化类,而不是让它不可能(嗯,主要是) 直接实例化(为此,使用第一种形式)。
  • 太棒了!感谢分享!这应该是公认的答案!
【解决方案5】:
class MongoConn(object):
    @classmethod
    def instance(cls):
        if not hasattr(cls, '_instance'):
            cls._instance = cls()
        return cls._instance

    def __init__(self):
        assert not hasattr(self.__class__, '_instance'), 'Do not call constructor directly!'

如果你想要一个实例。

【讨论】:

  • 我可以打电话给MongoConn()这里没问题
【解决方案6】:

___ 前缀没有提供将对象实例化限制为特定“工厂”的解决方案,但是 Python 是一个强大的工具箱,并且可以通过多种方式实现所需的行为(正如 Z 的@Jesse W 所证明的那样)。 这是一个可能的解决方案,可以让类公开可见(允许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 的值。由于此类属性的值是在运行时生成的,并且它的名称以 __ 为前缀,将其标记为不可访问,因此实际上“不可能”获取该值和/或构造对象。

【讨论】:

  • 不是不能访问,是OnlyCreatable._OnlyCreatable__create_key。或者,您可以使用 -O 标志或 PYTHONOPTIMIZE env var 禁用断言来运行 Python。
  • 这与 python 拥有“强大的工具箱”无关。任何语言都可以实现这一点。在任何真正的 OOP 中,它被认为是一种可怕的做法的原因是,在您实际运行代码之前,无法识别实例化一个抛出异常的类。因此,在这个用例中,最小惊讶原则被严重破坏了。
【解决方案7】:

如果模块级函数可以代替静态方法,请参阅我的other answer

您可以使用抽象类来实现这种效果。任何需要在“私有构造函数”中定义的实例属性都可以是抽象属性。然后,您的工厂类方法通过填充这些抽象属性以及执行任何其他初始化工作(例如数据验证)来构建自己的具体类。

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 的继承作为文档,或者定义一个虚拟属性以确保安全。

可选调整

  • 需要可变实例属性?添加设置器。
  • 不关心类和实例属性的区别?简化为
    class _Foo(cls):
        _a = a
    
    return _Foo()
    

【讨论】:

    【解决方案8】:

    虽然 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
    

    【讨论】:

    • 这个类的消费者在运行代码之前怎么知道不存在公共构造函数?
    • @Tarynn 添加我猜的文档字符串
    【解决方案9】:

    其他答案提到 Python 中没有“私有”构造函数(或一般的访问修饰符)之类的东西。虽然这是真的,但您仍然可以利用 __ 前缀行为通过在具有双下划线前缀的嵌套类中“隐藏”实际实现来实现相同的最终效果:

    class MyPublicClassWithPrivateConstructor:
    
        class __Implementation:
            def __init__(self, *args, **kwargs):
                # do the things
                self.args = args
                self.kwargs = kwargs
    
        @classmethod
        def make_my_thing(cls, *args, **kwargs)
            return cls.__Implementation(*args, **kwargs)
    
        def __new__(cls, *args, **kwargs):
            raise Exception("MyPublicClassWithPrivateConstructor cannot be initialized directly, use MyPublicClassWithPrivateConstructor.make_my_thing")
    
    # elsewhere
    from whateva import MyPublicClassWithPrivateConstructor
    
    MyPublicClassWithPrivateConstructor.__Implementation  # type object 'MyPublicClassWithPrivateConstructor' has no attribute '__Implementation'
    MyPublicClassWithPrivateConstructor.make_my_thing  # <bound method MyPublicClassWithPrivateConstructor.make_my_thing of <class 'whateva. MyPublicClassWithPrivateConstructor'>>
               
    

    【讨论】:

      【解决方案10】:

      如果模块级函数可以代替静态方法...

      将整个类设为私有,将 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 时重新定义类

      【讨论】:

      • 从类型注解的角度来看,这是一个很好的解决方案。我们仍然可以在类型注释中使用Foo,但_SpecificFoo 的受保护状态将强烈鼓励我们使用specific_foo 来建立新实例。如果我们尝试使用构造函数,这也比实际抛出错误的解决方案更 Pythonic。最好使用下划线来阻止它。我很惊讶这没有更多的赞成票。
      【解决方案11】:

      如果您只是想要一种简单的方式来表明不应直接使用构造函数,则可以按照以下模式使构造函数的默认用法引发异常:

      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 的一般设计的这种行为。

      【讨论】:

      • 请注意,当然在某些配置中(即PYTHONOPTIMIZE=TRUE)Python 会跳过检查断言。如果在这种情况下您仍需要此检查,您可以考虑将assert 更改为if not __private: raise RuntimeError("...")
      【解决方案12】:

      我知道,我迟到了,但我们可以做这样的事情来创建私有构造函数。

      class MyClass:
          __instance = None
          __private = False
      
          @staticmethod
          def get_instance():
      
              MyClass.__private = True
              __instance = MyClass()
              MyClass.__private = False
              return MyClass.__instance
           
          def __init__(self):
      
              if MyClass.__private == False:
                   raise Exception("Its a private method. Please use get_instance method")
              
      

      【讨论】:

      • 这对单身人士来说非常棒。其余的用例呢?
      • @Tarynn 对于正常用例,只需删除 if not MyClass.__instance: 条件并将所有内容置于条件之外,这样就可以了。让我为一般用例编辑它。
      猜你喜欢
      • 2011-04-20
      • 1970-01-01
      • 2019-10-16
      • 2018-04-01
      • 1970-01-01
      相关资源
      最近更新 更多