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 ?
Réponses
Trop de publicités?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.
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!
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).
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
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()
etspecific_foo()
peuvent avoir des signatures différentesspecific_foo()
retourne unFoo
. 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.
- Réponses précédentes
- Plus de réponses
9 votes
Il n'y a pas de notion de méthodes privées vs publiques en Python.
10 votes
Python n'est pas Java.
8 votes
@Tadeck, s'il vous plaît arrêtez de corriger à tort les gens. Dénaturer un nom ne le rend pas privé.
10 votes
@DanielRoseman: larsmans a dit "il n'y a pas de notion de méthodes privées vs publiques en Python". Je dis qu'il a tort. Documentation dit: "Puisqu'il y a un cas d'utilisation valide pour les membres privés de la classe (...), il y a un support limité pour un tel mécanisme, appelé embrouillage de noms.". Veuillez arrêter de débattre sans prouver que vous avez raison.
2 votes
@Tadeck, répétant ma réponse : une méthode marquée avec
__
peut toujours être appelée commeobj._ClassName__Method()
. Maintenant, pourriez-vous expliquer comment cela est privé ?4 votes
@BasicWolf: Veuillez lire pleinement mon commentaire en premier. Le mélange de noms vous donne une notion (partielle) des classes privées. Veuillez ne pas dire que l'appel de
obj._ClassName__Method()
en dehors deClassName
est un comportement correct.