Nieregularnie Zadawane Pytania

Ten tekst został zainspirowany artykułem Petera Norviga. W mojej wersji będzie zawierał pytania i odpowiedzi, które nie pojawiają się może zbyt często, ale są na tyle ciekawe, iż szkoda, aby zaginęły w pomrokach dziejów.

1   Kod w klauzuli finally zawsze zostanie wykonany, prawda?

Co znaczy zawsze? Hmm..., prawie zawsze. Kod w klauzuli finally jest wywoływany po klauzuli try niezależnie od tego czy wystąpi wyjątek i nawet wtedy gdy wywołana zostanie funkcja sys.exit. Jednak klauzula finally nie wykona się, gdy program w ogóle do niej nie dotrze. W poniższej sytuacji to właśnie nastąpi niezależnie od wartości wybor:

try:
    if wybor:
        while True:
            pass
    else:
        print "Prosze wyjac wtyczke z komputera za jakis czas..."
        time.sleep(60 * 60 * 24 * 365 * 10000)
finally:
    print "Finally ..."

2   Polimorfizm jest super; mogę sortować listy elementów dowolnego typu, prawda?

Nieprawda. Rozważ to:

>>> x = [1, 1j]
>>> x.sort()
Traceback (most recent call last):
...
TypeError: no ordering relation is defined for complex numbers

(Liczba 1j jest pierwiastkiem kwadratowym liczby -1.) Problem tu polega na tym, że metoda sort (w obecnej implementacji), porównuje elementy używając metody __lt__, która odmawia porównywania liczb zespolonych (ponieważ nie można ich sortować). Co ciekawe, complex.__lt__ nie ma żadnych problemów z porównywaniem liczb zespolonych do stringów, list i wszystkich innych typów poza liczbami zespolonymi. Czyli odpowiedź brzmi: możesz sortować sekwencje obiektów, które posiadają metodę __lt__ (i może też inne metody jeśli implementacja się zmieni).

Co do pierwszej części pytania, "Polimorfizm jest super", to bym się zgodził, ale Python czasami utrudnia użycie go, ponieważ wiele typów Pythona (jak np. sekwencje i liczby) jest zdefiniowanych nieformalnie.

3   Czy mogę zrobić ++x i x++ w Pythonie?

Właściwie to... i tak i nie; ale ze względów praktycznych: nie. Co mam na myśli?

  • Tak, ++x jest składniowo prawidłowym zapisem w Pythonie, ale nie oznacza tego co myślisz, jeśli jesteś programistą C++ lub Javy. + jest jednoargumentowym operatorem prefiksowym, a więc ++x jest parsowane jako +(+(x)), czego rezultatem (przynajmniej dla liczb) jest po prostu x.

    >>> x = 1
    >>> ++x
    1
    
  • Nie, x++ już samo w sobie nie jest prawidłowym wyrażeniem, chociaż są konteksty, w których może być prawidłowe. Na przykład: x++ -y jest parsowane jako x + +(-(y)), a więc dla liczb jest to to samo co x - y. Oczywiście mógłbyś utworzyć klasę, w której ++x miałoby jakiś (pewnie ograniczony) sens; np. klasa mogłaby zawierać liczbę i posiadać jednoargumentowy operator +, który zwiększałby ją o 0.5, ale ...

  • Nie, to byłoby niemądre. Lepiej korzystać z wyrażenia x += 1, które zostało dodane do Pythona 2.0.

Głębszym pytaniem jest: dlaczego Python nie dopuszcza wyrażenia x++? Myślę, że z tego samego powodu, dla którego nie pozwala też na przypisania w ramach wyrażeń. Python chce jasno odseparować instrukcje od wyrażeń. Jeśli ktoś uważa, że powinny one być rozdzielone, wtedy odrzucenie ++ jest prawdopodobnie najlepszą decyzją. Z drugiej jednak strony, zwolennicy języków funkcyjnych argumentują, że instrukcja powinna być wyrażeniem. Zgadzam się tu z moim kolegą Duńczykiem, Bjarnem Stroustrupem. Powiedział on w The Design and Evolution of C++ "Gdybym projektował język od zera, to poszedłbym ścieżką wytyczoną przez język Algol68 i uczyniłbym każdą instrukcję i deklarację wyrażeniem, które zwraca wartość".

