La dynamique des attributs

Emmenez-moi au bout de l'attr ♫

Antoine (entwanne) Rozo

CC BY-SA

La dynamique des attributs

Attributs en Python

Attributs en Python

  • Les attributs permettent d'associer des données à un objet
In [2]:
dana.attr = 10
In [3]:
dana.attr
Out[3]:
10
In [4]:
del dana.attr

Fonctions setattr, getattr et delattr

  • Ces opérations élémentaires correspondent à des fonctions Python
In [10]:
setattr(dana, 'foo', 'bar')
In [6]:
getattr(dana, 'foo')
Out[6]:
'bar'
In [7]:
delattr(dana, 'foo')

Fonction hasattr

  • Une fonction supplémentaire permet de tester la présence d'un attribut
In [11]:
hasattr(dana, 'foo')
Out[11]:
True

Stockage des attributs

  • Les objets Python possèdent un attribut spécial, __dict__
  • Il s'agit d'un dictionnaire qui stocke toutes les données de l'objet
In [12]:
dana.__dict__
Out[12]:
{'foo': 'bar'}
  • Ce dictionnaire est utilisé lors de l'accès à un attribut
In [13]:
dana.__dict__['foo']
Out[13]:
'bar'

Method Resolution Order (MRO)

Method Resolution Order

  • L'accès à un attribut ne se contente pas d'explorer le __dict__ de l'objet
  • Sont aussi analysés celui du type, et de tous les types parents
  • L'ordre d'évaluation des types est défini par le MRO

Method Resolution Order

In [14]:
class A:
    foo = 'A.foo'
    bar = 'A.bar'
    baz = 'A.baz'

class B(A):
    bar = 'B.bar'

b = B()
b.baz = 'b.baz'

b.foo, b.bar, b.baz
Out[14]:
('A.foo', 'B.bar', 'b.baz')

Method Resolution Order

  • On peut connaître le MRO d'une classe en faisant appel à sa méthode mro
In [15]:
B.mro()
Out[15]:
[__main__.B, __main__.A, object]

Method Resolution Order

  • Celui-ci est surtout utile lors d'héritages multiples, il se base sur l'algorithme C3
  • Il permet de linéariser la hiérarchie des classes parentes
In [16]:
class P1:
    foo = 'P1.foo'

class P2:
    foo = 'P2.foo'
    bar = 'P2.bar'

class C(P1, P2):
    pass

C.mro()
Out[16]:
[__main__.C, __main__.P1, __main__.P2, object]
In [17]:
C.foo, C.bar
Out[17]:
('P1.foo', 'P2.bar')

Method Resolution Order

In [18]:
object.mro()
Out[18]:
[object]
In [19]:
class A: pass

A.mro()
Out[19]:
[__main__.A, object]
In [20]:
class B(A): pass

B.mro()
Out[20]:
[__main__.B, __main__.A, object]
In [21]:
class C: pass

C.mro()
Out[21]:
[__main__.C, object]

Method Resolution Order

In [22]:
class D(A, C): pass

D.mro()
Out[22]:
[__main__.D, __main__.A, __main__.C, object]
In [23]:
class E(B, C): pass

E.mro()
Out[23]:
[__main__.E, __main__.B, __main__.A, __main__.C, object]
In [24]:
class F(D, E): pass

F.mro()
Out[24]:
[__main__.F,
 __main__.D,
 __main__.E,
 __main__.B,
 __main__.A,
 __main__.C,
 object]
In [25]:
class G(E, D): pass

G.mro()
Out[25]:
[__main__.G,
 __main__.E,
 __main__.B,
 __main__.D,
 __main__.A,
 __main__.C,
 object]

MRO de l'impossible

In [26]:
class H(A, B): pass
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-26-79e3052e9262> in <module>
----> 1 class H(A, B): pass

TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B
In [27]:
class H(B, A): pass

H.mro()
Out[27]:
[__main__.H, __main__.B, __main__.A, object]

