#!/bin/env python
# -*- coding: utf8 -*-

"""
gebundene und ungebundene Methoden
"""

from __future__ import print_function
import types

liste = [1, 2, 3]

# die Methode "append" gebunden und ungebunden bereitstellen
gebunden = liste.append
ungebunden = liste.__class__.append # entspricht: list.append

print(gebunden)       # <built-in method append of list object at 0x...>
print(ungebunden)     # <method 'append' of 'list' objects>

# Aufruf der gebundenen Methode
gebunden(4)           # eine 4 an die Liste anhängen
print(liste)          # [1, 2, 3, 4]

# Aufruf der ungebundenen Methode
ungebunden(liste, 5)  # eine 5 an die Liste anhängen
print(liste)          # [1, 2, 3, 4, 5]

# Benutzerdefierte Funktionen besitzen eine Methode __get__() und sind daher
# non-overriding descriptors. Die Deskriptor-Methode __get__() wird beim
# Zugriff auf ein entsprechendes Attribut eines Objekts (z.B. liste.append)
# gerufen und liefert eine gebundene Methode. Man kann __get__() auch direkt
# rufen:

def f(): pass
print(f.__get__('')) # Python 2: <bound method ?.f of ''>
                     # Python 3: <bound method f of ''>

# eigene Listen-Klasse vom eingebauten Typ "list" ableiten; hier sieht man die
# "bound method" bzw. "unbound method" explizit
class mylist(list):
    # Methode append() wird überschrieben
    def append(self, x):
        print('appending', x)
        # Methode append() der Basis- bzw. Superklasse wird gerufen
        super(mylist, self).append(x)
        # bei Python 3 kann man super() ohne Argumente nutzen:
        #   super().append(x)
        #
        # ohne das empfohlene super() ginge es auch so:
        #  list.append(self, x)

# eine Instanz von "mylist" kreieren
ml = mylist((1, 2, 3))
print(ml)

# die Methode 'append' wieder gebunden und ungebunden bereitstellen
gebunden = ml.append
ungebunden = mylist.append

print(gebunden)         # <bound method mylist.append of [1, 2, 3]>
print(ungebunden)       # Python 2: <unbound method mylist.append>
                        # Python 3: <function mylist.append at 0x...>
print(type(gebunden))   # Python 2: <type 'instancemethod'>
                        # Python 3: <class 'method'>
print(type(ungebunden)) # Python 2: <type 'instancemethod'>
                        # Python 3: <class 'function'>

print(isinstance(gebunden, types.MethodType))   # True
print(isinstance(ungebunden, types.MethodType)) # Python 2: True
                                                # Python 3: False

# Aufruf der gebundenen Methode
gebunden(4)           # appending 4
print(ml)             # [1, 2, 3, 4]
ml.append(5)          # appending 5
print(ml)             # [1, 2, 3, 4, 5]

# Aufruf der ungebundenen Methode
ungebunden(ml, 6)     # appending 6
print(ml)             # [1, 2, 3, 4, 5, 6]
mylist.append(ml, 7)  # appending 7
print(ml)             # [1, 2, 3, 4, 5, 6, 7]

# eine Standard-Liste weiß von unserem mylist.append() nichts
l = list((1, 2, 3))
l.append(99)
print(l)              # [1, 2, 3, 99]

# Bei nutzerdefinierten Klassen werden die gebundenen und bei Python 2 auch die
# ungebunden Methoden durch ein Objekt vom Typ types.MethodType repräsentiert.
# Das ist ein dünner Wrapper um ein normales Funktionsobjekt, der diese
# Attribute definiert:
#
#   m.__doc__   Dokumentations-String
#   m.__name__  Name der Methode
#   m.__class__ Klasse, in der die Methode definiert wurde
#   m.__func__  Funktionsobjekt, das die Methode implementiert
#   m.__self__  Instanz, an die die Methode gebunden ist (None im Falle von
#               ungebundenen Methoden)
#  
# Ab Python 3 fehlt bei ungebundenen Methoden der Wrapper vom Typ
# types.MethodType. Es handelt sich hier um das reine Funktionsobjekt, das die
# Methode implementiert. Außerdem erfolgt für den Parameter self keine
# Typprüfung mehr, während Python 2 einen TypeError wirft, wenn man der
# ungebundenen Methode ein Objekt übergibt, das keine Instanz der Klasse ist,
# für die man die ungebundene Methode ruft.

class liste():
    def append(self):
      'special append'
      pass

l = liste()
gebunden = l.append
ungebunden = liste.append

for obj in gebunden, ungebunden:
    # __name__ taucht bei dir() hier nicht auf, wir ergänzen es daher
    attribute = dir(obj) + ['__name__']
    for name in '__doc__', '__name__', '__class__', '__func__', '__self__':
        if name in attribute:
            attr = getattr(obj, name)
            print(name, attr, type(attr))
    print()

# Python 2:
# 
#   __doc__ special append <type 'str'>
#   __name__ append <type 'str'>
#   __class__ <type 'instancemethod'> <type 'type'>
#   __func__ <function append at 0x...> <type 'function'>
#   __self__ <__main__.liste instance at 0x...> <type 'instance'>
#   
#   __doc__ special append <type 'str'>
#   __name__ append <type 'str'>
#   __class__ <type 'instancemethod'> <type 'type'>
#   __func__ <function append at 0x...> <type 'function'>
#   __self__ None <type 'NoneType'>
#  
# Python 3:
#
#   __doc__ special append <class 'str'>
#   __name__ append <class 'str'>
#   __class__ <class 'method'> <class 'type'>
#   __func__ <function liste.append at 0x...> <class 'function'>
#   __self__ <__main__.liste object at 0x...> <class '__main__.liste'>
#  
#   __doc__ special append <class 'str'>
#   __name__ append <class 'str'>
#   __class__ <class 'function'> <class 'type'>
