Curing Python’s Neglect – polemika

Wczoraj Zed Shaw na swoim blogu napisał artykuł Curing Python’s Neglect. Jak można się domyślić z tytułu ma on na celu pokazanie niedbałości w języku. Z niektórymi uwagami muszę się zgodzić, lecz spora ich część jest nieprawdziwa lub nieaktualna.

Operacje na listach

Pierwszy z przykładów omawia operowanie na listach:


mystuff.append(mything)
mystuff.remove(mything)
# to ponoć jest nielogiczne
del mystuff[4]

Absolutnie nie mogę się zgodzić z tym zarzutem. Metody append(), remove(), index() jako parametr dostają wartość, która ma być (odpowiednio) dołączona, usunięta lub znaleziona w liście.

Natomiast jeśli mamy zamiar korzystać z indeksu elementu w liście należy użyć nawiasów kwadratowych i to nie zależnie czy chcemy pobrać, ustawić czy usunąć element o danym numerze.


foo = ['ala', 'ma']
# wszystkie poniższe metody mają parametr będący wartością
foo.append('kota')
foo.remove('ala')
foo.index('ma')
foo.extend(('i', 'kota'))
foo.count('kota')

# natomiast tak natomiast pracuje się z indeksami
a = foo[3]
foo[3] = 'psa'
del foo[1]

Obsługa argumentów linii poleceń

Podobnie nie rozumiem narzekań w sprawie modułu optparse. Zasadniczo Python udostępnia dwie standardowe metody obsłużenia argumentów.

Pierwszym z nich jest moduł getopt, który jest wzorowany na Unixowej funkcji getopt(), jest też dostępny w Bashu czy Perlu. Dzięki temu programiści znający te języki mają o wiele łatwiej przenieść się do Pythona. Dodatkowo jest on bardzo prosty w użyciu.

Istnieje też moduł optparse optparse, udostępniający większe możliwości kosztem większej złożoności.

Obsługa daty

Rzeczywiście, obsługa dat nie należy do najprzyjemniejszych rzeczy jakie może mieć programista do zrobienia. Lecz nie widzę aby cokolwiek miało czynić ją trudniejszą w Pythonie – wręcz przeciwnie, biblioteka standardowa bardzo ułatwia sprawę. Moduł odpowiadający za nią nazywa się datetime

Autor narzekał brak możliwości parsowania daty dziewięć la temu. Teraz jest to trywialnie proste, dla przykładu:


from datetime import datetime

a = datetime.strptime('2009-05-31', '%Y-%m-%d')
print a   # 2009-05-31 00:00:00

b = datetime.strptime('05/31/2009 22:08:23', '%m/%d/%Y %H:%M:%S')
print b   # 2009-05-31 22:08:23

Podsumowanie

Nie ma idealnego języka programowania więc i w Pythonie znajdą się niedociągnięcia i błędy projektowe. Lecz niestery Zed Shaw powołuje się na język taki jakim był on w roku 2000, a to cała epoka do tyłu. Popełnia też standardowy błąd człowieka dopiero poznającego dany język, czyli: ja bym to zrobił lepiej, na przykład tak jak jest w moim ulubionym języku.

Co do sugestii o tym, że część języka i biblotek wymaga poważnych zmian – zostały one już dawno zauważone. Efektem tego jest powstanie wersji 3.0 języka, która nie jest zgodna wstecznie, ale za to naprawia wiele błędów projektowych.

Python DB API – Korzystanie z relacyjnych baz danych w Pythonie



Wprowadzenie

Python posiada – jak każdy porządny język programowania – zunifikowany interfejs dostępu do relacyjnych baz danych. Koncepcyjnie jest on zbliżony do JDBC (Java DataBase Connectivity), lecz dzięki wykorzystaniu możliwości Pythona o wiele prostrzy.

W chwili obecnej obowiązuje Python Database API v2.0 (znane też jako PEP 249). Jest ona rozwinięciem Python Database API v1.0 (PEP 248).

Prosty przykład

Oto prosty przykład kodu wyświetlającego całą zawartość tabeli test. Korzystam z nim z relacyjnej bazy danych SQLite. Jej obsługa jest standardowo dostępna w Pythonie począwszy od wersji 2.5.


