Préface: J'essaie de me prémunir contre les abus (principalement par moi-même) et non contre les utilisations malveillantes (donc le principe des «adultes consentants» ne s'applique pas).
J'essaie de mettre en œuvre quelque chose comme ceci: p>
class Foo:
_trade_secret = 'super_secret_foo_message_dont_use'
def __init__(self, secret):
assert secret == Foo._trade_secret
...
class FooBarFactory:
...
@classmethod
def createFoo(cls):
# suppress private field access warning here
return Foo(Foo._trade_secret)
Le problème est que je veux limiter la création de Foo et Bar à seulement FooBarFactory ' s méthodes. Alors,
foo = FooBarFactory.createFoo() # OK
foo = Foo() # raise AssertionError('use factory method')
Comment puis-je faire cela? Une option que je vois est de mettre Foo et Bar à l'intérieur de la classe d'usine (pour s'assurer que les utilisateurs de code connaissent l'usine). Mais cela produirait une définition de classe gonflée. Une autre option est de faire quelque chose comme ceci:
class Foo(Base):
...
class Bar(Base):
...
class FooBarFactory:
__bar_cache = BarCache()
@classmethod
def createFoo(cls):
return Foo()
@classmethod
def createBar(cls, key):
return cls.__bar_cache.get_or_create(key)
Mais cela semble aussi maladroit et verbeux.
Toute aide est grandement appréciée. Merci!
3 Réponses :
Si votre usine peut le faire, tout le monde peut le faire. Il n'y a pas de solution pour cela en python car personne ne dispose de privilèges spéciaux.
D'un autre côté, même si vous ne pouvez pas forcer les gens à coder correctement, vous pouvez leur rendre la tâche difficile:
class Foo:
def __new__(*args, **kwargs):
raise NotImplementedError("Use the factory.")
@classmethod
def _new(cls, *args, **kwargs):
foo = super().__new__(cls)
foo.__init__(*args, **kwargs)
return foo
class Factory:
@staticmethod
def createFoo(*args, **kwargs):
return Foo._new(*args, **kwargs)
Factory.createFoo() # works fine
Foo() # raises an exception
Mais si vos utilisateurs le souhaitent pour appeler Foo._new alors rien ne les empêchera de créer un objet sans "votre permission".
> "Si votre usine peut le faire, alors tout le monde le peut" - Je comprends cela, c'est pourquoi j'ai écrit que j'essaie de protéger le code contre une utilisation abusive, pas une utilisation malveillante / délibérée. Votre solution résout le problème, mais semble plus verbeuse que la dernière option de ma question?
L'avantage que je vois dans ma réponse est qu'il ne crée jamais d'instance lorsque Foo () est appelé, contrairement à votre solution. Cela me paraît un peu plus propre pour éviter l'instanciation, car c'est votre premier objectif. Ensuite, nous ajoutons du code supplémentaire pour permettre l'instanciation via une méthode alternative ( _new ). La différence est suttle, je ne suis pas sûr que le mien soit globalement meilleur. Vous pouvez également remplacer votre assertion secret dans __new__ au lieu de __init__ .
Vous pouvez utiliser sys._getframe (1) pour obtenir le cadre de l'appelant, où vous pouvez obtenir la variable locale cls de l'appelant et le nom de la fonction de l'appelant. Pour vous assurer que quelqu'un n'appelle pas Foo .__ new__ depuis une classe différente avec le même nom et le même nom de méthode, vous pouvez vérifier si le nom de fichier du cadre de l'appelant est le même que le nom de fichier du image actuelle:
class Foo:
def __new__(cls):
if sys._getframe(1).f_code.co_filename != sys._getframe(0).f_code.co_filename:
raise RuntimeError('Foo must be instantiated via the FooBarFactory.createFoo method.')
return super().__new__(cls)
de sorte que:
RuntimeError: Foo must be instantiated via the FooBarFactory.createFoo method.
affiche:
Foo()
et :
Foo OK
renvoie:
FooBarFactory.createFoo()
Ou puisque vous contrôlez supposément votre propre fichier, et le FooBarFactor.createFoo est censée être le seul appelant que vous avez dans le fichier qui instancie Foo , la vérification du nom de fichier seule devrait suffire:
import sys
class Foo:
def __new__(cls):
caller_frame = sys._getframe(1)
if 'cls' not in caller_frame.f_locals or \
caller_frame.f_locals["cls"].__name__ != 'FooBarFactory' or \
caller_frame.f_code.co_name != 'createFoo' or \
caller_frame.f_code.co_filename != sys._getframe(0).f_code.co_filename:
raise RuntimeError('Foo must be instantiated via the FooBarFactory.createFoo method.')
print('Foo OK')
return super().__new__(cls)
class FooBarFactory:
@classmethod
def createFoo(cls):
return Foo()
p >
Solution intéressante! Mais si je crée une classe / méthode avec les bons noms, je peux créer moi-même des instances de Foo , n'est-ce pas?
J'ai mis à jour ma réponse avec une vérification supplémentaire du nom de fichier, donc ce n'est pratiquement pas faux.
Oui pratiquement: D, à la fin (je suis presque sûr) vous ne pourrez jamais avoir de garantie donc je trouve la réponse la plus simple meilleure. Je ne vois que deux options, soit l'utilisateur veut casser votre code (auquel cas vous êtes probablement condamné) ou l'utilisateur est paisible et il s'arrêtera dès qu'il verra que Foo () ne le fait pas ' t travailler. Mais j'aime votre idée de toute façon. Pensez-vous qu'il existe un moyen de supprimer les constantes de chaîne comme "FooBarFactory" ? Peut-être qu'en déplaçant Foo dans FooBarFactory.createFoo , cela peut paraître plus propre. Vous pouvez également mettre le long if dans une fonction (pour masquer le désordre).
toto = objet .__ nouveau __ (toto)
Et aussi nommer les différentes conditions, puis effectuer quelque chose comme any (...) ou all (...) .
Hmm, cela me fait penser, y a-t-il un moyen d'instancier un objet Foo si Foo est défini dans createFoo ? Je pense que cela doit être possible, mais je ne sais pas encore comment: p
@cglacet oui, type (some_instance) () vous donnera le type, mais pire, cela casserait le système de type IMO. Chaque instance serait un type distinct, puisque Foo serait une classe unique à chaque fois
Oui, c'est en effet un problème si nous voulons écrire des choses comme type (Factory.createFoo ()) == type (Factory.createFoo ())
@cglacet Je ne peux pas penser à un moyen élégant d'obtenir le nom de la classe d'usine à partir de Foo (la seule option à laquelle je pense est d'utiliser ast.NodeVistor pour parcourir le l'arborescence de code pour trouver la seule classe avec une méthode qui appelle Foo , mais c'est vraiment exagéré), et la définition de Foo dans une méthode introduit d'autres problèmes comme juanpa.arrivillaga a souligné. Mais maintenant, pensez-y, puisque vous êtes censé posséder le fichier qui a les définitions de Foo et FooBarFactory , et createFoo devrait être le seul appelant à Foo dans le fichier, la vérification du nom de fichier seule devrait suffire. Tu ne penses pas?
Hein, c'est une approche intéressante! Savez-vous quelles sont les implications de l'inspection de la pile sur les performances? De plus, je ne vois pas de moyen facile de mettre ce code dans la classe Base , car son __init__ est appelé à partir des Foo __init__ .
Je pense que cette solution pourrait également fonctionner dans __new__ .
En ce qui concerne la modification du nom de fichier à la volée, je ne sais pas comment le faire, mais je sais que les attributs en lecture seule peuvent être modifiés.
@EvilTosha Indeed inspect.stack () est plutôt lent. J'ai mis à jour ma réponse pour utiliser à la place sys._getframe () afin qu'elle soit environ 6000 fois plus rapide dans mes tests.
@ juanpa.arrivillaga Bon point sur la méthode __new__ . J'ai mis à jour ma réponse pour l'utiliser à la place.
@cglacet Je ne pense pas qu'il soit possible de modifier le nom de fichier dans la pile de code à partir de l'API Python normale.
@blhsing mon point est que tout ce rigamarole peut être facilement contourné en faisant foo = object .__ new __ (Foo)
@ juanpa.arrivillaga Oups, je n'ai pas remarqué votre utilisation de la classe object . Bon point.
J'ai trouvé la solution suivante en plus des différentes options dans d'autres réponses:
foo = Foo(23) # AssertionError foo = Factory.create_foo(23) # OK
Maintenant,
class Base:
def __new__(cls, *args, **kwargs):
assert kwargs.get('secret') == Factory._secret, 'use the factory'
return super(Base, cls).__new__(cls)
class Foo(Base):
def __init__(self, param, **kwargs):
self.param = param
class Factory:
_secret = 'super_secret_dont_copy'
@classmethod
def create_foo(cls, param):
return Foo(param=param, secret=cls._secret)
La solution permet de minimiser le code supplémentaire pour des sous-classes supplémentaires de Base (le code de validation est encapsulé dans la classe Base ), mais cela présente l'inconvénient de devoir ajouter ** kwargs à toutes les sous-classes __init__.
"Une option que je vois est de mettre Foo et Bar dans la classe d'usine, mais je veux vérifier leur type, donc ce n'est pas une solution acceptable." Pourquoi n'est-ce pas acceptable? Et comment mettre Foo and Bar à l'intérieur aide-t-il ici pour commencer? Peux-tu élaborer?
@ juanpa.arrivillaga, Hmm, j'ai supposé que les classes internes ne pouvaient pas être facilement accessibles de l'extérieur. Mais apparemment ce n'est pas vrai. Au moins, cela demanderait à l'utilisateur de la classe de lire la documentation dans la classe d'usine. Je n'aime toujours pas la solution car elle implique toutes sortes de problèmes de lisibilité: fichier gonflé, préfixes inutiles, etc.
Qu'est-ce que vous essayez d'empêcher, exactement ?
Vous pouvez définir les classes à l'intérieur des méthodes de classe d'usine elles-mêmes. Cela ne résout pas vos problèmes de perte de lisibilité et de gonflement des fichiers.
@ juanpa.arrivillaga, j'essaie d'empêcher une utilisation accidentelle de
Foo ()"constructeur" par un utilisateur de code sans méfiance. J'essaye également de le faire sans rendre le code inutilement complexe et illisible.