4   Mogę użyć składni C++ dla strumieni: cout << x << y ... ?

Możesz. Jeśli nie lubisz pisania "print x, y", to możesz spróbować tego:

import sys

class ostream(object):
    def __init__(self, fileobj):
        self.fileobj = fileobj

    def __lshift__(self, obj):
        self.fileobj.write(str(obj));
        return self

>>> cout = ostream(sys.stdout)
>>> cerr = ostream(sys.stderr)
>>> nl = '\n'
>>> x, y = 20, 30
>>> cout << x << " " << y << nl
20 30
<__main__.ostream object at 0xb7dd534c>

Ten kod dostarcza możliwości użycia odmiennej składni, ale nie daje jakiejś nowej możliwości wyświetlania -- po prostu opakowuje standardowy sposób Pythona, czyli str. Jest to podobne do sposobu Javy toString(). C++ ma inne podejście, zamiast standardowego sposobu konwersji obiektu do stringa, posiada on standardowy sposób na wysyłanie obiektu do strumienia (właściwie, semi-standardowy--- wiele programów w C++ nadal używa printf). Podejście strumieniowe jest bardziej skomplikowane, ale ma tę zaletę, że gdy potrzebujesz wyświetlić jakiś ogromny obiekt, to nie musisz tworzyć ogromnego obiektu tymczasowego.

5   Co jeśli lubię printf z C?

To nie jest taki zły pomysł, aby zdefiniować sobie funkcję printf w Pythonie. Ktoś mógłby argumentować, że printf("%d = %s", num, result) jest bardziej naturalne niż print "%d = %s" % (num, result),, ponieważ nawiasy są w bardziej odpowiednim miejscu (i nie ma dodatkowego znaku %). A co więcej to jest taaakie proste:

def printf(format, *args): print format % args,

Nawet w takiej jednej linijce jest jednak kilka subtelności. Po pierwsze, musiałem zadecydować, czy dodać przecinek na końcu, czy nie? Aby to było bardziej podobne do C, zdecydowałem się go dodać (co oznacza, że jeśli chcesz aby był wyświetlany znak nowej linii, musisz go dodać samodzielnie do formatu). Po drugie, to nadal będzie wyświetlało dodatkową spację na końcu. Jeśli nie chcesz tego, użyj sys.stdout.write zamiast print. Po trzecie ta funkcja, poza byciem podobnym do C, ma też inne zalety. Czasami potrzebujemy funkcji do wyświetlania (w odróżnieniu od instrukcji do wyświetlania), aby moć jej użyć w miejscach, które akceptują tylko funkcje, tak jak w wyrażeniach lambda czy w przypadku pierwszego argumentu do funkcji map. Właściwie to taka funkcja jest na tyle poręczna, że prawdopodobnie przydałaby się nawet taka, która nie robi żadnego formatowania.

def prin(x): print x,

Teraz map(prin, seq) wyświetli wszystkie elementy z seq, ale map(print, seq) zwróci błąd składni.

>>> seq = [1, 2, 3, 4]
>>> map(prin, seq)
1 2 3 4
[None, None, None, None]
>>> map(print, seq)
  File "<stdin>", line 1
    map(print, seq)
          ^
SyntaxError: invalid syntax

Widziałem już paru nieostrożnych programistów (no dobra, to byłem ja, ale wiedziałem o tym, że byłem nieostrożny), którzy myśleli, że dobrym pomysłem jest połączenie tych funkcji w jedną, np.:

def printf(format, *args): print str(format) % args,

Wtedy printf(42), printf('Tekst w kilku\n linijkach') i printf('%4.2f', 42) działa bez problemu. Jednak dobry pomysł szybko zmienia się w co ja miałem na myśli, gdy zrobimy printf('gwarantowane w 100%') lub cokolwiek innego ze znakiem % nie oznaczającym instrukcji formatowania. Jeśli zaimplementowałeś taką wersję funkcji printf, wymaga ona takiego komentarza:

