Neckbeard Republic: dunder functions

There can be alot of mystery involved in Python when it comes to the dunder methods. The most famous dunder method most python developer are most familar with is the __name__ method as follows.

In [ ]:
def main():
    print 'Ran the main method'


if __name__ == '__main__':
    main()

Many python developers just accept this as one of the many truism that are in programming and memorize this and move on with their lives. In this screencast will be pulling back the curtains on these dunder method and see what is possible and what things you should watch out for!

name

Name is the variable which contains the name of the caller of the module for example if I call a function from the commandline the __name__ has __main__ as the name. Other wise it will contain the name of the module that it is contained in.

new and init

The main difference between these two is:

  1. one controls the creation of the instance (__new__) and returns an instance.

  2. and the other deal with the initialization of the new instance (__init__) and returns nothing

__new__ is mainly used for subclassing immutable types like the example below.

In [92]:
class PositiveInteger(int):
    def __new__(cls, number):
        if number < 0:
            raise ValueError('The number has to be positive integer')
        else:
            return int.__new__(cls, number)

    def __init__(self, number):
        self.number = number

    def get_number(self):
        return self.number
In [93]:
p = PositiveInteger(1)
p.get_number()
PositiveInteger(-2)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-93-50009b309c8e> in <module>()
      1 p = PositiveInteger(1)
      2 p.get_number()
----> 3 PositiveInteger(-2)

<ipython-input-92-d00fbea09aab> in __new__(cls, number)
      2     def __new__(cls, number):
      3         if number < 0:
----> 4             raise ValueError('The number has to be positive integer')
      5         else:
      6             return int.__new__(cls, number)

ValueError: The number has to be positive integer

enter and exit

These dunder functions allow you to create context managers like the ones below which can be pretty cool when have to do something on entrance and exit. Lets look at the following context manager you maybe familiar with.

In [91]:
with open('numbers.txt') as numbers:
    list_of_numbers = numbers.read()
    print list_of_numbers.split()
    print numbers
print numbers
['5', '6', '7', '8', '9', '10', '29', '49', '58', '27']
<open file 'numbers.txt', mode 'r' at 0x107069420>
<closed file 'numbers.txt', mode 'r' at 0x107069420>

Now for example let's say we want to create a redis connection do some stuff in redis and always be sure to close the connection when I am done like a good programmer context managers will come in very handy. Like so:

In [ ]:
class Redis(object):
    
    def __init__(self, rdb):
        self.connection = CreateConnection(rdb)
    
    def __enter__(self):
        return self.connection
    
    def __exit__(self):
        self.connection.close()
In [ ]:
with Redis('redis://localhost:6379') as redis:
    # do things with new redis connection.
    redis.set('neck', 'beard')

str and repr

These two are often confused most developers just have one call the other or just worry about implementing __str__

The difference between the two is mostly semantic but it is useful to know and can be extremely helpful. If you implement a __repr__ and no __str__. __str__ == __repr__

In [89]:
class Person(object):
    def __init__(self, name='', age=10):
        self.name = name
        self.age = age

    def __repr__(self):
        return "Person(name=%r, age=%r)" % (self.name, self.age)
    
    def __str__(self):
        return 'A person name %s at age %d' % (self.name, self.age)
In [90]:
p = Person(name='Mahdi', age=50)
print str(p)
print repr(p)
p2 = Person(name='Mahdi', age=50)
print p2
A person name Mahdi at age 50
Person(name='Mahdi', age=50)
A person name Mahdi at age 50

@total_ordering and eq and one of lt, gt, le, ge,

In [ ]:
from functools import total_ordering

@total_ordering
class Name(object):
    def __init__(self, name):
        self.name = name
    
    def __eq__(self, other):
        if isinstance(other, Name):
            return len(self.name) == len(other.name)
        return False
    
    def __lt__(self, other):
        if isinstance(other, Name):
            return len(self.name) < len(other.name)
        return False
In [88]:
mahdi = Name('Mahdi')
mikey = Name('Mikey')
kelsey = Name('Kelsey')

print mahdi == mikey
print mahdi <= mikey
print kelsey > mahdi
True
True
True

getattr and setattr

These two method are quite useful if you want to control how things are set and retrieved. For example, if we want to raise an error whenever someone asks for a variable that isn't there. We can also limit certain assignments to attributes as well.

In [87]:
class Friend(object):
    def __init__(self, name, nickname):
        self.name = name
        self.nickname = nickname
    
    def __getattr__(self, name):
        raise AttributeError('The attribute you asked for %s for does not exist, clean it up sloppy'% name) 
    
    def __setattr__(self, name, value):
        if value == "Mahdi the Potty" and name == 'nickname':
            raise ValueError("No one calls me Mahdi the Potty")
        else:
            self.__dict__[name] = value
    
f = Friend('Mahdi', 'Mahdi the Bahdi')
# f.first_name
f.nickname = "Mahdi the Ladidai"

mro

Which stands for method resolution order, this can come in handy when dealing with multiple inheritance to clarify which version of the method will be run. This will return a list of classes in order for which the methods will be resolved. For example:

In [84]:
class First(object):
    pass

class Fifth(object):
    pass

class Second(First):
    pass

class Third(Fifth):
    pass

class Fourth(Third, Second):

    def __init__(self):
        pass


print Fourth.__mro__
    
(<class '__main__.Fourth'>, <class '__main__.Third'>, <class '__main__.Fifth'>, <class '__main__.Second'>, <class '__main__.First'>, <type 'object'>)

slots

This is mainly done for performance reasons, for example if you are going to have many and I mean many instances of the same class they each have a dictionary object associated with them. This can take up alot of memory unnecessarily. This is where __slots__ comes in saves the day, when doing this there are few things that you need to be aware of for example.

In [75]:
class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
class Point1(object):
    __slots__ = ('x', 'y')
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
In [83]:
p = Point(5, 6)
p1 = Point1(5, 6)
p.z = 9
print p.z
#p1.z = 8
p1.y = 10
p1.x = 9

print p1.y
print p1.x

p.__dict__
#p1.__dict__

#p1.x = 9
#p1.__dict__
9
10
9
Out[83]:
{'x': 5, 'y': 6, 'z': 9}
In [ ]:
p1.z = 5