Tricki w Pythonie, bez których nie mogę żyć
![Tricki w Pythonie, bez których nie mogę żyć](https://cdn.bulldogjob.com/system/readables/covers/000/003/277/max_res/281022_6_przydatnych_sztuczek_w_pythonie.png)
W Internecie znaleźć można mnóstwo podobnych artykułów, ale pomyślałem, że przedstawię również moją perspektywę tematu i uzupełnię nieco listę “tricków”. Fragmenty kodu przedstawione w tym artykule są w pewien sposób kluczowe dla organizacji mojej pracy i bardzo często z nich korzystam.
Zbiory
Programiści często zapominają, że Python posiada zbiory i używają do wszystkiego list. Czym jest zbiór? W skrócie:
Zbiór jest kolekcją nieuporządkowaną bez możliwości pojawienia się powtarzających się elementów.
Jeśli zapoznasz się ze zbiorami i ich logiką, pomoże ci to w rozwiązaniu wielu problemów. Przykładowo — jak otrzymać wszystkie unikalne litery użyte w danym słowie?
myword = "NanananaBatman"
set(myword)
{'N', 'm', 'n', 'B', 'a', 't'}
Bang. Problem rozwiązany, ale prawdę mówiąc, wziąłem to z oficjalnej dokumentacji Pythona, więc nie ma tu nic zaskakującego.
A co myślisz o tym? Da się uzyskać listę elementów bez powtórzeń elementów?
# first you can easily change set to list and other way around
mylist = ["a", "b", "c", "c"]
# let's make a set out of it
myset = set(mylist)
# myset will be:
{'a', 'b', 'c'}
# and, it's already iterable so you can do:
for element in myset:
print(element)
# but you can also convert it to list again:
mynewlist = list(myset)
# and mynewlist will be:
['a', 'b', 'c']
Jak pewnie zauważyłeś, powtarzające się “c” nie jest już problemem. Jedyną rzeczą, o której należy pamiętać, jest to, że kolejność elementów w mylist oraz w mynewlist może być inna:
mylist = ["c", "c", "a", "b"]
mynewlist = list(set(mylist))
# mynewlist is:
['a', 'b', 'c']
# as you can see it's different order;
Ale! Zejdźmy nieco głębiej.
Wyobraźmy sobie przypadek, w którym mamy relację jeden do wielu pomiędzy pewnymi podmiotami, mówiąc konkretniej — użytkownik i uprawnienia; zazwyczaj jest tak, że jeden użytkownik może mieć wiele uprawnień. Teraz wyobraźmy sobie, że ktoś chce zmodyfikować te uprawnienia — poprzez dodanie niektórych i usunięcie niektórych w tym samym czasie. Jak poradzić sobie z takim problemem?
# this is the set of permissions before change;
original_permission_set = {"is_admin", "can_post_entry", "can_edit_entry", "can_view_settings"}
# this is new set of permissions;
new_permission_set = {"can_edit_settings", "is_member", "can_view_entry", "can_edit_entry"}
# now permissions to add will be:
new_permission_set.difference(original_permission_set)
# which will result:
{'can_edit_settings', 'can_view_entry', 'is_member'}
# As you can see can_edit_entry is in both sets; so we do not need
# to worry about handling it
# now permissions to remove will be:
original_permission_set.difference(new_permission_set)
# which will result:
{'is_admin', 'can_view_settings', 'can_post_entry'}
# and basically it's also true; we switched admin to member, and add
# more permission on settings; and removed the post_entry permission
Mówiąc krótko — nie bój się zbiorów, ponieważ dzięki nim zaoszczędzisz dużo czasu. Więcej na ich temat znajdziesz w oficjalnej dokumentacji Pythona.
Zabawa z kalendarzem
Bardzo często kiedy tworzysz coś, co w dużym stopniu zależy od daty i czasu — to interesują cię np. pewne informacje, np. jaki jest ostatni dzień miesiąca z uwzględnieniem przestępności. Może się to wydawać proste, ale uwierzcie mi, że poprawna obsługa daty i czasu to niezwykle trudny temat, a ja uważam, że implementacja kalendarza ma bardzo słabą obsługę i może stać się koszmarem przez bardzo dużą liczbę przypadków brzegowych.
Jak więc znaleźć ostatni dzień miesiąca?
import calendar
calendar.monthrange(2020, 12)
# will result:
(1, 31)
# BUT! you need to be careful here, why? Let's read the documentation:
help(calendar.monthrange)
# Help on function monthrange in module calendar:
# monthrange(year, month)
# Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
# year, month.
# As you can see the first value returned in tuple is a weekday,
# not the number of the first day for a given month; let's try
# to get the same for 2021
calendar.monthrange(2021, 12)
(2, 31)
# So this basically means that the first day of December 2021 is Wed
# and the last day of December 2021 is 31 (which is obvious, cause
# December always has 31 days)
# let's play with February
calendar.monthrange(2021, 2)
(0, 28)
calendar.monthrange(2022, 2)
(1, 28)
calendar.monthrange(2023, 2)
(2, 28)
calendar.monthrange(2024, 2)
(3, 29)
calendar.monthrange(2025, 2)
(5, 28)
# as you can see it handled nicely the leap year;
Jeśli chodzi o początek miesiąca, to sprawa jest całkiem prosta — zawsze zaczyna się od pierwszego ?
A jak można wykorzystać informację, że miesiąc zaczyna się w dany dzień tygodnia? Możesz w łatwy sposób określić dzień tygodnia dla dowolnego dnia:
calendar.monthrange(2024, 2)
(3, 29)
# means that February 2024 starts on Thursday
# let's define simple helper:
weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
# now we can do something like:
weekdays[3]
# will result in:
'Thursday'
# now simple math to tell what day is 15th of February 2020:
offset = 3 # it's the first value from monthrange
for day in range(1, 29):
print(day, weekdays[(day + offset - 1) % 7])
1 Thursday
2 Friday
3 Saturday
4 Sunday
...
18 Sunday
19 Monday
20 Tuesday
21 Wednesday
22 Thursday
23 Friday
24 Saturday
...
28 Wednesday
29 Thursday
# which basically makes sense;
Nie jest to może przykład gotowy na “produkcję”, ponieważ dzień tygodnia można łatwo znaleźć za pomocą modułu datetime:
from datetime import datetime
mydate = datetime(2024, 2, 15)
datetime.weekday(mydate)
# will result:
3
# or:
datetime.strftime(mydate, "%A")
'Thursday'
W każdym razie jest trochę zabawy w module kalendarza, dobrze wiedzieć:
# checking if year is leap:
calendar.isleap(2021) # False
calendar.isleap(2024) # True
# or checking how many days will be leap days for given year span:
calendar.leapdays(2021, 2026) # 1
calendar.leapdays(2020, 2026) # 2
# read the help here, as range is: [y1, y2), meaning that second
# year is not included;
calendar.leapdays(2020, 2024) # 1
Enumerate ma drugi argument
Tak, ma drugi argument. Zabawne, ale niektórzy naprawdę doświadczeni programiści nie są tego świadomi; Koniecznie sprawdź przykłady:
mylist = ['a', 'b', 'd', 'c', 'g', 'e']
for i, item in enumerate(mylist):
print(i, item)
# Will give:
0 a
1 b
2 d
3 c
4 g
5 e
# but, you can add a start for enumeration:
for i, item in enumerate(mylist, 16):
print(i, item)
# and now you will get:
16 a
17 b
18 d
19 c
20 g
21 e
Jest to prosty punkt startowy, od którego powinna zacząć się enumeracja. Może to pomóc w sytuacjach, w których pracujesz nad jakimś przesunięciem logicznym.
Obsługa logiki if-else
Często zdarza się, że trzeba obsłużyć wiele różnych logik w zależności od warunku. Niedoświadczony programista skończy z czymś takim:
OPEN = 1
IN_PROGRESS = 2
CLOSED = 3
def handle_open_status():
print('Handling open status')
def handle_in_progress_status():
print('Handling in progress status')
def handle_closed_status():
print('Handling closed status')
def handle_status_change(status):
if status == OPEN:
handle_open_status()
elif status == IN_PROGRESS:
handle_in_progress_status()
elif status == CLOSED:
handle_closed_status()
handle_status_change(1) # Handling open status
handle_status_change(2) # Handling in progress status
handle_status_change(3) # Handling closed status
Nie wygląda to tragicznie, ale widziałem bazy danych z 20 lub większą liczbą warunków, które wyglądały podobnie.
Jak więc to zrobić?
from enum import IntEnum
class StatusE(IntEnum):
OPEN = 1
IN_PROGRESS = 2
CLOSED = 3
def handle_open_status():
print('Handling open status')
def handle_in_progress_status():
print('Handling in progress status')
def handle_closed_status():
print('Handling closed status')
handlers = {
StatusE.OPEN.value: handle_open_status,
StatusE.IN_PROGRESS.value: handle_in_progress_status,
StatusE.CLOSED.value: handle_closed_status
}
def handle_status_change(status):
if status not in handlers:
raise Exception(f'No handler found for status: {status}')
handler = handlers[status]
handler()
handle_status_change(StatusE.OPEN.value) # Handling open status
handle_status_change(StatusE.IN_PROGRESS.value) # Handling in progress status
handle_status_change(StatusE.CLOSED.value) # Handling closed status
handle_status_change(4) # Will raise the exception
Jest to powszechny wzór, który może być używany w Pythonie, a mówiąc najprościej — sprawia, że kod wygląda nieco czyściej — szczególnie tam, gdzie Twoja główna metoda obsługi jest ogromna i ma do obsługi wiele warunków.
Moduł Enum
W powyższym akapicie zarysowałem nieco temat, zagłębmy się w niego nieco bardziej.
Moduł enum udostępnia narzędzia do obsługi wyliczeń, z których najciekawsze to: Enum, IntEnum — sprawdźmy więcej:
from enum import Enum, IntEnum, Flag, IntFlag
class MyEnum(Enum):
FIRST = "first"
SECOND = "second"
THIRD = "third"
class MyIntEnum(IntEnum):
ONE = 1
TWO = 2
THREE = 3
# Now we can do things like:
MyEnum.FIRST # <MyEnum.FIRST: 'first'>
# it has value and name attributes, which are handy:
MyEnum.FIRST.value # 'first'
MyEnum.FIRST.name # 'FIRST'
# additionally we can do things like:
MyEnum('first') # <MyEnum.FIRST: 'first'>, get enum by value
MyEnum['FIRST'] # <MyEnum.FIRST: 'first'>, get enum by name
Z IntEnum
jest podobnie — ale są pewne różnice:
MyEnum.FIRST == "first" # False
# but
MyIntEnum.ONE == 1 # True
# to make first example to work:
MyEnum.FIRST.value == "first" # True
W średniej wielkości bazach kodu moduł enum
jest niezwykle pomocny w zarządzaniu stałymi w projekcie.
Lokalizacja enum może być trochę skomplikowana, ale wszystko jest do zrobienia, dlatego pozwól, że pokażę Ci szybko, jak sobie z tym radzę w django
:
from enum import Enum
from django.utils.translation import gettext_lazy as _
class MyEnum(Enum):
FIRST = "first"
SECOND = "second"
THIRD = "third"
@classmethod
def choices(cls):
return [
(cls.FIRST.value, _('first')),
(cls.SECOND.value, _('second')),
(cls.THIRD.value, _('third'))
]
# And later in eg. model definiton:
some_field = models.CharField(max_length=10, choices=MyEnum.choices())
IPython biegnie po zwycięstwo!
ipython oznacza interaktywnego Pythona i jest to powłoka poleceń dla interaktywnych obliczeń. Coś, jak interpreter Pythona, ale z bateriami w komplecie lub inaczej mówiąc: na sterydach.
Aby skorzystać z ipythona
, musisz go zainstalować:
pip install ipython
Zamiast typowego użycia python
, aby przejść do interpretera, użyj ipython
# you should see something like this after you start:
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.18.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
Obsługuje polecenia systemowe, takie jak: ls
czy cat
. Klawisz tab
wyświetli podpowiedzi, co znacznie uprzyjemni sesję z interaktywnym programowaniem. Możesz również użyć strzałek góra/dół, aby wyszukać ostatnie polecenia. Ogólnie mówiąc, jesteś w stanie zrobić bardzo dużo — sprawdź oficjalną dokumentację ipythona. Szczególnie polecam zapoznanie się z magicznymi funkcjami, za ich pomocą możesz zapisywać, udostępniać użyty kod, mierzyć czas wykonania i wiele więcej.
I w sumie to tyle. Mam nadzieję, że znalazłeś tutaj coś ciekawego.
Oryginał tekstu w języku angielskim przeczytasz tutaj.