def printf(format, *args):
    """Formatuje i wyświetla `args` używając pierwszego argumentu jako stringu
       formatującego.
       Jeśli `format` nie jest stringiem, to jest konwertowany przy pomocy `str`.
       Należy użyć `printf('%s', x)` zamiast `printf(x)` jeśli `x` może zawierać
       znaki % lub \ .
    """
    print str(format) % args,

6   Istnieje lepsza składnia literałowa dla słowników? Wszystkie moje klucze są identyfikatorami.

Tak! Zgadzam się, że może być żmudne wklepywanie znaków cydzysłowia otaczających klucze, szczególnie przy dużym słowniku. Na początku myślałem, że mogłaby być użyteczna zmiana w samym Pythonie dodająca specjalną składnię, może np.: {a=1, b=2} zamiast {'a':1, 'b':2}. Począwszy od Pythona 2.3 możemy używać składni dict(a=1, b=2, c=3, dee=4), która jest wystarczająco dobra, jeśli o mnie chodzi.

Przed Pythonem 2.3 używałem jednolinijkowej funkcji:

>>> def Dict(**d): return d

>>> Dict(a=1, b=2, c=3)
{'a': 1, 'c': 3, 'b': 2}

7   Istnieje podobny skrót dla obiektów?

Faktycznie istnieje. Jeśli chcesz tylko utworzyć obiekt, który przechowa jakieś dane w kilku polach, możesz to zrobić tak:

class Struct(object):
    def __init__(self, **pola): self.__dict__.update(pola)

>>> pozycja = Struct(odpowiedz=42, dl_linii=80, font='courier')
>>> pozycja.odpowiedz
42
>>> pozycja.odpowiedz = 'inna odpowiedz'
>>> vars(pozycja)
{'odpowiedz': 'inna odpowiedz', 'dl_linii': 80, 'font': 'courier'}

Zasadniczo to co tu robimy, to jest utworzenie klasy anonimowej. OK, wiem że klasą obiektu pozycja jest Struct, ale ponieważ dodajemy do niego atrybuty, to jest to właściwie jak utworzenie nowej, nienazwanej klasy (w ten sam sposób, w jaki lambda tworzy anonimowe funkcje). Nie cierpię mieszać w Struct, ponieważ jest ona tak zwięzła w tej postaci w jakiej jest, ale gdy dodamy tę metodę do niej, to mamy możliwość uzyskania ładnego wydruku każdej struktury:

def __repr__(self):
    args = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
    return 'Struct(%s)' % ', '.join(args)

>>> pozycja
Struct(odpowiedz='inna odpowiedz', dl_linii=80, font='courier')

8   To jest świetne do tworzenia obiektów. A co z modyfikacją?

No cóż, słowniki mają metodę update, a więc możemy wykonać d.update(dict(a=100, b=200)), gdzie d jest słownikiem. Nie ma analogicznej metody dla obiektów, a więc musimy napisać: obj.a = 100; obj.b = 200. Lub zdefiniować funkcję, która pozwoli na wykonanie update(x, a=100, b=200), gdzie x jest albo słownikiem albo obiektem:

import types

def update(x, **pozycje):
    if type(x) == types.DictType: x.update(pozycje)
    else: x.__dict__.update(pozycje)
    return x

To jest szczególnie fajne dla konstruktorów:

def __init__(self, a, b, c, d=42, e=None, f=()):
    update(self, a=a, b=b, c=c, d=d, e=e, f=f)

9   Czy mogę mieć słownik z wartością domyślną równą 0 lub [] lub cokolwiek?

Zgadzam się, że gdy w słowniku przechowujemy ilości czegoś, to dużo sympatyczniej jest móc napisać count[x] += 1 niż count[x] = count.get(x, 0) + 1. A skoro od wersji 2.2 Pythona jest łatwo dziedziczyć po typie wbudowanym dict, to jest to najprostszy sposób na rozwiązanie tego problemu. Nazwę moją wersję DefaultDict. Zauważ, że użyłem copy.deepcopy, aby mieć pewność, iż każdy klucz w słowniku otrzyma jako wartość domyślną oddzielny obiekt [], a nie będzie współdzielił tego samego obiektu (marnujemy trochę czasu na kopiowanie 0, ale ta strata czasu nie jest tak istotna, jeśli wykonujemy więcej modyfikacji i pobierania wartości, niż inicjalizacji):