Attributs et méthodes spéciales

__getattr__ et __getattribute__

  • Des méthodes spéciales sont impliquées dans la recherche des attributs d'un objet
  • Lors de l'accès à un attribut, la méthode __getattribute__ est appelée
  • C'est celle-ci qui s'occupe par défaut d'explorer les dictionnaires d'attributs
In [28]:
def __getattribute__(self, name):
    if name in self.__dict__:
        return self.__dict__[name]
    for cls in type(self).mro():
        if name in cls.__dict__:
            return cls.__dict__[name]
    raise AttributeError

__getattr__ et __getattribute__

In [29]:
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius

    def __getattribute__(self, name):
        print(f"Récupération de l'attribut {name}")
        if name == 'fahrenheit':
            return self.celsius * 1.8 + 32
        return super().__getattribute__(name)

t = Temperature(25)
t.celsius
Récupération de l'attribut celsius
Out[29]:
25
In [30]:
t.fahrenheit
Récupération de l'attribut fahrenheit
Récupération de l'attribut celsius
Out[30]:
77.0

Pièges de __getattribute__

  • Attention aux cas de récursions infinies
In [31]:
class WTF:
    def __getattribute__(self, name):
        return self.__dict__[name]

wtf = WTF()
wtf.foo = 0
In [32]:
wtf.foo
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-32-fad73b5a2465> in <module>
----> 1 wtf.foo

<ipython-input-31-273681741015> in __getattribute__(self, name)
      1 class WTF:
      2     def __getattribute__(self, name):
----> 3         return self.__dict__[name]
      4 
      5 wtf = WTF()

... last 1 frames repeated, from the frame below ...

<ipython-input-31-273681741015> in __getattribute__(self, name)
      1 class WTF:
      2     def __getattribute__(self, name):
----> 3         return self.__dict__[name]
      4 
      5 wtf = WTF()

RecursionError: maximum recursion depth exceeded

__getattr__ et __getattribute__

  • __getattr__ est appelée lorsqu'un attribut n'est pas trouvé par __getattribute__
  • Elle permet plus facilement de gérer des attributs dynamiques en plus des existants
In [33]:
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius

    def __getattr__(self, name):
        if name == 'fahrenheit':
            return self.celsius * 1.8 + 32
        raise AttributeError(name)

t = Temperature(25)
t.celsius
Out[33]:
25
In [34]:
t.fahrenheit
Out[34]:
77.0

__setattr__ et __delattr__

  • Ces méthodes sont appelées respectivement pour l'écriture et la suppression d'un attribut
  • Elles sont appelées dans tous les cas, pour tous les attributs

__setattr__ et __delattr__

In [35]:
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius

    def __getattr__(self, name):
        if name == 'fahrenheit':
            return self.celsius * 1.8 + 32
        raise AttributeError(name)

    def __setattr__(self, name, value):
        if name == 'fahrenheit':
            self.celsius = (value - 32) / 1.8
        else:
            super().__setattr__(name, value)

t = Temperature()
t.fahrenheit = 100
t.celsius
Out[35]:
37.77777777777778

Pièges de __setattr__

  • Attention encore aux récursions infinies
In [36]:
class WTF:
    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        self.last_attribute_modified = name

wtf = WTF()
wtf.foo = 0
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-36-4473e83aa31e> in <module>
      5 
      6 wtf = WTF()
----> 7 wtf.foo = 0

<ipython-input-36-4473e83aa31e> in __setattr__(self, name, value)
      2     def __setattr__(self, name, value):
      3         super().__setattr__(name, value)
----> 4         self.last_attribute_modified = name
      5 
      6 wtf = WTF()

... last 1 frames repeated, from the frame below ...

<ipython-input-36-4473e83aa31e> in __setattr__(self, name, value)
      2     def __setattr__(self, name, value):
      3         super().__setattr__(name, value)
----> 4         self.last_attribute_modified = name
      5 
      6 wtf = WTF()

