Hints

PRAWDA I FAUSZ

Aby kontrolować wartość prawdy w instancjach klas zdefiniowanych przez użytkownika, należy korzystać z metod specjalnych __nonzero__ lub __len__. Korzystamy z __len__, jeżeli nasza klasa jest kontenerem, który ma długość (przykład z sieci, Code Like a Pythonist).


class MyContainer(object):
    """Klasa definiująca pewien kontener."""

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

    def __len__(self):
        """Return my length."""
        return len(self.data)

Jeżeli nasza klasa nie jest kontenerem, korzystamy z __bool__ (Py3) i __nonzero__ (Py2).


class MyClass(object):
    """Klasa nie będąca kontenerem."""

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

    def __bool__(self):    Py3
        """Return my truth value (True or False)."""
        # This could be arbitrarily complex:
        return bool(self.value)

    # Kompatybilność Py2 i Py3.
    __nonzero__ = __bool__

DESTRUKTOR


class Life:

    def __init__(self, name="NN"):   # konstruktor
        print("Hello {}".format(name))
        self.name = name

    def __str__(self):
        return self.name

    def __del__(self):   # destruktor
        print("Goodbye {}".format(self.name))

adam = Life("Adam")
# Zamazujemy jedyną referencję do Adama, działa destruktor.
adam = Life("Bogdan")
print("Before delete")
print(adam)
# Usuwamy nazwę adam z przestrzeni nazw.
# Zamazujemy jedyną referencję do Bogdana, działa destruktor.
del adam
print("After delete")

# Wypisywane komunikaty.
Hello Adam
Hello Bogdan
Goodbye Adam
Before delete
Bogdan
Goodbye Bogdan
After delete

HASHING

Każda klasa Pythona ma metodę __hash__(), której domyślna implementacja ma postać:


def __hash__(self):
    return id(self)

User może zaimplementować to na nowo po swojemu. Funkcja hash(X) wywołuje metodę X.__hash__(). Klasy wbudowane Pythona z reguły redefiniują metodę __hash__().


# Przykładowo dla int mamy coś takiego.
def __hash__(self):
    return int(self)

# Dla list mamy wyjątek.
def __hash__(self):
    raise TypeError("list objects are unhashable")

Główna idea dotycząca hash jest taka, że jeżeli dwa obiekty mają różny hash, to nie są równe, ale jeżeli mają taki sam hash, to _mogą_ być równe (to jest nazywane "hash collision"). Jedyne wymaganie ze strony Pythona jest aby wartości hash nie zmieniały się po tym, jak zostaną zwrócone przez metodę __hash__().

Trzeba pamiętać, że hashing jest platform-dependent. Stąd w SymPy mogą być różne kolejności zwracanych wyrażeń, co rodzi problemy przy pisaniu testów (np. doctest).

http://stackoverflow.com/questions/17192418/hash-function-in-python
hash() is randomised by default each time you start a new instance of recent versions (Py3.3+) to prevent dictionary insertion DOS attacks. If you want something that does hash to the same thing every time, use one of the hashes in hashlib (import hashlib).

METHOD RESOLUTION

Niech a, b i c będą instancjami pewnej klasy Pythona. Sprawdzamy co się dzieje przy wywołaniu polecenia a == b. Python wywołuje metody w podanej kolejności. Jeżeli dana metoda nie jest zaimplementowana, lub jeżeli zwróci wyjątek NotImplemented, Python przeskakuje ją i próbuje następnej, aż do skutku. Ostatnia linia zawsze kończy się sukcesem.


# Polecenie a == b.
a.__eq__(b)
b.__eq__(a)
a.__cmp__(b)   # Py2
b.__cmp__(a)   # Py2
id(a) == id(b)

# Polecenie a != b.
a.__ne__(b)
b.__ne__(a)
a.__cmp__(b)   # Py2
b.__cmp__(a)   # Py2
id(a) != id(b)      # tu jest błąd w SymPy

# Polecenie a < b.
a.__lt__(b)
b.__gt__(a)
a.__cmp__(b)   # Py2
b.__cmp__(a)   # Py2
id(a) < id(b)