class DefaultDict(dict):
    """Slownik z domyslna wartoscia, dla nieznanych kluczy."""
    def __init__(self, default):
        self.default = default

    def __getitem__(self, key):
        if key in self: return self.get(key)
        return self.setdefault(key, copy.deepcopy(self.default))

>>> import copy
>>> d = DefaultDict(0)
>>> d['hello'] += 1
>>> d
{'hello': 1}
>>> d2 = DefaultDict([])
>>> d2[1].append('hej')
>>> d2[2].append('swiecie')
>>> d2[1].append('witaj')
>>> d2
{1: ['hej', 'witaj'], 2: ['swiecie']}
def bigrams(slowa):
    "Zlicza pary slow w slowniku slownikow."
    d = DefaultDict(DefaultDict(0))
    for (s1, s2) in zip([None] + slowa, slowa + [None]):
        d[s1][s2] += 1
    return d

>>> bigrams('i am what i am'.split())
{None: {'i': 1}, 'i': {'am': 2}, 'what': {'i': 1}, 'am': {None: 1, 'what': 1}}

Zauważ, że bez DefaultDict zapis d[s1][s2] += 1 w powyższym przykładzie musiałby wyglądać tak:

d.setdefault(s1,{}).setdefault(s2, 0); d[s1][s2] += 1

10   Hej, można napisać kod transponujący macierz w 0.007KB lub mniej?

Już myślałem, że nigdy nie zapytasz. Jeśli macierz reprezentowana jest jako sekwencja sekwencji, wtedy można użyć zip:

>>> m = [(1,2,3), (4,5,6)]
>>> zip(*m)
[(1, 4), (2, 5), (3, 6)]

Aby to zrozumieć, musisz wiedzieć, że f(*m) jest równoważne apply(f, m). To rozwiązanie bazuje na starym problemie Lispowym, na który odpowiedzią Pythona jest odpowiednik dla map(None, *m), ale wersja z zip, zasugerowana przez Chih-Chung Chang, jest nawet krótsza. Mógłbyś pomyśleć, że to może się przydać jedynie do jakichś głupich programistycznych sztuczek, ale któregoś dnia natknąłem się na taki problem: Miałem listę wierszy z bazy danych, gdzie każdy wiersz był listą uporządkowanych wartości. Miałem znaleźć listę unikalnych wartości, które występują w każdej kolumnie. A więc napisałem:

mozliwe_wartosci = map(unique, zip(*db))

11   Ten trik z f(*m) jest fajny. Czy ta składnia działa też z wywołaniami metod jak np.: x.f(*y)?

Takie pytanie ujawnia powszechne błędne mniemanie. Nie ma składni dla wywołania metod! Jest tylko składnia do wywoływania funkcji, składnia do odczytu atrybutów obiektu i są też metody związane. Te trzy cechy połączone wyglądają tak: x.f(y), to jest po prostu składnia, która właściwie odpowiada temu (x.f)(y), co z kolei odpowiada temu (getattr(x, 'f'))(y). Widzę, że mi nie wierzysz. Spójrz:

class X(object):
    def f(self, y): return 2 * y

>>> x = X()
>>> x.f
<bound method X.f of <__main__.X object at 0xb7b737ec>>
>>> y = 21
>>> x.f(y)
42
>>> (x.f)(y)
42
>>> (getattr(x, 'f'))(y)
42
>>> xf = x.f
>>> xf(y)
42
>>> map(x.f, range(5))
[0, 2, 4, 6, 8]

A więc odpowiedź na Twoje pytanie brzmi: możesz użyć *y lub **y (lub cokolwiek innego czego użyłbyś przy wywołaniu funkcji) przy wywołaniu metody, ponieważ wywołania metod są to po prostu wywołania funkcji.

12   Czy można tworzyć klasy abstrakcyjne w Pythonie w 0 liniach kodu? Lub w 4?

