Hériter de str et survivre

Rémi Bois
PyCon Fr 2019 - 3 Novembre 2019

Docteur en Informatique
Ingénieur R&D Senior @ Jouve

  • Traitement des langues
  • Machine Learning et IA
  • Extraction d'information
  • Python

  • 2 000 salariés dans le monde
  • BPO, SaaS, SDK, et mobile

Jouve recrute

  • DevOps
  • Produits
  • Applications mobiles

Hériter de str

L'objectif


				  class PyconStr(str):
				    pass
				  # Magic code
			      

				  s = PyconStr(" bonjour ", "Pycon")
				  print(s) # " bonjour "
				  s = s.strip().shout().capitalize()
				  print(s) # "Bonjour!"
			      

Ne faites pas ça !

Encapsulez plutôt :

				  class JolieStr:
				    def __init__(self, content, info):
				      self._content = content
				      self._info = info
			      

				  s = JolieStr(" bonjour ", "Pycon").strip().shout().capitalize()
				  print(s) # "Bonjour!"
			      

Faites ça (et mieux plus tard) !


				  class JolieStr:
				    def __init__(self, content, info):
				      self._content = content
				      self._info = info

				    def capitalize(self):
				      self._content = self._content.capitalize()
				      return self

				    def shout(self):
				      self._content = self._content + '!'
				      return self

				    def strip(self):
				      self._content = self._content.strip()
				      return self

				    def __repr__(self):
				      return self._content
			      

				  s = JolieStr(" bonjour ", "Pycon")
				  print(s) # " bonjour "
				  s = s.strip().shout().capitalize()
				  print(s) # "Bonjour!"
			      

BasicStr

Première tentative


				  class BasicStr(str):
				    def __init__(self, content, info):
				      super().__init__(content)
				      self._info = info
			      

				  s = BasicStr(" bonjour ", "Pycon")
				  print(s)
			      

Première Traceback


				  Traceback (most recent call last):
				  File "pyconfr2019isawesome.py", line 6, in module
				     s = BasicStr(" bonjour ",  "Pycon")
				  TypeError: decoding str is not supported
			      

Comprenons


				  class BasicStr(str):
				    def __init__(self, content, info):
				      print("Hello there!", content)
				      super().__init__(content)
				      self._info = info
			      

				  Traceback (most recent call last):
				  File "pyconfr2019isawesome.py", line 6, in module
				     s = BasicStr(" bonjour ",  "Pycon")
				  TypeError: decoding str is not supported
			      

A __new__ challenger arrives!

object.__init__(self[, ...]):
Called after the instance has been created (by __new__()), but before it is returned to the caller.

object.__new__(cls[, ...]):
__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.

NewStr

Newer is better


				  class NewStr(str):
				    def __new__(cls, content, info):
				      obj = super().__new__(cls, content)
				      return obj
				  
				    def __init__(self, content, info):
				      super().__init__()
				      self._info = info

				    def shout(self):
				      return self + "!"
			      

				  s = NewStr(" bonjour ", "Pycon")
				  print(s)
				  print(s.upper())
				  print(s.shout())
			      

Challenge completed?


				  s = NewStr(" bonjour ", "Pycon")
				  print(s)
				  print(s.upper())
				  print(s.shout())
			      

				  bonjour 
				  BONJOUR 
				  bonjour !!
			      

A new Traceback


				  s = NewStr(" bonjour ", "Pycon")
				  print(s.shout().shout())
			      

				  Traceback (most recent call last):
				  File "pyconfr2019isawesome.py", line 6, in module
				    print(s.shout().shout())
				  AttributeError: 'str' object has no attribute 'shout'
			      

Facile !


				  class NewStr(str):
				    def __new__(cls, content, info):
				      obj = super().__new__(cls, content)
				      return obj
				  
				    def __init__(self, content, info):
				      super().__init__()
				      self._info = info

				    def shout(self):
				      return NewStr(self + "!", self._info)
			      

				  print(s.shout().shout())
				  bonjour !!!!
			      

Insuffisant


				  print(s.upper().shout())
			      

				  Traceback (most recent call last):
				  File "pyconfr2019isawesome.py", line 6, in module
				    print(s.upper().shout())
				  AttributeError: 'str' object has no attribute 'shout'
			      

Il faut aussi intercepter les méthodes de str !

InterceptStr

__getattribute__


class InterceptStr(str):

    def __new__(cls, content, info):
        obj = super().__new__(cls, content)
        return obj

    def __init__(self, content, info):
        super().__init__()
        self._info = info

    def __getattribute__(self, name):
        if name in dir(str):  # only handle str methods here
            return # the right function
        else:  # do the usual thing
            return super().__getattribute__(name)

    def shout(self):
        return InterceptStr(self + "!", self._info)
			      

object.__getattribute__(self, name):
Called unconditionally to implement attribute accesses for instances of the class.

getattr à la rescousse


				  class InterceptStr(str):

				    def applyfunction(self, funcname, *args, **kwargs):
				      value = getattr(super(), funcname)(*args, **kwargs)
				      if isinstance(value, str):
				        return InterceptStr(value, self._info)
				      return value
				  
				    def __getattribute__(self, name):
				      if name in dir(str):  # only handle str methods here
				        return functools.partial(self.applyfunction, name)
				      else:  # do the usual thing
				        return super().__getattribute__(name)

			      

				  s = InterceptStr(" bonjour ", "Pycon")
				  print(s) # ' bonjour '
				  print(s.upper()) # ' BONJOUR '
				  print(s.shout()) # ' bonjour !'
				  print(s.shout().shout()) # ' bonjour !!'
				  print(s.upper().shout()) # ' BONJOUR !'
				  print(s.shout().split('j')) # [' bon', 'our !']
			      

PyconStr


class PyconStr(str):

    def __new__(cls, content, info):
        obj = super().__new__(cls, content)
        return obj

    def __init__(self, content, info):
        super().__init__()
        self._info = info

    def applyfunction(self, funcname, *args, **kwargs):
        value = getattr(super(), funcname)(*args, **kwargs)
        if isinstance(value, str):
            return PyconStr(value, self._info)
        return value

    def __getattribute__(self, name, *args, **kwargs):
        if name in dir(str):  # only handle str methods here
            return functools.partial(self.applyfunction, name)
        else:  # do the usual thing
            return super().__getattribute__(name)

    def shout(self):
        return PyconStr(self + "!", self._info)

			      

En conclusion

On a vu

  • __new__ pour l'initialisation des objets immutables
  • __getattribute__ pour intercepter les méthodes
  • getattr pour appliquer une méthode à partir de son nom

Le retour de la JolieStr


class JolieStr:
    def __init__(self, content, info):
        self._content = content
        self._info = info

    def shout(self):
        self._content = self._content + '!'
        return self

    def applyfunction(self, name, *args, **kwargs):
        self._content = getattr(self._content, name)(*args, **kwargs)
        return self

    def __getattribute__(self, name):
        if name in dir(str):  # only handle str methods here
            return functools.partial(self.applyfunction, name)
        else:
            return super().__getattribute__(name)

    def __repr__(self):
        return str(self._content)
			      

				  s = JolieStr(" bonjour ", "Pycon")
				  print(s) # ' bonjour '
				  print(s.upper().shout()) # ' BONJOUR !'
				  print(s.shout().split('j')) # [' bon', 'our !']				  
			      

Merci


  • remibois.contact@gmail.com / rbois@jouve.com
  • github.com/sildar
  • linkedin.com/in/remi-bois
  • ledatablog.com