Python Metaprogramming

META ~ "beyond"

Metadata

Metaphysics

Metaprogramming

AGENDA:

 

- Duck typing

- Exec and eval

- Decorators

- Descriptors

- Metaclasses

Duck typing

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

 

"If there is the method we need, we don't care about the type"

Duck typing

Duck Typing

class Duck:
    def quack(self):
        print("Quack!")

class Panda:
    def quack(self):
        print("Panda quack!")

def duck_duck_go(obj):
    obj.quack()


duck_duck_go(Duck())  # Works
duck_duck_go(Panda())  # Works
duck_duck_go(1)  # AE: 'int' object has no attribute 'quack'

Duck Typing

def duck_duck_go(obj):
    if hasattr(obj, 'quack'):
        obj.quack()


duck_duck_go(Duck())  # Works
duck_duck_go(Panda())  # Works
duck_duck_go(1)  # Nothing happens

hasattr(object, name)

Checks if object has the given name attribute.

class Person:
    def __init__(self, name):
        self.quack = name
duck_duck_go(Person('Ivo'))
# TypeError: 'str' object is not callable

setattr(obj, name, value)

class Panda:
    pass


def create_panda(attributes):
    panda = Panda()

    for key, value in attributes.items():
        setattr(panda, key, value)

    return panda

setattr(obj, name, value)

p = create_panda({'name': 'Ivo', 'age': 23, 'weight': 80})
print(p.__dict__)  # {'name': 'Ivo', 'age': 23, 'weight': 80} 
p = create_panda({'name': 'Ivo', 'weight': 80})
print(p.__dict__)  # {'name': 'Ivo', 'weight': 80} 

callable(object)

def duck_duck_go(obj):
    if hasattr(obj, 'quack')\
        and callable(getattr(obj, 'quack')):
        obj.quack()

Checks if object is callable (__call__)

What means "callable" ?

 

What about the decorators ? 🤔

 

exec and eval

😱

Exec python code

code = '''
for i in range(0, 3):
    print(i)
'''

exec(code)

# 0
# 1
# 2

Dynamic execution of Python code. Returns None.

Eval python code

expression = '1 + 2*(3**3)'
print(eval(expression))   # 55

Evals a single expression, returns result

code = '''
for i in range(0, 3):
    print(i)
'''

eval(code) # Syntax error

Decorators

def debug(func):
    fname = func.__qualname__

    @wraps(func)
    def wrapper(*arg, **kwargs):
        print('Calling {}'.format(fname))

        return func(*arg, **kwargs)

    return wrapper

Decorate methods?

class Panda:

    @debug
    def be_panda(self):
        print('Being panda')
    
    @debug
    def awesome(self):
        print('Pandas are awesome!')

Repetative work. Can we decorate a class?

@debugmethods
class Panda:

    def be_panda(self):
        print('Being panda')

    def awesome(self):
        print('Pandas are awesome!')

Class decorators!

def debugmethods(cls):
    return cls
  • Takes a class

  • Should return the modified class

  • This is where we go meta. We alter our class so all methods are decorated with @debug

Class decorators!

def debugmethods(cls):
    for attr, value in vars(cls).items():
        if callable(value):
            setattr(cls, attr, debug(value))

    return cls

Putting everything together.

@debugmethods
class Panda:

    def be_panda(self):
        print('Being panda')

    def awesome(self):
        print('Pandas are awesome!')


p = Panda()
p.be_panda() # prints 'Calling be_panda'
p.awesome()  # prints 'Calling awesome'

Class decorators!

Class decorators

@debugmethods
class Panda:
    pass


@debugmethods
class Person:
    pass


@debugmethods
class Task:
    pass

# ...

Descriptors

- Called for object being accessed as an atrubite of another object

- If we have class A and class  B


class Panda:
    def __get__(self, instance, owner):
        # You're trying to get me ?
        return super().__get__(instance, owner)

    def __set__(self, instance, value):
        # You're trying to change me ?
        return super().__set__(instance, value)

    def __delete__(self, instance):
        # You're trying to delete me ?
        return super().__delete__(instance)

Descriptors


class Panda:
    def __get__(self, instance, owner):
        return "Don't touch me"

    def __set__(self, instance, value):
        print("You're trying to change me :O ?!")

    def __delete__(self, instance):
        print("You can't delete me :D !")