Java ma słowo kluczowe abstract, a więc można definiować klasy abstrakcyjne, które nie mogą mieć swoich instancji, ale mogą mieć klasy potomne, jeśli zostaną zaimplementowane wszystkie metody abstrakcyjne w tejże klasie. Mało znanym faktem jest to, że w Pythonie można użyć słowa abstract w prawie taki sam sposób. Różnica polega na tym, że błąd otrzymamy w czasie działania programu, gdy spróbujemy wywołać niezaimplementowaną metodę, a nie w czasie kompilacji. Porównajmy:

# Python
class MojaKlasaAbstrakcyjna(object):
    def metoda1(self): abstract

class MojaKlasa(MojaKlasaAbstrakcyjna):
    pass

>>> MojaKlasa().metoda1()
Traceback (most recent call last):
...
NameError: global name 'abstract' is not defined
/* Java */
public abstract class MojaKlasaAbstrakcyjna {
   public abstract void metoda1();
}

class MojaKlasa extends MojaKlasaAbstrakcyjna {}
$ javac MojaKlasaAbstrakcyjna.java
MojaKlasaAbstrakcyjna.java:6: MojaKlasa is not abstract and does not override
abstract method metoda1() in MojaKlasaAbstrakcyjna
class MojaKlasa extends MojaKlasaAbstrakcyjna {}
^
1 error

Nie trać zbyt wiele czasu na poszukiwanie słowa kluczowego abstract w podręczniku do Pythona; nie ma go tam. Dodałem go do języka, a najlepsze jest to, że jego implementacja wymagała zero linii kodu! To działa tak, że jeśli wywołasz metoda1, to otrzymasz błąd NameError, ponieważ nie istnieje zmienna abstract. (Możesz powiedzieć, że to oszustwo, bo to nie zadziała, jeśli ktoś zdefiniuje zmienną abstract. Ale w końcu każdy program przestanie działać, jeśli ktoś przedefiniuje zmienną, od której działanie programu zależy. Jedyna różnica jest taka, że my w tym wypadku polegamy raczej na braku definicji, niż na jej wystąpieniu.)

Jeśli masz ochotę napisać abstract() zamiast abstract, to możesz zdefiniować funkcję, która rzuci wyjątek NotImplementedError zamiast NameError, który ma nieco więcej sensu. (A także, jeśli ktoś zdefiniuje abstract jako cokolwiek niebędącego bezargumentową funkcją, to nadal otrzymamy komunikat o błędzie.) Aby nieco poprawić komunikat błędu zaglądamy do ramki stosu, aby zobaczyć kim jest wywołujący:

def abstract():
    import inspect
    caller = inspect.getouterframes(inspect.currentframe())[1][3]
    raise NotImplementedError(caller + ' musi byc zaimplementowana w klasie potomnej')

class MojaKlasaAbstrakcyjna(object):
    def metoda1(self): abstract()

class MojaKlasa(MojaKlasaAbstrakcyjna):
    pass

>>> MojaKlasa().metoda1()
Traceback (most recent call last):
...
NotImplementedError: metoda1 musi byc zaimplementowana w klasie potomnej

13   Jak można utworzyć typ wyliczeniowy (enum) w Pythonie?

Powodem, dla którego nie ma jednej odpowiedzi na to pytanie w Pythonie, jest to, że może ich być kilka, w zależności od tego czego oczekujesz od typu wyliczeniowego. Jeśli chcesz po prostu zdefiniować kilka zmiennych i do każdej przypisać unikalną liczbę całkowitą, możesz po prostu zrobić tak:

red, green, blue = range(3)

Wadą tego rozwiązania jest to, że ilekroć dodasz zmienną po lewej stronie, będziesz musiał także zwiększyć liczbę po prawej stronie. W sumie to nie jest takie złe, ponieważ jeśli się w tym pomylisz, to Python zgłosi błąd. Jednak prawdopodobnie bardziej higienicznie będzie opakować ten typ wyliczeniowy w klasę:

class Colors(object):
    red, green, blue = range(3)

