Menu Zamknij
23 kwietnia 2021

#1 Przygotowanie do Interview

Słowem wstępu...

Tym artykułem rozpoczynamy serię przygotowań do rozmów kwalifikacyjnych na Python Dev-a. Będziemy w niej rozważali i wspólnie rozwiązywali zadania pojawiące się na niektórych rozmowach kwalifikacyjnych. A że wraz z naszymi uczniami mamy z nimi styczność w niemal każdym tygodniu pracy, to zbiór pomysłów na artykuły jest naprawdę obszerny i chcemy dobrze zrealizować ten pomysł. Tak, abyś czuł, Czytelniku, że taki bullet wiedzy może również sporo wnieść w Twój rozwój.

Czym jest __getattribute__() i __getattr__()?

Otóż na jednej z ostatnich rozmów kwalifikacyjnych padło następujące zadanie:

Określ, jaki będzie wynik poniższego kodu.

import datetime

class Human(object):
    name = None
    gender = None
    birthdate = None

    def __getattr__(self, name):
        if name == 'age':
            return datetime.datetime.now() - self.birthdate
        else:
            return None

    def __getattribute__(self, name):
        return object.__getattribute__(self, name)

h = Human()
h.birthdate = datetime.datetime(1984, 8, 20)
h.age = 28
print(h.age)

Aby móc prawidłowo rozwiązać to zadanie, musimy przede wszystkim wiedzieć, za co odpowiedzialne są metody specjalne __getattribute__() oraz __getattr__(). Nie ukrywam, że w trakcie Naszych szkoleń, nie omawiamy za często różnic i case-ów stosowania wspomnianych metod. A z racji, iż różnica między nimi jest dość tricky, to warto zagłębić się w ten temat i rozszerzyć swoją wiedzę z bardziej niecodziennej teorii z Pythona. Czytaj zatem dalej!

Odwołanie do nieistniejącego pola w klasie?

Zdefiniujmy klasę w poniższy sposób:

class ClassUnderTest: 
    def __init__(self, field1, field2): 
        self.field1 = field1 
        self.field2 = field2 

c = ClassUnderTest('Devs', 'Mentoring') 
print(c.field1) # 'Devs' 
print(c.field2) # 'Mentoring' 
print(c.field3) # AttributeError: 'ClassUnderTest' object has no attribute 'field3'

W przykładzie tym zdefiniowaliśmy 2 pola dla klasy ClassUnderTest: field1, field2. Następnie po jej zainstancjonowaniu, wyświetliliśmy wymienione atrybuty wraz z odwołaniem się do niestniejącego pola - field3. Nie trzeba być wyjadaczem Pythona, aby przewidzieć, jak zakończy się taka próba dostępu. Skutkuje on rzucenie wyjątku AttributeError i jest to naturalna kolej rzeczy.

Możemy jednak temu zaradzić!

Chcąc obsługiwać sytuację, w której użytkownik próbować będzie odwołać się do niestniejącego pola, wystarczy dodać w ClassUnderTest metodę __getattr__() i odpowiednio zdefiniować jej zachowanie. Naszym celem będzie utworzenie takiej metody dostępowej, że przestanie już być rzucany wyjątek AttributeError, a zamiast tego program wyświetli napis: "This attribute's not been defined". Przekładając więc to wszystko na kod, otrzymamy:


class ClassUnderTest: 
    def __init__(self, field1, field2): 
        self.field1 = field1 
        self.field2 = field2 

    def __getattr__(self, item): 
        return "This attribute's not been defined" 

c = ClassUnderTest('Devs', 'Mentoring') 
print(c.field1) # 'Devs' 
print(c.field2) # 'Mentoring' 
print(c.field3) # "This attribute's not been defined"

Jak widzisz, teraz każda próba pobrania wartości z nieistniejącego pola, będzie kończyła się automatycznym wywołaniem metody __getattr__() i wyświetleniem zwracanego napisu.

Wiem, wiem. Stawiasz sobie zapewne teraz pytanie, do czego to nam się może przydać. Otóż, jak się okazuje, metoda __getattr__, może umożliwić Nam realizację bardzo ciekawych funkcjonalności w kodzie.

Spójrz na przykład:

class UrlGenerator:
    def __init__(self, url):
        self.url = url

   def __getattr__(self, item):
       if item == 'get' or item == 'post':
           print(self.url)
       else:
           return UrlGenerator(f'{self.url}/{item}')

url_gen = UrlGenerator('https://xxxx')
url_gen.users.show.get # https://xxxx/users/show

Powyższa klasa umożliwia łatwe budowanie (generowanie) adresów URL. Zauważ, że odwołanie się za każdym razem do kolejnego nieistniejącego pola, powoduje dodanie nowej podstrony do adresu https://xxxx/. Niesamowicie wygodne, co? Metoda specjalna __getattr__() naprawdę potrafi czasami usprawniać i optymalizować pisany kod.

A co z __getattribute__()?

Przyznaję się, że odbiegłem nieco od pytania rekrutacyjnego zadanego we wstepie tego posta. Dlatego, wracając już na ziemię - aby móc na nie poprawnie odpowiedzieć, musimy dodatkowo poznać, czym jest i za co odpowiada, metoda __getattribute__().