import sqlite3
db = sqlite3.connect('dbapi.db')
cur = db.cursor()

cur.execute('SELECT * FROM `test`')
print cur.rowcount
for i in cur.fetchall():
    print  i

db.close()

W pierwszej linii importujemy moduł obsługi wybranej przez nas bazy danych. Warto zwrócić uwagę, że niektóre bazy danych mają więcej niż jeden dostępny moduł (przykładowo PostgreSQL ma psycopg oraz psycopg2). Moduły dla większości baz trzeba samemu zainstalować.

W drugiej linii nawiązujemy połączenie z bazą. Paramatry jakie przekazujemy funkcji connect zależą od wybranej bazy danych. W przypadku SQLite jest to tylko nazwa pliku w którym przechowywane są dane. Innym bazą trzeba przekazać często o wiele więcej informacji (zwykle: nazwę bazy danych, adres maszyny na której się ona znajduje, nazwę użytkownika i jego hasło).

W trzeciej linii tworzymy kursor. Będziemy za jego pomocą wysyłali zapytania do bazy danych i odbierali z niej ich wyniki.

W piątej linii wysyłamy do bazy zapytanie. Warto zwrócić uwagę, że treść zapytania (tzn 'SELECT * FROM `test`') nie jest ujednolicana i zależy od bazy z jakiej korzystamy.

W szóstej linii wyświetlamy ile wierszy wyniku uzyskaliśmy.

W siódmej i ósmej linii pobieramy wszystkie wiersze wyniku do tablicy (robi to cur.fetchall()), a następnie wyświetlamy każdy wiersz w nowej linii.

W dziesiątej linii zamykamy połączenie z bazą danych.

Skrócony opis API

Interface modułu

connect(parametry...)
Łączy się z bazą danych wskazana poprzez parametry. Zwraca obiekt połączenia.
paramstyle

Określa sposób w jaki w zapytaniach wskazuje się miejsce na parametry.

Nie należy tworzyć zapytania przez konkatenację fragmentów SQL i parametrów. Tak stworzony kod jest podatny na ataki SQL injection.

'qmark'

cur.execute('SELECT * FROM `test` WHERE ' +
    'name = ? AND salary > ?',
    ('Adam', 1000) )
'numeric'

cur.execute('SELECT * FROM `test` WHERE ' +
    'name = :1 AND salary > :2',
    ('Adam', 1000) )
'named'

cur.execute('SELECT * FROM `test` WHERE ' +
    'name = :nm AND salary > :sal',
    {'nm': 'Adam', 'sal': 1000} )
'format'

cur.execute('SELECT * FROM `test` WHERE ' +
    'name = %s AND salary > %d',
    ('Adam', 1000) )
'pyformat'

cur.execute('SELECT * FROM `test` WHERE ' +
    'name = %(nm)s AND salary > %(sal)d',
    {'nm': 'Adam', 'sal': 1000} )

Wyjątki

Poniżej opisana jest hierarchia wyjątków.

Warning
Wyrzucany gdy nastąpi ważne ostrzeżenie.
Error
Klasa bazowa dla pozostałych błędów.

InterfaceError
Wyrzucany gdy błąd nastąpił w interfejsie bazy danych, a nie w samej bazie danych.
DatabaseError
Wyrzucany gdy błąd nastapił wewnątrz bazy danych.

DataError
Wyrzucany gdy pojawią się niepoprawne dane, przykładowo liczby z poza obsługiwanego zakresu, napisy dłuższe niż pole na nie przeznaczone.
OperationalError
Wyrzucany gdy pojawią się błędy w trakcie przetwarzania transakcji, nastąpi nagłe rozłączenie. W praktyce bywa wyrzucany również w przypadku gdzie powinien być zastosowany ProgrammingError.
IntegrityError
Wyrzucany gdy zachodzi próba przekroczenia więzów integralności, typu ustawienie już istniejącej wartości w kolumnie, której wartości powinny być unikalne lub ustawienie nieprawidłowego klucza obcego.
InternalError
Wywoływany gdy w bazie wystąpi wewnętrzny problem.
ProgrammingError
Wyrzucany gdy pojawią się odwołania do nieistniejących tabel, próby stworzenia już istniejących, itp.
NotSupportedError
Wyrzucany gdy nastąpi próba skorzystania z metody nie obsługiwanej przez bazę danych (przykładowo rollback() w bazie bez wsparcia transakcji).