# Polecenie a <= b.
a.__le__(b)
b.__ge__(a)
a.__cmp__(b)   # Py2
b.__cmp__(a)   # Py2
id(a) <= id(b)

# Polecenie a > b.
a.__gt__(b)
b.__lt__(a)
a.__cmp__(b)   # Py2
b.__cmp__(a)   # Py2
id(a) > id(b)

# Polecenie a >= b.
a.__ge__(b)
b.__le__(a)
a.__cmp__(b)   # Py2
b.__cmp__(a)   # Py2
id(a) >= id(b)

# Jeżeli napiszemy
a in {d: 5}
a in set([d, d, d])
set([a, b]) == set([a, b])
# Python najpierw porównuje hash, tzn.:
a.__hash__()
d.__hash__()
# Jeżeli hash(a) != hash(d), to wynik wyrażenia a in {d: 5}
# jest natychmiast False.
# Jeżeli hash(a) == hash(d), to Python przechodzi przez method 
# resolution dla operatora == jak pokazano powyżej.

Widać, że w method resolution dla operatorów porównań nie ma hash, więc nie ma znaczenia co zwraca. Ale hash jest używany przy słownikach i zbiorach. Oficjalna dokumentacja Pythona mówi o obiektach hashable i non-hashable. W praktyce wystarczy prześledzić method resolution opisaną tutaj.

SLOTS

Python domyślnie przechowuje atrybuty instancji w słowniku __dict__, co daje możliwość dodawania atrybutów w czasie pracy programu. Przy małych klasach ze znanymi atrybutami może to jednak prowadzić do marnowania pamięci RAM. Można wymusić alokowanie stałej ilości pamięci na atrybuty przy zastosowaniu __slots__ zamiast słownika. Pewne problemy pojawiają się przy korzystaniu z dziedziczenia.


# https://stackoverflow.com/questions/472000/usage-of-slots

class ClassWithoutSlots(object):
    def __init__(self, name, value):
        self.name = name
        self.value = value

class ClassWithSlots(object):
    __slots__ = ['name', 'value']
    def __init__(self, name, value):
        self.name = name
        self.value = value

FUNKCJA SUPER

PEP 367 -- New Super [Py2.6]

PEP 3135 -- New Super [Py3]

https://rhettinger.wordpress.com/2011/05/26/super-considered-super/

https://www.programiz.com/python-programming/methods/built-in/super

W Pythonie 2.2 pojawiła się funkcja wbudowana super(), która działa tylko z klasami w nowym stylu. Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class.


# super(type[, object-or-type])    # Py2.7
# Typowe wywołanie funkcji super(): przy pojedynczym dziedziczeniu można
# odwołać się do klasy nadrzędnej bez jawnego użycia jej nazwy.
# To będzie działać nawet po zmianie klasy nadrzędnej.

#class B(object):   # Py2.7, klasy w nowym stylu
class B:   # Py3
    def method(self, arg):
        print("B: {}".format(arg))

class C(B):
    def method(self, arg):
        super(C, self).method(arg)   # Py2.7, szuka powyżej C
        # super(self.__class__, self).method(arg)   # Py2.7, bez powtarzania C
        # super().method(arg)   # Py3

# Korzystanie z nowej wersji super() w Pythonie 2.5 wymaga (PEP 367):
from __future__ import new_super

TWORZENIE INSTANCJI KLASY W JEJ METODACH

Pewne metody klasy mogą tworzyć nowe instancje danej klasy. Powstaje pytanie jak to zrobić, aby w przyszłości ułatwić sobie zmiany w kodzie.


class Time:
    """Klasa reprezentująca odcinek czasu."""

    def __init__(self, s=0):
        """Zwraca instancję klasy Time."""
        self.s = int(s)

    def __add__(self, other):
        """Dodawanie odcinkow czasu."""
        #return Time(self.s + other.s)   # jawnie używamy nazwy "Time"
        #return type(self)(self.s + other.s)   # działa dla klas w nowym stylu
        return self.__class__(self.s + other.s)   # uniwersalne
        # self.__class__.__name__ to string "Time", np. dla __repr__