2. OOP Advanced

2.1. __str__() and __repr__()

  • __repr__ jest dla developerów (być jednoznacznym),
  • __str__ dla użytkowników (być czytelnym).
Code Listing 2.46. Using __repr__() on a class
class Astronaut:
    def __init__(self, name, agency='NASA'):
        self.name = name
        self.agency = agency

    def __str__(self):
        return f'My name... {self.name}'

    def __repr__(self):
        return f'Astronaut(name="{self.name}", agency="{self.agency}")'


jose = Astronaut(name='José Jiménez', agency='NASA')
print(jose)
# My name... José Jiménez

crew = [
    Astronaut(name='José Jimenéz', agency='NASA'),
    Astronaut(name='Matt Kowalski', agency='NASA'),
    Astronaut(name='Иван Иванович', agency='Roscosmos'),
    Astronaut(name='Alex Vogel', agency='ESA'),
]

print(crew)
# Astronaut(name='José Jiménez', agency='NASA')
# Astronaut(name='Matt Kowalski', agency='NASA')
# Astronaut(name='Ivan Иванович', agency='Roscosmos')
# Astronaut(name='Alex Vogel', agency='ESA')
import datetime

datetime.datetime.now()  # ``__repr__``
# datetime.datetime(2018, 7, 3, 11, 32, 51, 684972)

print(datetime.datetime.now())  # ``__str__``
# 2018-07-03 11:32:58.927387

2.2. What should be in the class and what not?

  • Jeżeli metoda w swoim ciele ma self i z niego korzysta to powinna być w klasie
  • Jeżeli metoda nie ma w swoim ciele self to nie powinna być w klasie
  • Jeżeli metoda nie ma w swoim ciele self ale wybitnie pasuje do klasy, to można ją tam zamieścić oraz dodać dekorator @staticmethod
Code Listing 2.47. Case Study uzasadnionego użcycia @staticmethod
def say_hello():
    print('Hello')


class Astronaut:
    name = 'José Jiménez'


jose = Astronaut()
say_hello()
# Hello
Code Listing 2.48. Case Study uzasadnionego użcycia @staticmethod
class Astronaut:
    name = 'José Jiménez'

    def say_hello(self):
        print('Hello')


jose = Astronaut()
jose.say_hello()
# Hello
Code Listing 2.49. Case Study uzasadnionego użcycia @staticmethod
class Astronaut:
    name = 'José Jiménez'

    @staticmethod
    def say_hello():
        print('Hello')


jose = Astronaut()
jose.say_hello()        # Hello

Astronaut.say_hello()   # Hello

2.2.1. @staticmethod

  • Using class as namespace
  • Will not pass instance as a first argument
  • self is not required
Code Listing 2.50. Using @staticmethod
def increment_population():
    Astronaut.population += 1

def decrement_population():
    Astronaut.population -= 1


class Astronaut:
    population = 0

    def __init__(self, name):
        self.name = name
        increment_population()

    def __del__(self):
        decrement_population()


jose = Astronaut('José Jiménez')
print(Astronaut.population)  # 1

ivan = Astronaut('Иван Иванович')
print(Astronaut.population)  # 2

# ----------------

class Astronaut:
    population = 0

    def __init__(self, name):
        self.name = name
        Astronaut.increment_population()

    def __del__(self):
        decrement_population()

    @staticmethod
    def increment_population():
        Astronaut.population += 1

    @staticmethod
    def decrement_population():
        Astronaut.population -= 1


jose = Astronaut('José Jiménez')
print(Astronaut.population)  # 1

ivan = Astronaut('Иван Иванович')
print(Astronaut.population)  # 2

del ivan
print(Astronaut.population)  # 1

2.3. Dynamically creating fields

Code Listing 2.51. Funkcja inicjalizująca, która automatycznie dodaje pola do naszej klasy w zależności od tego co zostanie podane przy tworzeniu obiektu
class Astronaut:
    def __init__(self, last_name, **kwargs):
        self.last_name = last_name

        for key, value in kwargs.items():
            setattr(self, key, value)


ivan = Astronaut(first_name='Иван', last_name='Иванович', agency='Roscosmos')
jose = Astronaut(last_name='Jimenez', addresses=())

print(ivan.first_name)  # Иван
print(jose.last_name)   # Jimenez

print(jose.__dict__)    # {'last_name': 'Jimenez', 'addresses': ()}
print(ivan.__dict__)    # {'last_name': 'Иванович', 'first_name': 'Иван', 'agency': 'Roscosmos'}

2.4. Setter and Getter

2.4.1. Accessing class fields