Obiekt połączenia (connection)

close()
Zamyka połączenie z bazą danych. Automatycznie zamyka również wszystkie kursory z niego korzystające. Zamknięcie połączenia bez uprzedniego zatwierdzenia transakcji spowoduje jej wycofanie.
commit()
Zatwierdza transakcje. Metoda istnieje tylko w bazach obsługujących transakcje.
rollback()
Wycofuje transakcje. Metoda istnieje tylko w bazach obsługujących transakcje.
cursor()
Zwraca nowy kursor powiązany z tym połączeniem. W razie potrzeby może być emulowane.

Obiekt kursora (cursor)

Informacje o wyniku

description
Lista opisów poszczególnych kolumn odpowiedzi. Opis pojedynczej kolumny zawiera kolejno elementy:

name
nazwa kolumny – element obowiązkowy, musi być ustawiony
type_code
typ kolumny – element obowiązkowy, musi być ustawiony
display_size
internal_size
precision
scale
null_ok
False jeśli kolumna ma atrybut NOT NULL, True w przeciwnym wypadku

Elementy które nie są ustawione mają wartość None.

rowcount
Liczba zwróconych wierszy w przypadku SELECT, lub liczba stworzonych/zmienionych/usuniętych wierszy w przypadku INSERT / UPDATE / DELETE. Jeśli nie można ustalić ilość wierszy ostatniej instrukcji to rowcount jest ustawione na -1.

Wysyłanie zapytań

execute(operation[, parameters])
Tworzy i wywołuje zapytanie w bazie danych. Opcjonalny argument parameters może być (w zależności od paramstyle) sekwencją lub słownikiem. Wyniki zapytania są dostępne za pomocą kursora.
executemany(operation, seq_of_parameters)
Tworzy i wywołuje wielokrotne zapytanie w bazie danych. Zasadniczo działa jak poniższy kod (ale umożliwia lepszą optymalizację):


for parameters in seq_of_parameters:
    execute(operation, parameters)

Pobieranie danych

Po wykonaniu zapytania pobierającego dane z bazy należy je z niej odebrać.

fetchone()
Pobierana następny wiersz z wyniku zapytania, zwracając pojedynczą sekwencję, lub None kiedy wszystkie dane zostały już pobrane. Przykład użycia tej metody do pobrania wszystkich elementów z kursora


row = cur.fetchone()
while row is not None:
    print row
    row = cur.fetchone()
fetchmany([size=cursor.arraysize])
Pobiera pewną określoną poprzez argument size ilość elementów. Jeśli argument nie jest jawnie podany to zostaje użyta wartość cursor.arraysize.
fetchall()
Pobiera wszystkie (z tych, które oczekują na pobranie) wiersze z wyniku zapytania. Zwraca sekwencję sekwencji.

Inne

callproc(procname[, parameters])
Wywołuje procedurę składowaną. Dostępne tylko w niektórych bazach danych.
close()
Zamyka kursor, od tej pory nie można z niego korzystać.

Obsługa danych hierarchicznych z użyciem PL/pgSQL


Wstęp

Pracując z bazami danych często ma się do czynienia z danymi hierarchicznymi. Oznacza to, że struktura rekordów przypomina drzewo. Każdy element może mieć maksymalnie jednego rodzica (jeśli nie ma rodzica to nazywany jest korzeniem drzewa), jak również dowolną ilość dzieci. Przodkami elementu nazywamy jego rodzica, rodzica jego rodzica, rodzica rodzica jego rodzica, itd. Natomiast potomkami nazywamy dzieci elementu, dzieci jego dzieci, itd.

Struktura bazy danych

Istnieje wiele metod przechowywania danych hierarchiczny w relacyjnych bazach danych. Najprostsza polega na dodaniu do rekordu pola wskazującego element który jest rodzicem (jest to zwykle klucz obcy odnoszący się do klucza głównego rodzica lub NULL jeśli mamy do czynienia z korzeniem). Jest to bardzo prosta struktura, którą bardzo łatwo zaimplementować.