Teraz Colors.red zwraca 0 i dir(Colors) też może być użyteczne (chociaż musimy ignorować __doc__ i __module__). Jeśli potrzebujesz mieć kontrolę nad tym jaką wartość przyjmie każda za zmiennych naszego enuma, to możesz wykorzystać klasę Struct z odpowiedzi kilka pytań temu:

Enum = Struct
Colors = Enum(red=0, green=100, blue=200)

Chociaż te proste rozwiązania zwykle wystarczają, niektórzy ludzie chcą czegoś więcej. Tu można znaleźć kilka implementacji: python.org, ASPN, faqts.

14   Jak się realizuje wzorzec Singletona w Pythonie?

Zakładam, że masz na myśli klasę, która może mieć tylko jedną instancję i rzuca wyjątek, gdy próbujesz powołać ich więcej. Najprostszym sposobem jaki znam, aby to osiągnąć jest zdefiniowanie funkcji, która wprowadza w życie tę ideę i wywołanie tej funkcji z konstruktora tejże klasy:

def singleton(object, instancje=[]):
    "Rzuca wyjatek, gdy instancja tej klasy juz byla powolana wczesniej."
    assert object.__class__ not in instancje,         "%s instancja zostala juz powolana" % object.__class__
    instancje.append(object.__class__)

class MojaKlasa(object):
    "Klasa singletona do zrobienia czegos ..."
    def __init__(self, args):
        singleton(self)
        ...

>>> a = MojaKlasa(1)
>>> a
<__main__.MojaKlasa object at 0xb7b78a6c>
>>> b = MojaKlasa(2)
Traceback (most recent call last):
...
AssertionError: <class '__main__.MojaKlasa'> jest klasa Singletona i jej instancja zostala juz powolana

Moglibyśmy także kombinować z metaklasami, pisząc później: class MojaKlasa(Singleton), ale po co sobie utrudniać życie? Zanim "Banda Czworga" [1] wszystko sformalizowała singleton (jeszcze bez formalnej nazwy) był tylko prostą ideą, która wymagała kilku prostych linii kodu, a nie całej religii.

15   Czy brak "new" to dobra nowina? [2]

Przypuszczam, że uważasz, że to dobrze, iż Python nie ma słowa kluczowego new. I tak faktycznie jest. W C++ new jest używany do podkreślenia faktu, iż alokacja następuje na stercie, a nie na stosie. A zatem to słowo kluczowe jest tu użyteczne. W Javie wszystkie obiekty są alokowane na stercie, a więc słowo kluczowe new nie ma większego sensu; służy jedynie jako rozróżnienie pomiędzy konstruktorem i innymi metodami statycznymi. Ale czynienie takiego rozróżnienia przynosi Javie więcej szkody niż pożytku, ponieważ to rozróżnienie jest bardzo niskopoziomowe i wymusza decyzje implementacyjne, które naprawdę powinny móc zapaść później. Myślę, że Python dokonał właściwego wyboru, pozostawiając składnię wywołania konstruktora taką samą jak składnię normalnego wywołania funkcji.

Na przykład zanim pojawiła się klasa bool, mogliśmy mieć chęć do zaimplementowania jej. Nazwijmy ją Bool dla odróżnienia od typu wbudowanego. Załóżmy, że chcemy wcielić w życie pomysł, iż powinien występować tylko jeden obiekt true i jeden false typu Bool. Jednym sposobem jest zmiana nazwy klasy z Bool na _Bool (aby nie była eksportowana) i zdefiniowanie funkcji Bool tak:

def Bool(val):
    if val: return true
    else: return false

true, false = _Bool(1), _Bool(0)

16   Jak można zmierzyć czas wykonania funkcji?

Najprostsza odpowiedź jest taka:

def timer(fn, *args):
    """Mierzy czas wykonania funkcji `fn` z argumentami `args`.
       Zwraca (wynik, sekundy).
    """
    import time
    start = time.clock()
    return fn(*args), time.clock() - start

>>> timer(max, range(10**6))
(999999, 0.059999999999999998)

Nieco bardziej elastyczny sposób dostarcza biblioteka standardowa moduł timeit.


[1]ang. "Gang of Four"
[2]nieudolna próba oddania gry słów w ang. 'Is no "news" good news?'