Code Listing 2.52. Accessing class fields
class Astronaut:
    name = ''

    def set_name(self, name):
        print('I can print some log messages')
        self.name = name

    def get_name(self):
        return self.name


# Java way
jose = Astronaut()
jose.set_name('José Jiménez')
print(jose.get_name())

# Python way
ivan = Astronaut()
ivan.name = 'Ivan Иванович'
print(ivan.name)
Code Listing 2.53. Case Study uzasadnionego użycia gettera w kodzie
from django.contrib import admin
from habitat._common.admin import HabitatAdmin
from habitat.sensors.models import ZWaveSensor


@admin.register(ZWaveSensor)
class ZWaveSensorAdmin(HabitatAdmin):
    change_list_template = 'sensors/change_list_charts.html'
    list_display = ['date', 'time', 'type', 'device', 'value', 'unit']
    list_filter = ['created', 'type', 'unit', 'device']
    search_fields = ['^date', 'device']
    ordering = ['-datetime']

    def get_list_display(self, request):
        list_display = self.list_display

        if request.user.is_superuser:
            list_display = ['datetime'] + list_display

        return list_display

2.4.2. @property, @x.setter, @x.deleter

  • @propery - for defining getters
  • @kola.setter - for defining setter
  • @kola.deleter - for defining deleter
  • Blokowanie możliwości edycji pola klasy
  • Dodawanie logowania przy ustawianiu wartości
Code Listing 2.54. @property, @x.setter, @x.deleter
class Celsius:
    def __init__(self, value=0):
        self._value = value

    @property
    def temperature(self):
        print('Getting value')
        return self._value

    @temperature.setter
    def temperature(self, value):
        if value < -273:
            raise ValueError('Temperature below -273 is not possible')
        print('Setting value')
        self._value = value


body = Celsius(36.6)

body.temperature
body.temperature = 34     # Setting value
body.temperature = -1000  # ValueError: Temperature below -273 is not possible

2.5. Hash

  • set() można zrobić z dowolnego hashowalnego obiektu
  • dict() może mieć klucze, które są dowolnym hashowalnym obiektem
Code Listing 2.55. dict() może mieć klucze, które są dowolnym hashowalnym obiektem
key = 'last_name'

my_dict = {
    'fist_name': 'José',
    key: 'Jiménez',
    1: 'id',
}
Code Listing 2.56. set() można zrobić z dowolnego hashowalnego obiektu
class Astronaut:
    def __init__(self, name):
        self.name = name


{1, 1, 2}
# {1, 2}

jose = Astronaut(name='Jose Jimenez')
data = {jose, jose}
len(data)
# 1

data = {Astronaut(name='Jose Jimenez'), Astronaut(name='Jose Jimenez')}
len(data)
# 2
Code Listing 2.57. Generating hash and object comparision
class Astronaut:
    def __init__(self, first_name, last_name, agency='NASA'):
        self.first_name = first_name
        self.last_name = last_name
        self.agency = agency

    def __hash__(self, *args, **kwargs):
        """
        __hash__ should return the same value for objects that are equal.
        It also shouldn't change over the lifetime of the object;
        generally you only implement it for immutable objects.
        """
        return hash(self.first_name) + hash(self.last_name) + hash(self.agency)

    def __eq__(self, other):
        if self.first_name == other.first_name and \
                self.last_name == other.last_name and \
                self.agency == other.agency:
            return True
        else:
            return False
Code Listing 2.58. Generating hash and object comparision
from collections import OrderedDict


class Astronaut:
    def __init__(self, first_name, last_name, agency='NASA'):
        self.first_name = first_name
        self.last_name = last_name
        self.agency = agency

    def __hash__(self):
        d = OrderedDict(self.__dict__)
        return hash(d)

    def __eq__(self, other):
        if self.__dict__ == other.__dict__:
            return True
        else:
            return False

2.6. is

  • is porównuje czy dwa obiekty są tożsame
  • Najczęściej służy do sprawdzania czy coś jest None
if name is None:
    print('Name is not set')
else:
    print('You have set your name')

Bardzo kuszący jest następujący przykład:

if name is 'Mark Watney':
   print('You are Space Pirate!')
else:
   print('You are not pirate at all!')

Nie jest on jednak do końca poprawny. Słowo kluczowe ``is`` porównuje czy dwa obiekty są tym samym obiektem, nie czy mają taką samą wartość. * Poniższy przykład ilustruje, że pomimo że dwa obiekty przechowują takiego samego stringa to nie są sobie tożsame, mimo że są sobie równe.

a = 'hello'
b = 'hello'

print(f'a is {a}, b is {b}')        # a is hello, b is hello
print(f'a == b returns: {a==b}')    # a == b returns: True
print(f'a is b returns: {a is b}')  # a is b returns: True