Descriptors

DEMO

We need to go deeper.

Types.

Every value in Python has a type.

>>> type(5)
<class 'int'>
>>> type('python')
<class 'str'>
>>> class Panda: pass
... 
>>> p = Panda()
>>> type(p)
<class '__main__.Panda'>

Types.

Every type is defined by a class

>>> class Panda: pass
... 
>>> p = Panda()
>>> type(p)
<class '__main__.Panda'>
>>> int
<class 'int'>
>>> str
<class 'str'>
>>> Panda
<class '__main__.Panda'>

Types.

Whats the type of a class?

>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(Panda)
<class 'type'>
>>> type(type)
<class 'type'>

Every value in Python has a type.

Every type is defined by a class.

The type of class is type.

There is a class type

class type:
    ...

But first, lets deconstruct a class!

__init__()

vs

__new__()

class Panda:
    def __new__(cls, name):
        print('This is the constructor')
        return super().__new__(cls)

    def __init__(self, name):
        print('This is the initializator')
        self.name = name

    def __delete__(self):
        print('Aaand this is the destructor')
        return super().__delete__(self)

__dict__ or vars(obj)

The dictionary with attributes of the given object, module or class.

Panda.__dict__ == vars(Panda)

p = Panda()
p.__dict__ == vars(p)

module.__dict__ == vars(module)

Class deconstruction

class Panda:
    def __init__(self, name):
        self.name = name

    def be_panda(self):
        print('Being panda')

Class consist of:

  1. Name

  2. List of base classes

  3. Class dictionary with functions.

Step 1: Body gets isolated

body = '''
def __init__(self, name):
    self.name = name

def be_panda(self):
    print('Bamboo & sleep')
'''

Step 2: Class dictionary is created

clsname = 'Panda'
bases = (object, )

clsdict = type.__prepare__(clsname, bases)

Step 3: Body is executed in clsdict

exec(body, globals(), clsdict)

Step 4: Class is constructed from type

Panda = type(clsname, bases, clsdict)
body = '''
def __init__(self, name):
    self.name = name

def be_panda(self):
    print('Bamboo & sleep')
'''

clsname = 'Panda'
bases = (object, )

clsdict = type.__prepare__(clsname, bases)

exec(body, globals(), clsdict)

Panda = type(clsname, bases, clsdict)

p = Panda('Ivo')
print(p.name)
p.be_panda()

type is called a metaclass

class Panda(metaclass=type):
    def __init__(self, name):
        self.name = name

    def be_panda(self):
        print('Being panda')

metaclass is a class that handles class creation

Usually defines __new__ or __init__

Our own metaclass

class mytype(type):
    def __new__(cls, name, bases, clsdict):
        clsobj = super().__new__(cls, name, bases, clsdict)
        print('Constructed new type')
        return clsobj

class Panda(metaclass=mytype):
    pass

p = Panda() # Prints 'Constructed a new type'

Metaclasses

@debugmethods
class Panda:
    pass


@debugmethods
class Person:
    pass


@debugmethods
class Task:
    pass

# ...

Metaclasses

class debugmeta(type):
    def __new__(cls, clsname, bases, clsdict):
        clsobj = super().__new__(cls, clsname, bases, clsdict)
        # Decoration here.
        clsobj = debugmethods(clsobj)
        return clsobj

Metaclasses

class Base(metaclass=debugmeta):
    pass

class Panda(Base):
    pass


class Person(Base):
    pass


class Task(Base):
    pass

Metaclasses propagate down the hierarchy! 

Big picture:

Decorators are about wrapping & modifying functions.

Class decorators are about wrapping & modifying all methods in given class.

Metaclasses are about wrapping & modifying the hierarchy

Metaclass example: no multiple inheritance

class no_multiple_inheritence(type):
    def __new__(cls, name, bases, clsdict):
        if len(bases) > 1:
            raise TypeError('No multiple inheritence!')

        return super().__new__(cls, name, bases, clsdict)


class Base(metaclass=no_multiple_inheritence):
    pass

Metaclass example: no multiple inheritance

class A(Base):
    pass


class B(Base):
    pass


class C(A, B): # Raises error
    pass

Materials:

Python Metaprogramming

By Hack Bulgaria

Python Metaprogramming

  • 137
Loading comments...

More from Hack Bulgaria