RecursionError: maximum recursion depth exceeded while calling a Python object

Pièges de __setattr__

  • Et aux appels par l'initialiseur
In [37]:
class WTF:
    def __init__(self, path, prefix=''):
        self.path = path
        self.prefix = prefix

    def __setattr__(self, name, value):
        if name == 'path':
            self.path = value + self.suffix
        else:
            super().__setattr__(self, name, value)

wtf = WTF('foo', '/tmp/')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-37-a23d859e0122> in <module>
     10             super().__setattr__(self, name, value)
     11 
---> 12 wtf = WTF('foo', '/tmp/')

<ipython-input-37-a23d859e0122> in __init__(self, path, prefix)
      1 class WTF:
      2     def __init__(self, path, prefix=''):
----> 3         self.path = path
      4         self.prefix = prefix
      5 

<ipython-input-37-a23d859e0122> in __setattr__(self, name, value)
      6     def __setattr__(self, name, value):
      7         if name == 'path':
----> 8             self.path = value + self.suffix
      9         else:
     10             super().__setattr__(self, name, value)

AttributeError: 'WTF' object has no attribute 'suffix'

Attributs et méthodes spéciales

  • En raison des potentiels bugs décrits précédemment, évitez au maximum d'avoir recours à ces méthodes
  • Elles sont de plus complexes à utiliser car nécessitent de traiter tous les attributs un à un
  • Heureusement Python nous offre d'autres facilités pour gérer des attributs dynamiques

Propriétés

Propriétés

  • Les propriétés permettent de simplifier l'usage d'attributs dynamiques
  • Elles associent des fonctions de récupération, de modification et de suppression à un nom d'attribut
  • On associe une propriété à un nom d'attribut en la définissant comme attribut de classe

Propriétés

In [38]:
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius

    def _get_fahrenheit(self):
        return self.celsius * 1.8 + 32

    def _set_fahrenheit(self, value):
        self.celsius = (value - 32) / 1.8

    fahrenheit = property(_get_fahrenheit, _set_fahrenheit)

t = Temperature()
t.fahrenheit = 100
t.celsius
Out[38]:
37.77777777777778

Décorateur @property

  • property peut aussi s'utiliser comme un décorateur
  • Le nom de l'attribut découle alors du nom du getter
In [39]:
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius

    @property
    def fahrenheit(self):
        return self.celsius * 1.8 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) / 1.8

t = Temperature()
t.fahrenheit = 100
t.celsius
Out[39]:
37.77777777777778

Propriétés en lecture seule

  • Le getter peut être implémenté sans le setter
In [40]:
class Rect:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)
    @property
    def area(self):
        return self.width * self.height

rect = Rect(10, 20)
In [41]:
rect.perimeter
Out[41]:
60
In [42]:
rect.area
Out[42]:
200

Descripteurs

Descripteurs

  • Les propriétés sont un sous-ensemble des descripteurs
  • Un descripteur est un objet spécial qui permet de régir le comportement d'un attribut
  • Il possède pour cela des méthodes __get__, __set__ et __delete__

Descripteurs

  • Le descripteur est instancié une seule fois pour toute la classe
  • Ses méthodes spéciales sont appelées lors des différents accès à l'attribut
  • L'objet duquel on accède à l'attribut est alors passé en paramètre

Descripteurs

In [43]:
class Fahrenheit:
    def __get__(self, instance, owner):
        return instance.celsius * 1.8 + 32

    def __set__(self, instance, value):
        instance.celsius = (value - 32) / 1.8

class Temperature:
    def __init__(self, celsius=0):
        self.celsius = 0

    fahrenheit = Fahrenheit()

t = Temperature()
t.fahrenheit = 100
t.celsius
Out[43]:
37.77777777777778

Méthode __get__ des descripteurs

  • Quel est donc ce paramètre owner de la méthode __get__ ?
  • Un descripteur peut-être récupéré depuis la classe et non depuis une instance de cette classe
  • Dans ce cas, le paramètre instance vaudra None, et owner référence toujours la classe utilisée