print(id(a))  # 4640833352
print(id(b))  # 4640833352
a = 'hello'
b = ''.join('hello')

print(f'a is {a}, b is {b}')        # a is hello, b is hello
print(f'a == b returns: {a==b}')    # a == b returns: True
print(f'a is b returns: {a is b}')  # a is b returns: False

print(id(a))  # 4640833352
print(id(b))  # 4662440600

2.7. Monkey Patching

Code Listing 2.59. Monkey Patching
class User:
    def hello(self):
        print('hello')


def monkey_patch():
    print('My function')


User.hello = monkey_patch
User.hello()
# 'My function'
Code Listing 2.60. Monkey Patching
import datetime
import json


def datetime_encoder(self, obj):
    if isinstance(obj, datetime.date):
        return f'{obj:%Y-%m-%d}'
    else:
        return str(obj)

json.JSONEncoder.default = datetime_encoder

data = {"datetime": datetime.date(1961, 4, 12)}
json.dumps(data)
# {"datetime": "1961-04-12"}
Code Listing 2.61. Monkey Patching
def hello():
    print('hello')


class Astronaut:
    def say_name(self):
        print(self.first_name)


jose = Astronaut()
jose.first_name = 'José Jiménez'
print(jose.first_name)
# 'José Jiménez'

jose.say_hello = hello
jose.say_hello()
# hello

jose.say_ehlo = lambda: print('ehlo')
jose.say_ehlo()
# ehlo

jose.say_name()
# José Jiménez

Astronaut.say_name = lambda self: print(f'My name is... {self.first_name}')
jose.say_name()
# My name is... José Jiménez

2.8. Method Resolution Order

../_images/inherintace-diamond.jpg

Fig. 2.14. Inherintace Diamond

Code Listing 2.62. Method Resolution Order
from pprint import pprint


class A:
    def wyswietl(self):
        print('a')


class B:
    def wyswietl(self):
        print('b')


class C:
    def wyswietl(self):
        print('c')


class D(A, B, C):
    pass


d = D().wyswietl()  # a

pprint(D.__mro__)
# (<class '__main__.D'>,
#  <class '__main__.A'>,
#  <class '__main__.B'>,
#  <class '__main__.C'>,
#  <class 'object'>)

2.9. Objects and instances

Code Listing 2.63. Objects and instances
text = 'Ehlo,world'
text.split(',')  # ['Ehlo', 'world']
str.split(text, ',')  # ['Ehlo', 'world']
str.split('Ehlo,world', ',')  # ['Ehlo', 'world']

2.10. Metaclass

  • Można zmienić, że obiekt nie dziedziczy po object
  • Każdy obiekt klasy jest instancją tej klasy
  • Każda napisana klasa jest instancją obiektu, który nazywa się metaklasą
  • Na 99% tego nie potrzebujesz

Warning

więcej na ten temat w rozdziale Metaclass

Code Listing 2.64. Metaclass
class FooClass:
    pass


f = FooClass()
isinstance(f, FooClass)
isinstance(f, object)

2.11. Assignments

2.11.1. Dragon (Part 3)

../_images/dragon.gif

Fig. 2.15. Firkraag dragon from game Baldur’s Gate II: Shadows of Amn

  1. Dodaj możliwość poruszania się smoka i bohatera w 3 wymiarach

  2. Bohater może należeć do drużyny, który może składać się maks z 6 postaci (różnych klas)

  3. Żadna z istot na planszy nie może wyjść poza zakres ekranu

  4. Bohater może dodatkowo założyć ekwipunek i może być to wiele obiektów na raz

  5. Każdy z przedmiotów ma swoją nazwę, typ oraz modyfikator

    • zbroję (dodatkowe punkty obrony, np. +10%)
    • tarczę (dodatkowe punkty obrony, np. +5%)
    • miecz (dodatkowe punkty ataku, np. +5%)
  6. Zbroja i tarcza chroni przed uderzeniami obniżając ilość obrażeń o wartość obrony

  7. Miecz zwiększa ilość zadawanych obrażeń

  8. Obrażenia smoka maleją z sześcianem odległości (zianie ogniem)

  9. Bohater nie może zadawać obrażeń jak jest dalej niż 50 punktów od przeciwnika

  10. Wszystkie istoty mogą lewelować a bazowe punty życia i obrażeń się zmieniają z poziomem

  11. Przeprowadź symulację walki. Kto zginie pierwszy?

About:
  • Filename: oop_dragon_3.py
  • Lines of code to write: 50 lines
  • Estimated time of completion: 30 min