Poniższym przykład pokazuje prostą hierarchiczną strukturę danych. Warto zwrócić uwagę na narzucone więzy integralności.


CREATE TABLE tree (
  id INTEGER PRIMARY KEY,
  parent INTEGER
    REFERENCES tree(id)
    ON DELETE CASCADE,
  value VARCHAR(100)
);
INSERT INTO tree VALUES (1,   NULL, 'a');
INSERT INTO tree VALUES (2,   NULL, 'b');
INSERT INTO tree VALUES (11,  1,    'aa');
INSERT INTO tree VALUES (111, 11,   'aaa');
INSERT INTO tree VALUES (112, 11,   'aab');
INSERT INTO tree VALUES (12,  1,    'ab');
INSERT INTO tree VALUES (121, 12,   'aba');
INSERT INTO tree VALUES (122, 12,   'abb');
INSERT INTO tree VALUES (21,  2,    'ba');
INSERT INTO tree VALUES (211, 21,   'baa');

Wiele prostych operacji da się wykonać na tej strukturze za pomocą nie skąplikowanych zapytań SQL. Poniższe przykłady pokazują operacje dla rekordu, którego wartość wynosi 'ab'. Warto zwrócić uwagę na usuwanie danych (za pomocą instrukcji DELETE) – dzięki użyciu konstrukcji ON DELETE CASCADE podczas tworzenia tabeli baza zapewnia nam automatyczne usuwanie wszystkich potomków usuwanego rekordu.


-- pobranie klucza rodzica
SELECT parrent FROM tree WHERE value = 'ab';
-- pobranie rodzica
SELECT * FROM tree WHERE id = (
  SELECT parent FROM tree WHERE value = 'ab'
);
-- pobranie dzieci
SELECT * FROM tree WHERE parent = (
  SELECT id FROM tree WHERE value = 'ab'
);
-- zmiana rodzica elementu
UPDATE tree
  SET parent = (SELECT id FROM tree WHERE value = 'b')
  WHERE value = 'ab';
-- usuniecie elementu i wszystkich jego potomków
DELETE FROM tree WHERE value = 'ab';

Pobieranie przodków i potomków

Niestety w tej strukturze danych pobieranie przodków i potomków jest bardziej skomplikowane. Zwykle aby rozwiązać ten problem stosuje się wielokrotne wywoływanie zapytań SQL z poziomu aplikacji korzystającej z bazy danych. Niestety jest to rozwiązanie wolniejsze od zastosowania pojedynczego wywołania.

Jeśli korzystamy z bazy danych PostgreSQL to możemy rozwiązać ten problem poprzez wykorzystanie języka PL/pgSQL.

Pobieranie przodków (ancestors)

Poniższy funkcja zwraca ciąg kluczy rekordów na ścieszce od klucza podanego jako argument do korzenia danego drzewa.


CREATE OR REPLACE FUNCTION tree_ancestors(
  start_id int
) RETURNS SETOF int AS $$
DECLARE
  cid int := start_id;
BEGIN
  WHILE cid IS NOT NULL LOOP
    RETURN NEXT cid;
    SELECT parent INTO cid FROM tree WHERE tree.id = cid;
  END LOOP;
END;
$$ LANGUAGE plpgsql strict;

-- przykłady użycia:
-- pobranie samych numerów przodków
SELECT * FROM tree_ancestors(111);
SELECT t FROM tree_ancestors(111) AS t;
-- pobranie głebokości w drzewie (1 == korzen)
SELECT count(*) FROM tree_ancestors(111);
-- pobranie całych rekordów przodków
SELECT id, parent, value
  FROM tree_ancestors(111) AS t JOIN tree ON t = tree.id;

Pobieranie potomków (descendants)

Poniższy funkcja zwraca ciąg kluczy rekordów potomków danego rekordu. Kolejność wyników jest typowa dla wyszukiwania wszerz.
Oznacza to, że najpierw są zwracane dzieci rekordu, dopiero potem dzieci tych dzieci, itd.


