92 votes

Constructeur privé en Python

Comment créer un constructeur privé qui ne doit être appelé que par la fonction statique de la classe et non par un autre endroit ?

9 votes

Il n'y a pas de notion de méthodes privées vs publiques en Python.

10 votes

8 votes

@Tadeck, s'il vous plaît arrêtez de corriger à tort les gens. Dénaturer un nom ne le rend pas privé.

76voto

Adam Points 601

Les préfixes _ et __ ne offrent pas de solution pour restreindre l'instanciation d'un objet à une 'factory' spécifique, cependant Python est une boîte à outils puissante et le comportement désiré peut être atteint de plus d'une manière (comme l'a démontré @Jesse W chez Z).

Voici une solution possible qui garde la classe publiquement visible (permettant isinstance etc.) mais garantit que la construction n'est possible que par des méthodes de classe :

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), \
            "Les objets OnlyCreatable doivent être créés en utilisant OnlyCreatable.create"
        self.value = value

Construction d'un objet en utilisant la méthode de classe create :

>>> OnlyCreatable.create("Je suis un test") 
<__main__.OnlyCreatable object at 0x1023a6f60>

Lorsque l'on tente de construire un objet sans utiliser la méthode de classe create, la création échoue en raison de l'assertion :

>>> OnlyCreatable(0, "Je suis un test")
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 11, in __init__
AssertionError: Les objets OnlyCreatable ne peuvent être créés qu'en utilisant OnlyCreatable.create

Si l'on tente de créer un objet en imitant la méthode de classe create, la création échoue en raison du mélange du compilateur de OnlyCreatable.__createKey.

>>> OnlyCreatable(OnlyCreatable.__createKey, "Je suis un test")
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: l'objet de type 'OnlyCreatable' n'a pas d'attribut '__createKey'

La seule façon de construire OnlyCreatable en dehors d'une méthode de classe est de connaître la valeur de OnlyCreatable.__create_key. Comme la valeur de cet attribut de classe est générée au moment de l'exécution et que son nom est préfixé par __ le rendant inaccessible, il est effectivement 'impossible' d'obtenir cette valeur et/ou de construire l'objet.

40voto

mac Points 16282

Comment puis-je créer un constructeur privé?

En essence, c'est impossible à la fois parce que python n'utilise pas les constructeurs de la manière dont vous pourriez le penser si vous venez d'autres langages orientés objet et parce que python ne impose pas la confidentialité, il a simplement une syntaxe spécifique pour suggérer qu'une méthode/propriété donnée devrait être considérée comme privée. Laissez-moi élaborer...

Premièrement: la chose la plus proche d'un constructeur que vous pouvez trouver en python est la méthode __new__ mais ceci est très très rarement utilisé (vous utilisez normalement __init__, qui modifie l'objet tout juste créé (en fait il a déjà self comme premier paramètre).

Quoi qu'il en soit, python est basé sur l'hypothèse que tout le monde est un adulte consentant, donc le privé/public n'est pas imposé comme le font certains autres langages.

Comme mentionné par d'autres répondants, les méthodes qui sont censées être "privées" sont normalement précédées d'un ou deux tirets bas: _private ou __private. La différence entre les deux est que le dernier va brouiller le nom de la méthode, donc vous ne pourrez pas l'appeler depuis en dehors de l'instanciation de l'objet, alors que le premier ne le fera pas.

Donc par exemple si votre classe A définit à la fois _private(self) et __private(self):

>>> a = A()
>>> a._private()   # fonctionnera
>>> a.__private()  # lèvera une exception

Vous voulez normalement utiliser le tiret bas simple, car - surtout pour les tests unitaires - avoir deux tirets bas peut rendre les choses très compliquées....

J'espère que cela vous aide!

18voto

Jesse W at Z Points 1091

Vous pouvez avoir un contrôle considérable sur les noms qui sont visibles dans quelles portées - et il y a beaucoup de portées disponibles. Voici trois autres façons de limiter la construction d'une classe à une méthode de fabrique :

#Définir la classe dans la méthode de fabrique
def factory():
  class Foo:
    pass
  return Foo()

OU

#Attribuer la classe comme attribut de la méthode de fabrique
def factory():
  return factory.Foo()
class Foo:
  pass
factory.Foo = Foo
del Foo

(Remarque : Cela permet toujours de faire référence à la classe depuis l'extérieur (pour des vérifications avec isinstance, par exemple), mais cela rend assez évident que vous ne devez pas l'instancier directement.)

OU

#Attribuer la classe à une variable locale d'une fonction externe
class Foo:
  pass
def factory_maker():
  inner_Foo=Foo
  def factory():
    return inner_Foo()
  return factory
factory = factory_maker()
del Foo
del factory_maker

Cela rend impossible (du moins, sans utiliser au moins une propriété magique (à double tiret bas)) d'accéder à la classe Foo, mais permet toujours à plusieurs fonctions de l'utiliser (en les définissant avant de supprimer le nom global Foo).

17voto

Noé Rubinstein Points 173

Bien que les attributs strictement privés n'existent pas en Python, vous pouvez utiliser une métaclass pour empêcher l'utilisation de la syntaxe MyClass() afin de créer un objet MyClass.

Voici un exemple adapté du projet Trio:

from typing import Type, Any, TypeVar

T = TypeVar("T")

class NoPublicConstructor(type):
    """Métaclass qui assure un constructeur privé

    Si une classe utilise cette métaclass comme ceci :

        class SomeClass(metaclass=NoPublicConstructor):
            pass

    Si vous essayez d'instancier votre classe (`SomeClass()`),
    une `TypeError` sera levée.
    """

    def __call__(cls, *args, **kwargs):
        raise TypeError(
            f"{cls.__module__}.{cls.__qualname__} n'a pas de constructeur public"
        )

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

Voici un exemple d'utilisation :

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) # soulève une erreur de type
Point.from_cartesian(1, 2) # OK
Point.from_polar(1, 2) # OK

5voto

Joel Berkeley Points 1532

Si une fonction au niveau du module est acceptable à la place d'une méthode statique...

Rendre la classe entière privée, exposer l'API sous forme de classe abstraite, et utiliser une fonction pour instancier votre classe privée

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)

Remarque

  • _SpecificFoo() et specific_foo() peuvent avoir des signatures différentes
  • specific_foo() retourne un Foo. L'API ne mentionne pas _SpecificFoo

Cela est très similaire à la première option de Jesse, mais la classe n'est pas redéfinie à chaque appel à specific_foo, et l'interface de la classe est typée statiquement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X