Najprościej ujmując, __getattribute__() jest niejawnie wywoływany przy dostępie do każdego pola. Niezależnie, czy istnieje ono w klasie, czy też nie. Tak więc __getattribute__ może być dobrym sposobem na zabronienie dostępu do określonych pól, np. tych, które rozpoczynają się prefixem 'devs'.

class ClassUnderTest:
    def __init__(self, devsfield1, field1):
        self.devsfield1 = devsfield1
        self.field1 = field1

    def __getattribute__(self, item):
        if item.startswith('devs'):
            raise AttributeError
        return super().__getattribute__(item) # linia, aby zapobiec rekursji

c = ClassUnderTest(1, 3)
print(c.field1) # 3
print(c.devsfield1) # AttributeError

W zasadzie wyżej zdobyta wiedza powinna umożliwić Nam dojście do poprawnej odpowiedzi na postawione pytanie rekrutacyjne.

Tak więc wróć śmiało do początku i rozważ problem ponownie. Jak już powyrywasz wystarczająco dużo włosów z głowy, poszukując poprawnego rozwiązania zadania, zapoznaj się z prawidłową odpowiedzią:

A to dlatego, iż w zadaniu, zapis:

spowoduje wywołanie metody:


def __getattribute__(self, name):
return object.__getattribute__(self, name)

Nie byłbym jednak sobą, gdybym artykuł zakończył już w tym miejscu. Spróbujmy jeszcze bardziej wyczerpać temat różnic między metodami __getattr__() oraz __getattribute__(), rozważając różne skrajne przypadku zastosowania.

__getattr__() oraz __getattribute__ równocześnie w jednej klasie?

Wiemy, iż metoda __getattr__() jest niejawnie wywoływana, gdy próbujemy odczytać z obiektu nieistniejące pole, a __getattribute__ za każdym razem, gdy odczytujemy jakiekolwiek pole. Pytanie teraz, co w momencie, gdy zdecujemy się na implementację obu tych metod w tej samej klasie? Jaka będzie wówczas kolejność wywoływania omawianych metod przy próbie odczytu nieistniejącego pola?

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

    def __getattribute__(self, item):
        super().__getattribute__(item)
        return '__getattribute__ ', item

    def __getattr__(self, item):
        return '__getattr__ ', item

c = ClassUnderTest('devs')
print(c.name) # ('__getattribute__ ', 'name')
print(c.foo) # ('__getattr__ ', 'foo')

Zapoznanie się z działaniem powyższego kodu powinno odpowiedzieć na Nasze dywagacje. Otóż tutaj również bez większych zmian - przy odwołaniu się do nieistniejącego pola, wywoływana wywoływana jest metoda __getattr__(), a w przypadku istniejącego atrybutu - __getattribute__(). Sytuacja jednak robi się bardziej ciekawa, gdy w trakcie dostępu pojawia się wyjątek AttributeError. Również dokumentacja wspomina o tym dość enigmatycznym przypadku:

Called unconditionally to implement attribute accesses for instances of the class. If the class also defines __getattr__(), the latter will not be called unless __getattribute__() either calls it explicitly or raises an AttributeError.

Musimy zatem wspólnie go rozważyć.

Edge case z exception-em.

Na zakończenie, pochylmy się również nad poniższym kodem:

class ClassUnderTest: 
    def __init__(self, devsfield1, field1): 
        self.devsfield1 = devsfield1 
        self.field1 = field1 
    
    def __getattribute__(self, item): 
        if item.startswith('devs'): 
            raise AttributeError 
        return super().__getattribute__(item) 

    def __getattr__(self, item): 
        return 'got' 

c = ClassUnderTest(1, 3) 
print(c.field1) # 3 
print(c.devsfield1) # 'got'

Zwróć uwagę na rzucany wyjątek w metodzie __getattribute__(). Powinien on zostać rzucony w momencie odwołania się do pola c.devsfield1. Po analizie programu, okazuje się jednak, że wywołanie print(c.devsfield1) nie kończy się wyjątkiem. Zwrócona zostaje natomiast wartość 0 z metody __getattr__(). Okazuje się bowiem, że __getattr__() jest również niejawnie wywoływany w momencie, gdy jego rodzima metoda __getattribute__() zwróci wyjątek AttributeError. Stąd wydruk print(c.devsfield1) kończy się wyświetleniem napisu 'got', a nie terminacją programu.

Pierwsze koty za płoty...

Cieszę się, że dotrwałeś do końca tego krótkiego, ale treściwego posta. Zadania na interview mają to do siebie, że nie muszą poruszać nie wiadomo jak skomplikowanych zagadnień. Mają one sprawdzić tok rozumowania kandydata oraz jaka jest jego strategia na analizowanie, nie zawsze super jasnego na pierwszy rzut oka, kodu. W następnych artykułach siadamy do kolejnych zagadnień związanych z ciekawy aspektami Pythona...

Wszystkiego optymalnego!

Sprawdź również nasz system mentorowania i outsourcowania specjalistów
  • Wyznaczona ścieżka od A do Z w kierunku przebranżowienia
  • Indywidualnie dostosowany materiał pod ucznia
  • Code Review ze strony mentora
  • Budowa portfolio
  • Intensywna praca z materiałami szkoleniowymi