CREATE OR REPLACE FUNCTION tree_descendants(
  start_id int
) RETURNS SETOF int AS $$
DECLARE
  rec RECORD;
  current INT[];
  build INT[];
  tmp INT;
BEGIN
  build := ARRAY[0];
  current := ARRAY[0, start_id];
  WHILE current > ARRAY[0] LOOP
    build := ARRAY[0];
    FOR i IN 2..array_upper(current, 1) LOOP
      tmp := current[i];
      FOR rec IN SELECT * FROM tree WHERE parent = tmp LOOP
        RETURN NEXT rec.id;
        build := build || rec.id;
      END LOOP;
    END LOOP;
    current := build;
  END LOOP;
END;
$$ LANGUAGE plpgsql strict;

-- przykłady użycia:
-- pobranie numerów potomków
SELECT * FROM tree_descendants(1);
SELECT t FROM tree_descendants(1) AS t;
-- pobranie ilości potomków
SELECT count(*) FROM tree_descendants(1);
-- pobranie całych rekordów potomków
SELECT id, parent, value
  FROM tree_descendants(1) AS t JOIN tree ON t = tree.id;

Podsumowanie

Kilka ogólnych uwag dotyczących korzystania z PL/pgSQL do obsługi tej struktury danych:

  • wykorzystanie PL/pgSQL wiąże nas z bazą PostgreSQL
  • bardzo trudno było by korzystać z tej metody za pomocą ORM
  • jeśli mamy zamiar korzystać ze zliczania przodków a zwłaszcza potomków to zalecane by było napisać funkcje bardziej dostosowane do tego celu

RINQ – Ruby Integrated Query Language

Cele

Już od dłuższego czasu odchodzi się od pisania zapytań do baz danych poprzez pisanie zapytań SQL. Ma to wiele przyczyn, wśród nich można wymienić:

  • niezgodność pomiędzy modelem relacyjnym a obiektowym
  • brak przenośności – niestety SQL nie jest standardem, różne bazy danych mają własną składnię, często mocno się różniącą
  • kolejny język do nauki
  • zagrożenie SQL Injection

Wszystkie te problemy rozwiązuje Mapowanie Obiektowo-Relacyjne (ORM – Object-Relational Mapping). Lecz istnieje jeszcze jeden zagadnienie, którym warto by się zająć:

  • obsługa wyłącznie baz danych

Istnieje wiele różnych źródeł danych, nie tylko relacyjne bazy danych. Odwołujemy się do nich bardzo często w ten sam sam sposób. Wybieramy tylko te elementy kolekcji które są nam potrzebne, wybieramy potrzebne pola elementów, sortujemy wedle zadanych kryteriów. Czy można korzystać z różnych danych w ten sam sposób?

LINQ – Language Integrated Query

LINQ to technika dostępna w .NET (np. z poziomu języka C#). Umożliwia ona tworzenie zapytań zintegrowanych z językiem z którego korzystamy.

Jej zadaniem jest ujednolicenie sposobu dostępu do danych z różnych źródeł. Przykładowe zapytania:

  • Obiekty w aplikacji
    
    int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
    
    var q =
      from n in numbers
      where n < 5
      select n;
    
  • Dane XML
    
    XElement doc = XElement.Load(@"numbers.xml");
    var q = new XElement("numbers",
      from n in doc.Elements("number")
      where (int) n < 5
      select new XElement("number", n)
      )
    );
    
  • Bazy danych
    
    Database db = Database(...)
    var q =
      from n in db.numbers
      where n.number < 5
      select new {n.number};
    

Przegląd możliwości języka Ruby

Odpowiedniki LINQ da się stworzyć w wielu językach. Ale w Ruby będzie to implementacja nie tylko działająca ale też dająca możliwość tworzenia czytelnego kodu.

DSL – Domain-specific programming language

Języki dziedzinowe w odróżnieniu od języków ogólnych (takich jak C, Java czy Python) są wysokowyspecjalizowane i tworzone to rozwiązywania konkretnych problemów. Dzięki temu są dobrze dopasowane do określonych zadań, a przez to wygodne w stosowaniu. Pozwala to na przyśpieszenie pracy z nimi.