Méthode __get__ des descripteurs

In [44]:
class Descriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return f'Attribute of class {owner}'
        return f'Attribute of {instance}'

class C:
    attr = Descriptor()

C.attr
Out[44]:
"Attribute of class <class '__main__.C'>"
In [45]:
obj = C()
obj.attr
Out[45]:
'Attribute of <__main__.C object at 0x7f9a684b46d0>'

Descripteurs

  • Ce comportement n'est valable que pour le __get__
  • En effet, la redéfinition et la suppression de l'attribut de classe doivent toujours être possibles
In [46]:
C.attr = 'foo'
In [47]:
del C.attr

Méthode __set_name__

  • Depuis Python 3.6, les descripteurs peuvent aussi comporter une méthode __set_name__ appelée lorsqu'ils sont définis dans une classe
In [48]:
class cachedescriptor:
    def __init__(self, func):
        self.func = func

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, inst, owner):
        if inst is None:
            return self
        if self.name not in inst.__dict__:
            inst.__dict__[self.name] = self.func(inst)
        return inst.__dict__[self.name]

Méthode __set_name__

In [49]:
class Calculation:
    @cachedescriptor
    def result(self):
        print('Complex calculation')
        ...
        return 0

calc = Calculation()
In [50]:
calc.result
Complex calculation
Out[50]:
0

Propriétés

  • Implémentation simple des propriétés (ne gère pas l'utilisation en décorateurs)
In [51]:
class my_property:
    def __init__(self, fget, fset, fdel):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
    def __get__(self, instance, owner):
        return self.fget(instance)
    def __set__(self, instance, value):
        return self.fset(instance, value)
    def __delete__(self, instance):
        return self.fdel(instance)

Méthodes

Méthodes

  • Derrière leur apparente simplicité, les méthodes sont en fait des descripteurs
  • C'est ce qui explique la différence entre méthodes et bound methods
In [52]:
class C:
    def method(self):
        pass

C.method
Out[52]:
<function __main__.C.method(self)>
In [53]:
c = C()
c.method
Out[53]:
<bound method C.method of <__main__.C object at 0x7f9a68345040>>

Méthodes

  • Une méthode est en alors un descripteur autour d'une fonction
  • Ce descripteur réagit différemment suivant si la méthode est accédée depuis la classe ou l'une de ses instances
In [54]:
from functools import partial

class Method:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self.func
        return partial(self.func, instance)

Méthodes

  • Les méthodes de classe fonctionnent de la même manière en utilisant l'owner
In [55]:
class ClassMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return partial(self.func, owner)
  • Les méthodes statiques sont les plus simples et ne dépendent d'aucun descripteur
In [56]:
class StaticMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return self.func

Slots

Slots

  • Tous les objets ne possèdent pas de __dict__
  • Il est possible d'optimiser le stockage des attributs en définissant des slots au niveau de la classe
  • Cela évite l'instanciation d'un dictionnaire mais empêche de définir des attributs non déclarés
In [57]:
class Point:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y
In [58]:
p = Point(3, 4)
p.x, p.y
Out[58]:
(3, 4)
In [59]:
p.z = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-59-33e53240e34b> in <module>
----> 1 p.z = 1

AttributeError: 'Point' object has no attribute 'z'

Slots

  • Les classes utilisant des slots restent compatibles avec les mécanismes d'attributs dynamiques
In [60]:
class Point:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

    @property
    def distance(self):
        return (self.x**2 + self.y**2)**0.5

p = Point(3, 4)
p.distance
Out[60]:
5.0

Conclusion

Python 3.7 : Module et __getattr__

  • Depuis Python 3.7, les modules peuvent aussi définir une méthode spéciale __getattr__
  • Ils permettent plus facilement de gérer des attributs dynamiques au niveau d'un module

Questions ?