1.4. Good Engineering Practises
SOLID Principles
Ask don't tell principle
KISS - Keep it Simple Stupid
YAGNI - You ain't gonna need it
EFAP - Easier to ask for forgiveness than permission
1.4.1. Code Language
import this
- The Zen of Python, by Tim PetersReadability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
If the implementation is hard to explain, it's a bad idea.
In US: The states are not administrative divisions of the country, in that their powers and responsibilities are in no way assigned to them from above by federal legislation or the Constitution; rather they exercise all powers of government not delegated to the federal government by the Constitution.
Political divisions of the United States are the various recognized governing entities that together form the United States – states, the District of Columbia, territories and Indian reservations.
https://en.wikipedia.org/wiki/List_of_administrative_divisions_by_country
>>> class Obywatel:
... def get_wojewodztwo(self):
... pass
...
... def get_powiat(self):
... pass
...
... def get_gmina(self):
... pass
>>> class Citizen:
... def get_voivodeship(self):
... pass
...
... def get_state(self):
... pass
...
... def get_county(self):
... pass
...
... def get_ceremonial_county(self):
... pass
...
... def get_metropolitan_county(self):
... pass
...
... def get_nonmetropolitan_county(self):
... pass
...
... def get_district(self):
... pass
...
... def get_civil_parish(self):
... pass
...
>>> class Obywatel:
... def get_PESEL(self):
... pass
...
... def get_NIP(self):
... pass
>>> class Citizen:
... def get_SSN(self):
... ...
...
... def get_VATEU(self):
... pass
>>> class Obywatel:
... def get_NIP(self):
... pass
...
... def get_PESEL(self):
... pass
>>> class Citizen:
... def get_VATEU(self):
... pass
Stdnum https://github.com/arthurdejong/python-stdnum/tree/master/stdnum
1.4.2. Objects and instances
Creating string instance:
''
is just a syntactic sugar:
>>> name1 = 'Mark Watney'
>>> name2 = str('Mark Watney')
>>> name1 == name2
True
>>> name = 'Mark Watney'
>>> name.upper()
'MARK WATNEY'
>>> str.upper('Mark Watney')
'MARK WATNEY'
Use case:
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
... firstname: str
... lastname: str
...
... def say_hello(self):
... print(f'My name... {self.firstname} {self.lastname}')
>>>
>>>
>>> jose = Astronaut('Jose', 'Jimenez')
>>> jose.say_hello()
My name... Jose Jimenez
>>>
>>> Astronaut.say_hello()
Traceback (most recent call last):
TypeError: Astronaut.say_hello() missing 1 required positional argument: 'self'
>>>
>>> Astronaut.say_hello(jose)
My name... Jose Jimenez
1.4.3. Tell - don't ask
Tell-Don't-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data.
It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do.
This encourages to move behavior into an object to go with the data.
Bad:
>>> class Light:
... status = 'off'
>>>
>>>
>>> light = Light()
>>> light.status = 'on'
>>> light.status = 'off'
Good:
>>> class Light:
... status = 'off'
...
... def switch_on(self):
... self.status = 'on'
...
... def switch_off(self):
... self.status = 'off'
>>>
>>>
>>> light = Light()
>>> light.switch_on()
>>> light.switch_off()
Bad:
>>> class Hero:
... health: int = 10
>>>
>>>
>>> hero = Hero()
>>>
>>> while hero.health > 0:
... hero.health -= 2
Good:
>>> class Hero:
... health: int = 10
...
... def is_alive(self):
... return self.health > 0
...
... def take_damage(self, damage):
... self.health -= damage
>>>
>>>
>>> hero = Hero()
>>>
>>> while hero.is_alive():
... hero.take_damage(2)
1.4.4. Setters, Getters, Deleters
Java way: setters, getters, deleters
Python way: properties, reflection, descriptors
More information in Protocol Property
More information in Protocol Reflection
More information in Protocol Descriptor
In Python you prefer direct attribute access
Accessing class fields using setter and getter:
>>> class Astronaut:
... _name: str
...
... def set_name(self, name):
... self._name = name
...
... def get_name(self):
... return self._name
>>>
>>>
>>> mark = Astronaut()
>>> mark.set_name('Mark Watney')
>>> result = mark.get_name()
>>> print(result)
Mark Watney
Problem with setters and getters:
>>> class Point:
... _x: int
... _y: int
...
... def get_x(self):
... return self._x
...
... def set_x(self, value):
... self._x = value
...
... def del_x(self):
... del self._x
...
... def get_y(self):
... return self._y
...
... def set_y(self, value):
... self._x = value
...
... def del_y(self):
... del self._y
Rationale for Setters and Getters:
>>> class Temperature:
... kelvin: int
...
... def set_kelvin(self, kelvin):
... if kelvin < 0:
... raise ValueError('Kelvin cannot be negative')
... else:
... self._kelvin = kelvin
...
>>>
>>> t = Temperature()
>>> t.set_kelvin(-1)
Traceback (most recent call last):
ValueError: Kelvin cannot be negative
Rationale for Setters and Getters habitatOS Z-Wave sensor admin:
>>>
...
... 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 = ['mission_date', 'mission_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 = ['earth_datetime'] + list_display
... return list_display
1.4.5. Calling Methods in the Initializer
It is better when user can choose a moment when call
.connect()
method
Let user to call method:
>>> class Server:
... def __init__(self, host, username, password=None):
... self.host = host
... self.username = username
... self.password = password
... self.connect() # Better ask user to ``connect()`` explicitly
...
... def connect(self):
... print(f'Logging to {self.host} using: {self.username}:{self.password}')
>>>
>>>
>>> connection = Server(
... host='nasa.gov',
... username='mwatney',
... password='Ares3')
Logging to nasa.gov using: mwatney:Ares3
Let user to call method:
>>> class Server:
... def __init__(self, host, username, password=None):
... self.host = host
... self.username = username
... self.password = password
...
... def connect(self):
... print(f'Logging to {self.host} using: {self.username}:{self.password}')
>>>
>>>
>>> connection = Server(
... host='nasa.gov',
... username='mwatney',
... password='Ares3')
>>>
>>> connection.connect()
Logging to nasa.gov using: mwatney:Ares3
However it is better to use self.set_position(position_x, position_y)
than to set those values one by one and duplicate code. Imagine if there
will be a condition boundary checking (for example for negative values):
>>> class PositionBad:
... def __init__(self, position_x=0, position_y=0):
... self.position_x = position_x
... self.position_y = position_y
...
... def set_position(self, x, y):
... self.position_x = x
... self.position_y = y
>>>
>>>
>>> class PositionGood:
... def __init__(self, position_x=0, position_y=0):
... self.set_position(position_x, position_y)
...
... def set_position(self, x, y):
... self.position_x = x
... self.position_y = y
>>> class PositionBad:
... def __init__(self, position_x=0, position_y=0):
... self.position_x = min(1024, max(0, position_x))
... self.position_y = min(1024, max(0, position_y))
...
... def set_position(self, x, y):
... self.position_x = min(1024, max(0, x))
... self.position_y = min(1024, max(0, y))
>>>
>>>
>>> class PositionGood:
... def __init__(self, position_x=0, position_y=0):
... self.set_position(position_x, position_y)
...
... def set_position(self, x, y):
... self.position_x = min(1024, max(0, x))
... self.position_y = min(1024, max(0, y))