Języki dziedzinowe wcale nie są czymś nowym. Stykamy się z nimi codzienne i wydają się nam tak oczywiste że nie dostrzegamy czym są. Warto więc wymienić kilka przykładów:

  • znaki drogowe
  • notacja muzyczna
  • wyrażenia regularne
  • pliki Makefile

DSL jest bardzo często stosowany w języku Ruby. Przykładem tego może być ActiveRecord dostępny w Ruby On Rails. Przykładowy model:


class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags
end

Metaprogramowanie

Metaprogramowanie dało by się streścić jako programy piszące programy. W Ruby, jako że jest on językiem dynamicznym, program może zmieniać nawet swój własny kod podczas działania. Najczęściej służy temu funkcja eval:


name = 'plus'
op = '+'
a = "def #{name}(a, b) a #{op} b end"
eval(a)
plus(1,2)

Symbole


'very_long_name'

:very_long_name

Symbole bywają (błędnie) nazywane lekkimi stringami. Bardzo często stosuje się je jako klucze w słownikach.

Atrybuty – settery, gettery


class Cos
  def a
    @a
  end
  def a=(value)
    @a = value
  end
end

class Cos
  attr_accesor :a
end

Pomijanie nawiasów w wywołaniu funkcji


c = Cos.new()
c.a=(7)
puts c.a()

funkcja({'a'=>'Ala', b=>'Beata'})

inna_funkcja(3.14, {'a'=>'Ala'})

c = Cos.new
c.a = 7
puts c.a

funkcja 'a'=>'Ala', b=>'Beata'

inna_funkcja 3.14, 'a'=>'Ala'

Jeśli nie spowoduje to niejednoznaczności to można usunąć nawiasy z wywołania funkcji. Dzięki temu oszczędza się kilku znaków oraz sprawia, że kod wygląda czytelniej.

Ruby nie posiada argumentów w postaci słów kluczowych tak jak Python. Można je jednak jest bardzo łatwo zastąpić używając słowników z pominiętymi nawiasami klamrowymi. W takich konstrukcjach bardzo często używa się symboli.

Otwarte moduły


module Kernel
  def query()
    puts 'this is query'
  end
end

Moduły można łatwo modyfikować. Jeśli coś dodamy do modułu Kernel

Otwarte klasy


class Fixnum
  def hours
    self * 3600
  end
  alias hour hours
end

Time.mktime(2006, 01, 01) + 14.hours

Metody można nie tylko dodawać ale też zamieniać (korzystając z aliasów).

Otwarte obiekty


a = 'hello'

class <<a
  def to_s
    "<#{self}>"
  end
end

Brakujące metody


class Cos
  def method_missing(method_name, *args)
    method_name = method_name.to_s
    if method_name[0..3] == 'say_'
      puts method_name[4..-1]
    else
      raise NoMethodError, "`#{method_name}' in #{self}"
    end
  end
end

Domknięcia


def two_times()
  if block_given?
    yield
    yield
  end
end
two_times { puts 'Hello'}

[1,2,3,4].delete_if {|a| a % 2 == 0}

RINQ

Pozwolę sobie przedstawić tu dwie propozycje propozycje RINQ. Są one autorstwa Stena Friedricha z Technische Fachhochschule Berlin.

Operatory zapytań

Pierwsza metoda przypomina trochę korzystanie z cout w C++, lub Django ORM. Wywołujemy po koleji ciąg funkcji z których każda zwraca ten sam obiekt, lecz zmodyfikowany przez to wywołanie.


customernames = customers.
  qwhere   {|c| c.address.city == "Torun"}.
  qselect  {|c| {:firstname => c.firstname,
                 :lastname => c.lastname}}.
  qorderby {|c| c.lastname}

Wyrażenia zapytań

Tu wkracza DSL. Przykład takiej implementacji autorstwa Petera Vanbroekhovena jest How to create a Domain Specific Language? – Metaprogramming in Ruby.


customerfirstnames = query do
  qfrom :c => customers
  qwhere c.lastname == "Kowalski"
  qselect c.firstname
end

Slajdy

RINQ – slajdy