In [ ]:
class count_calls:
    def __init__(self, f):
        self.count = 0
        self.f = f
        
    def __call__(self, *args, **kwargs):
        self.count += 1
        print('Count nb {} to {}'.format(self.count, self.f))
        return self.f(*args, **kwargs)


# Equivalent to:
# add_up = count_calls(add_up)
@count_calls
def add_up(x, y, *, a, b):
    return x + y + a + b

print(add_up(1, 2, a = 2, b = 3))
print(add_up(4, 5, a = 6, b = 7))
print(add_up(8, 9, a = 10, b = 11))

In [ ]:
def count_calls(f):
    count = 0
    
    def wrap(*args, **kwargs):
        nonlocal count
        count += 1
        print('Count nb {} to {}'.format(count, f))
        return f(*args, **kwargs)
        
    return wrap


# Equivalent to:
# add_up = count_calls(add_up)
@count_calls
def add_up(x, y, *, a, b):
    return x + y + a + b

print(add_up(1, 2, a = 2, b = 3))
print(add_up(4, 5, a = 6, b = 7))
print(add_up(8, 9, a = 10, b = 11))

In [ ]:
def count_calls_starting_from(start = 0):
    def count_calls(f):
        count = start
    
        def wrap(*args, **kwargs):
            nonlocal count
            count += 1
            print('Count nb {} to {}'.format(count, f))
            return f(*args, **kwargs)
        
        return wrap
    
    return count_calls


# Equivalent to:
# add_up = count_calls_starting_from(1)(add_up)
@count_calls_starting_from(1)
def add_up(x, y, *, a, b):
    return x + y + a + b

print(add_up(1, 2, a = 2, b = 3))
print(add_up(4, 5, a = 6, b = 7))
print(add_up(8, 9, a = 10, b = 11))

In [ ]:
def count_calls(cls):
    def wrap(datum):
        wrap.count += 1
        print('Count nb {} to {}'.format(wrap.count, cls))
        return cls(datum)
    
    wrap.count = 0
    return wrap


# Equivalent to:
# C = count_calls(C)
@count_calls
class C:
    def __init__(self, datum):
        self.datum = datum


I1, I2, I3 = C(11), C(12), C(13)
print(I1.datum, I2.datum, I3.datum)

In [ ]:
class C:
    count_1 = 0
    count_2 = 0
    
    def __init__(self):
        C.count_1 += 1
        C.count_2 += 1
    
    def display_count_1(mark):
        print('count_1' + mark, C.count_1)

    # Equivalent to:
    # display_count_2 = staticmethod(display_count_2)
    @staticmethod
    def display_count_2(mark):
        print('count_2' + mark, C.count_2)


I1, I2, I3 = C(), C(), C()
C.display_count_1(':')
C.display_count_2('...')
I2.display_count_2(' ')

In [ ]:
class C:
    count = 0
    
    def __init__(self):
        C.count += 1
    
    # Equivalent to:
    # display_count = classmethod(display_count)
    @classmethod
    def display_count(cls, mark):
        print('count for {}'.format(cls.__name__) + mark, C.count)


I1, I2, I3 = C(), C(), C()
C.display_count('...')
I2.display_count(':')

A descriptor is any class with at least one of the three methods:

  • __get__(self, instance, owner)
  • __set__(self, instance, value)
  • __delete__(self, instance)

It is called:

  • a data descriptor if it implements both __get__() and __set__()
  • a non-data descriptor if it implements __get__() but not __set__().

In [ ]:
class D:
    def __init__(self):
        self.datum = 'Descriptor datum'

    def __get__(self, instance, owner):
        print(self.datum)
        print(owner._datum)
        return instance._datum
        
    def __set__(self, instance, value):
        self.datum = 'New descriptor datum'
        instance._datum = value
        
    def __delete__(self, instance):
        print('Deleting instance datum')
        del instance._datum


class C:
    _datum = 'Owner datum'

    def __init__(self):
        self._datum = 'Instance datum'

    datum = D()


I = C()
print(I.datum)
print()

I.datum = 'New instance value'
print(I.datum)
print()

del I.datum
print()

I = C()
print(I.datum)

In [ ]:
class DataDescriptor:
    def __get__(self, instance, owner):
        return 'X1'
    
    def __set__(self, instance, owner):
        pass


class NonDataDescriptor:
    def __get__(self, instance, owner):
        return 'X3'
    
    
class C:
    x1 = DataDescriptor()
    x3 = NonDataDescriptor()

    def __init__(self):
        self.x1 = 'x1'
        self.x2 = 'x2'
        
        
I = C()
print(I.x1, I.x2, I.x3)
I.x3 = 'x3'
print(I.x3)

In [ ]:
class C:
    def __init__(self, datum):
        self._datum = datum
    
    # Equivalent to:
    # datum = property(fget = datum, fset = None, fdel = None, doc = None)
    # Using that form would set C.datum.__doc__ to the value of doc;
    # with the decorator, that value is instead 'For illustration purposes'.
    @property
    def datum(self):
        'For illustration purposes'
        print('You asked for the value of datum')
        return self._datum
    # C.datum is now a descriptor, with in particular
    # - the built-in methods getter, setter and deleter;
    # - the functions fget, fset, fdel;
    # - the method-wrappers __get__, __set__, __delete__. 
    # C.datum.__get__ is a method wrapper of C.datum.fget (the function above).

    # Equivalent to:
    # datum = datum.setter(datum)
    # Returns a copy of datum with C.datum.fset assigned the function below.
    # C.datum.__set__ is a method wrapper of C.datum.fset.
    @datum.setter
    def datum(self, value):
        print('You want to modify the value of datum')
        self._datum = value

    # Equivalent to:
    # datum = datum.deleter(datum)
    # Returns a copy of datum with C.datum.fdel assigned the function below.
    # C.datum.__delete__ is a method wrapper of C.datum.fdel.
    @datum.deleter
    def datum(self):
        print('You have decided to delete datum')
        del self._datum


I = C(3)
print(I.datum)

print()
I.datum = 4
print()

print(I.datum)
print()

del I.datum