Salutations,

long time no see (oui oui je vais éviter l'anglais ;p), à vrai dire depuis les concours c2i diggers, ça date, salut Nix et … wazaaaaa !

Pour des raisons que je suis prêt à défendre même si ça n'est pas le sujet, j'essaye de mettre en place un mécanisme "fort" de gestion des membres de classes public/protected/private. Fort mais simple, l'idée étant que ça doit rester léger dans l'utilisation …

Après de nombreuses recherches et essais ratés, j'y suis presque, mais ça coince

les contraintes :
- doit passer sur toutes les versions de python à partir de la 2.5, sans tests bourrins sur du sys.version_info dans les modules (2.5, 2.6 et 2.7 en activité, 3.x à l'horizon)
- le plus léger possible dans la syntaxe pour ne pas polluer la définition des classes
- toujours pour ne pas polluer, pas de brouzouf au niveau des locales de la classe, tout doit se passer dans les accesseurs
- fonctionnement classique : public sans restriction, private réservé à la classe, protected accessible aux sous-classes
- support complet (get/set/delattr)
- respect des conventions python en cas d'inspection (simple / double underscore)

Mon approche actuelle se base sur les décorateurs qui sont en partie viables dès la 2.5 tant qu'on reste dans une utilisation simple (pas de @property / @prop.setter/deleter, pas de surcharge sale des __setattr__ __getattr__ non plus à l'inverse). Le but étant de me retrouver avec des définition de membres de ce type :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
from MClasses import public, protected, private
 
class MyClass(object):
	def __init__(self):
		self._pub    = 42
		self._prot   = 42
		self.__priv  = 42
 
        @public
	def pub(): pass
 
        @protected
	def prot(): pass
 
        @private
	def priv(): pass
 
	def assign(self):
		self.pub  = 666
		self.prot  = 666
		self.priv  = 666
 
class MySubClass(MyClass):
	pass
Dans ce contexte, un myclass.assign() doit marcher de bout en bout, un mysubclass.assign() doit lever une exception sur le self.priv, et une affectation de l'extérieur doit péter sur prot et priv.

Je coince pour l'instant à la récupération de la classe d'origine d'un membre dans les decorateurs, étant donné que la classe n'existe justement pas lors du chargement de la définition … j'ai tenté de créer une classe vide juste avant la "vraie" définition (class MyClass(object): pass) pour pouvoir la passer en argument (genre @public(MyClass)), mais l'id diffère (self.__class__ != MyClass), et la recherche d'héritage par issubclass/isinstance échoue, logique.

Quelques bouts de mon implémentation super naïve :

get_call_class.py, pour récupérer la classe de l'objet qui accède à un membre :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
import inspect
 
from   get_frame_class import get_frame_class
 
def get_call_class(depth=1):
    """class get_call_class(int depth=1)                                                                              
                                                                                                                      
    Returns the current calling class at given depth (1 by default so that we get                                     
    the first container available)."""
 
    # add 2 depth to avoid get_call_class and get_frame_class levels                                                  
    depth += 2
 
    stack = inspect.stack()
    if stack and len(stack) > depth and stack[depth]:
        return get_frame_class(stack[depth][0])
 
    return None
get_frame_class.py, pour extraire la classe à partir d'une frame :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
import inspect
 
def get_frame_class(obj):
    """class get_frame_class(frame obj)                                                                               
                                                                                                                      
    Returns the class object for the given frame (see inspect module)."""
 
    args, _, _, value_dict = inspect.getargvalues(obj)
    if len(args) and args[0] == 'self':
        instance = value_dict.get('self', None)
        if instance:
            return getattr(instance, '__class__', None)
 
    return None
get_func_class.py pour avoir la classe de l'objet qui a défini un membre / méthode (c'est là que ça bloque ;p)
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
 
import inspect
 
def get_func_class(func):
  if hasattr(func, "im_class"):
    for cls in inspect.getmro(func.im_class):
      if func.__name__ in cls.__dict__:
        return cls
 
  return None
safe_decorator.py, un décorateur central appelé par les décorateurs public/private/protected lors de la construction, merci stack overflow !
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
from get_func_class import get_func_class
from safe_getattr   import safe_getattr
from safe_setattr   import safe_setattr
from safe_delattr   import safe_delattr
 
def safe_decorator(func, scope):
    ops = func() or {}
    name = ops.get("prefix", "__" if scope == "private" else "_") + func.__name__
 
    cls = get_func_class(func)
 
    fget = ops.get("fget", lambda self: safe_getattr(self, name, scope, cls))
    fset = ops.get("fset", lambda self, value: safe_setattr(self, name, value, scope, cls))
    fdel = ops.get("fdel", lambda self: safe_delattr(self, name, scope, cls))
 
    return property(fget, fset, fdel, ops.get("doc", ""))
public.py, private.py, protected.py, les surcharges de safe_decorator, passant juste un argument "scope" pour préciser dans quel cas on se trouve :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
 
def public(func):                                                                                                   
    return safe_decorator(func, "public")
 
def private(func):                                                                                                   
    return safe_decorator(func, "private")
 
def protected(func):                                                                                                   
    return safe_decorator(func, "protected")
safe_setattr.py, le setter générique (laissons de côté les safe_getattr et safe_delattr pour l'instant, il suffit de les remplacer par de simples getattr/delattr)
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
from get_call_class import get_call_class
 
def safe_setattr(self, name, value, scope, cls):
    caller = get_call_class()
    print "SETATTR " + name + " " + str(cls) + " " + str(caller)
 
    doit = True
    if scope == "private":
        doit = caller == cls
    elif scope == "protected":
        doit = cls and issubclass(caller, cls)
 
    if doit:
        setattr(self, name, value)
    else:
	raise AttributeError(str(cls) + "." + name + " is " + scope + " !")
La question principale reste donc : comment récupérer la classe de définition d'un membre dans un décorateur, ou en tout cas un élément de comparaison avec la classe appelante ? Toute astuce sera la bienvenue !

La question subsidiaire : l'approche vous semble viable ? réalisable ? pythonesque ? ^^

Quoi qu'il en soit merci aux courageu(x|ses) qui auront eu le courage de lire cet horrible pavé jusqu'au bout !

Tonio