Jaki jest sens dziedziczenia w Pythonie?

83

Załóżmy, że masz następującą sytuację

#include <iostream>

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
    void speak() { std::cout << "woff!" <<std::endl; }
};

class Cat : public Animal {
    void speak() { std::cout << "meow!" <<std::endl; }
};

void makeSpeak(Animal &a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    makeSpeak(d);
    makeSpeak(c);
}

Jak widać, makeSpeak to procedura, która akceptuje ogólny obiekt Animal. W tym przypadku Animal jest dość podobny do interfejsu Java, ponieważ zawiera tylko czystą metodę wirtualną. makeSpeak nie zna natury zwierzęcia, które jest przekazywane. Po prostu wysyła sygnał „speak” i pozostawia późne powiązanie, aby zadecydować, która metoda wywołania: Cat :: speak () lub Dog :: speak (). Oznacza to, że w przypadku makeSpeak wiedza o tym, która podklasa jest faktycznie przekazywana, jest nieistotna.

Ale co z Pythonem? Zobaczmy kod dla tego samego przypadku w Pythonie. Zwróć uwagę, że staram się przez chwilę być jak najbardziej podobny do przypadku C ++:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Teraz w tym przykładzie widzisz tę samą strategię. Używasz dziedziczenia, aby wykorzystać hierarchiczną koncepcję psów i kotów będących zwierzętami. Ale w Pythonie nie ma potrzeby stosowania tej hierarchii. Działa to równie dobrze

class Dog:
    def speak(self):
        print "woff!"

class Cat:
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

W Pythonie możesz wysłać sygnał „przemów” do dowolnego obiektu, który chcesz. Jeśli obiekt jest w stanie sobie z nim poradzić, zostanie wykonany, w przeciwnym razie zgłosi wyjątek. Załóżmy, że dodajesz klasę Airplane do obu kodów i przesyłasz obiekt Airplane do makeSpeak. W przypadku C ++ nie będzie się kompilować, ponieważ Airplane nie jest pochodną klasą Animal. W przypadku Pythona zgłosi wyjątek w czasie wykonywania, co może być nawet oczekiwanym zachowaniem.

Z drugiej strony załóżmy, że dodajesz klasę MouthOfTruth za pomocą metody speak (). W przypadku C ++, albo będziesz musiał zmienić hierarchię, albo będziesz musiał zdefiniować inną metodę makeSpeak, aby akceptować obiekty MouthOfTruth, albo w Javie możesz wyodrębnić zachowanie do CanSpeakIface i zaimplementować interfejs dla każdego z nich. Rozwiązań jest wiele ...

Chciałabym zwrócić uwagę, że nie znalazłem jeszcze żadnego powodu, aby używać dziedziczenia w Pythonie (poza frameworkami i drzewami wyjątków, ale myślę, że istnieją alternatywne strategie). nie trzeba implementować hierarchii wyprowadzonej z bazy, aby wykonywać operacje polimorficzne. Jeśli chcesz użyć dziedziczenia do ponownego wykorzystania implementacji, możesz osiągnąć to samo poprzez zawieranie i delegowanie, z dodatkową korzyścią polegającą na tym, że możesz je zmienić w czasie wykonywania, i jasno zdefiniować interfejs zawartej implementacji, bez ryzyka niezamierzonych skutków ubocznych.

W końcu pytanie brzmi: jaki jest sens dziedziczenia w Pythonie?

Edycja : dzięki za bardzo interesujące odpowiedzi. Rzeczywiście można go użyć do ponownego wykorzystania kodu, ale zawsze jestem ostrożny podczas ponownego wykorzystywania implementacji. Ogólnie rzecz biorąc, mam tendencję do tworzenia bardzo płytkich drzew dziedziczenia lub braku drzewa, a jeśli funkcjonalność jest powszechna, refaktoryzuję ją jako wspólną procedurę modułu, a następnie wywołuję ją z każdego obiektu. Widzę zaletę posiadania jednego punktu zmiany (np. Zamiast dodawać do Dog, Cat, Moose itd., Po prostu dodaję do Animal, co jest podstawową zaletą dziedziczenia), ale możesz osiągnąć to samo z łańcuch delegacji (np. a la JavaScript). Nie twierdzę jednak, że jest lepiej, po prostu w inny sposób.

Znalazłem też podobny post na ten temat.

Stefano Borini
źródło
18
-1: „to samo można osiągnąć dzięki łańcuchowi delegacji”. To prawda, ale o wiele bardziej bolesne niż dziedziczenie. Możesz osiągnąć to samo bez używania jakichkolwiek definicji klas, po prostu wiele skomplikowanych czystych funkcji. Możesz osiągnąć to samo na kilkanaście sposobów, a wszystkie są mniej proste niż dziedziczenie.
S.Lott
10
rzeczywiście powiedziałem „Nie twierdzę, że jest lepiej;)”
Stefano Borini
4
„Nie znalazłem jeszcze żadnego powodu, aby używać dziedziczenia w Pythonie” ... z pewnością brzmi jak „moje rozwiązanie jest lepsze”.
S.Lott
10
Przepraszam, jeśli sprawiło to wrażenie. Mój post miał na celu uzyskanie pozytywnych opinii na temat prawdziwych przypadków użycia dziedziczenia w Pythonie, których do dziś nie udało mi się znaleźć (głównie dlatego, że we wszystkich programach w Pythonie miałem do czynienia z przypadkami, w których nie było to potrzebne i kiedy Tak, to była sytuacja, którą wyjaśniłem powyżej).
Stefano Borini
2
Taksonomie świata rzeczywistego rzadko stanowią dobrą podstawę dla przykładów zorientowania obiektowego.
Apalala

Odpowiedzi:

81

Odnosisz się do pisania kaczego w czasie wykonywania jako dziedziczenia „nadpisującego”, jednak uważam, że dziedziczenie ma swoje zalety jako podejście do projektowania i wdrażania, będąc integralną częścią projektowania obiektowego. Moim skromnym zdaniem pytanie, czy można coś osiągnąć w inny sposób, nie jest zbyt istotne, ponieważ tak naprawdę można by kodować Pythona bez klas, funkcji i nie tylko, ale pytanie brzmi, jak dobrze zaprojektowany, solidny i czytelny będzie Twój kod.

Mogę podać dwa przykłady, w których dziedziczenie jest moim zdaniem właściwym podejściem, jestem pewien, że jest ich więcej.

Po pierwsze, jeśli mądrze kodujesz, twoja funkcja makeSpeak może chcieć sprawdzić, czy jej dane wejściowe są rzeczywiście Zwierzęciem, a nie tylko, że „może mówić”. W takim przypadku najbardziej elegancką metodą byłoby użycie dziedziczenia. Ponownie, możesz to zrobić na inne sposoby, ale na tym polega piękno projektowania obiektowego z dziedziczeniem - Twój kod „naprawdę” sprawdzi, czy wejście jest „zwierzęciem”.

Po drugie, i wyraźnie prostsze, jest Encapsulation - kolejna integralna część projektowania obiektowego. Staje się to istotne, gdy przodek ma członków danych i / lub metody nieabstrakcyjne. Weźmy następujący głupi przykład, w którym przodek ma funkcję (speak_twice), która wywołuje funkcję abstrakcyjną:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

Zakładając, że "speak_twice"jest to ważna funkcja, nie chcesz jej kodować zarówno w języku Dog, jak i Cat, i jestem pewien, że możesz ekstrapolować ten przykład. Jasne, możesz zaimplementować samodzielną funkcję Pythona, która zaakceptuje jakiś obiekt typu kaczego, sprawdź, czy ma funkcję mówienia i wywołaj ją dwukrotnie, ale jest to zarówno nieeleganckie, jak i pomijają punkt numer 1 (sprawdź, czy jest to Animal). Co gorsza, aby wzmocnić przykład Encapsulation, co by było, gdyby funkcja składowa w klasie potomnej chciała użyć "speak_twice"?

Staje się jeszcze jaśniejsze, jeśli klasa nadrzędna ma członka danych, na przykład "number_of_legs"który jest używany przez nieabstrakcyjne metody w przodku "print_number_of_legs", ale jest inicjowany w konstruktorze klasy podrzędnej (np. Dog zainicjowałby ją 4, podczas gdy Snake zainicjowałby to z 0).

Ponownie, jestem pewien, że jest nieskończenie więcej przykładów, ale w zasadzie każde (wystarczająco duże) oprogramowanie oparte na solidnym projekcie obiektowym będzie wymagało dziedziczenia.

Roee Adler
źródło
3
W pierwszym przypadku oznaczałoby to, że sprawdzasz typy zamiast zachowania, co jest trochę nieszablonowe. W drugim przypadku zgadzam się, i zasadniczo stosujesz podejście „ramowe”. Odzyskujesz implementację speak_twice, nie tylko interfejs, ale aby nadpisać, możesz żyć bez dziedziczenia, jeśli weźmiesz pod uwagę Pythona.
Stefano Borini
9
Możesz żyć bez wielu rzeczy, takich jak klasy i funkcje, ale pytanie brzmi, co sprawia, że ​​kod jest świetny. Myślę, że tak jest w przypadku dziedziczenia.
Roee Adler
@Stefano Borini - Wygląda na to, że przyjmujesz podejście bardzo „oparte na zasadach”. Jednak stary frazes jest prawdziwy: zostały stworzone, aby je złamać. :-)
Jason Baker
@Jason Baker - Zwykle lubię zasady, ponieważ opisują mądrość zdobytą na podstawie doświadczenia (np. Błędy), ale nie lubię, gdy utrudniają one kreatywność. Więc zgadzam się z twoim stwierdzeniem.
Stefano Borini
1
Nie uważam tego przykładu za tak jasny - zwierzęta, samochody i przykłady kształtów naprawdę są do niczego dla tych dyskusji :) Jedyną rzeczą, która ma znaczenie dla IMHO, jest to, czy chcesz odziedziczyć implementację, czy nie. Jeśli tak, reguły w Pythonie są bardzo podobne do java / C ++; różnica dotyczy głównie dziedziczenia dla interfejsów. W takim przypadku pisanie na klawiaturze jest często rozwiązaniem - znacznie większym niż dziedziczenie.
David Cournapeau
12

Dziedziczenie w Pythonie polega na ponownym wykorzystaniu kodu. Uwzględnij wspólną funkcjonalność w klasie bazowej i zaimplementuj różne funkcje w klasach pochodnych.

Roberto Bonvallet
źródło
11

Dziedziczenie w Pythonie jest wygodniejsze niż cokolwiek innego. Uważam, że najlepiej jest zapewnić klasę „zachowanie domyślne”.

Rzeczywiście, istnieje znacząca społeczność programistów Pythona, którzy w ogóle sprzeciwiają się używaniu dziedziczenia. Cokolwiek robisz, nie przesadzaj. Posiadanie zbyt skomplikowanej hierarchii klas jest pewnym sposobem na otrzymanie etykiety „programista Java”, a tego po prostu nie można. :-)

Jason Baker
źródło
8

Myślę, że celem dziedziczenia w Pythonie nie jest skompilowanie kodu, ale prawdziwym powodem dziedziczenia jest rozszerzenie klasy na inną klasę potomną i przesłonięcie logiki w klasie bazowej. Jednak pisanie typu kaczego w Pythonie sprawia, że ​​koncepcja "interfejsu" jest bezużyteczna, ponieważ możesz po prostu sprawdzić, czy metoda istnieje przed wywołaniem, bez konieczności używania interfejsu w celu ograniczenia struktury klasy.

bashmohandes
źródło
3
Przyczyną dziedziczenia jest nadpisywanie selektywne. Jeśli zamierzasz wszystko przesłonić, to dziwny przypadek specjalny.
S.Lott
1
Kto by wszystko przesłonił? możesz myśleć o Pythonie tak, jakby wszystkie metody były publiczne i wirtualne
bashmohandes
1
@bashmohandes: Nigdy nie zastąpiłbym wszystkiego. Ale pytanie pokazuje zdegenerowany przypadek, w którym wszystko zostaje unieważnione; ten dziwny, szczególny przypadek jest podstawą pytania. Ponieważ nigdy nie zdarza się to w normalnym projekcie OO, pytanie jest trochę bezcelowe.
S.Lott
7

Myślę, że przy tak abstrakcyjnych przykładach bardzo trudno jest udzielić sensownej, konkretnej odpowiedzi ...

Aby uprościć, istnieją dwa rodzaje dziedziczenia: interfejs i implementacja. Jeśli musisz dziedziczyć implementację, to Python nie różni się tak bardzo od języków OO ze statycznym typowaniem, takich jak C ++.

Dziedziczenie interfejsu jest tam, gdzie istnieje duża różnica, z fundamentalnymi konsekwencjami dla projektowania oprogramowania z mojego doświadczenia. Języki takie jak Python nie zmuszają cię do używania dziedziczenia w takim przypadku, a unikanie dziedziczenia jest w większości przypadków dobrym punktem, ponieważ później bardzo trudno jest naprawić zły wybór projektu. To dobrze znana kwestia poruszona w każdej dobrej książce OOP.

Są przypadki, w których używanie dziedziczenia dla interfejsów jest zalecane w Pythonie, na przykład dla wtyczek itp. W takich przypadkach, Python 2.5 i starsze nie mają "wbudowanego" eleganckiego podejścia, a kilka dużych frameworków zaprojektowało własne rozwiązania (zope, trac, twister). Python 2.6 i nowsze wersje mają klasy ABC do rozwiązania tego problemu .

David Cournapeau
źródło
6

To nie dziedziczenie sprawia, że ​​pisanie na klawiaturze jest bezcelowe, ale interfejsy - takie jak ten, który wybrałeś, tworząc całkowicie abstrakcyjną klasę zwierząt.

Gdybyś użył klasy zwierząt, która wprowadza pewne prawdziwe zachowania dla swoich potomków do wykorzystania, to klasy psów i kotów, które wprowadzają dodatkowe zachowania, byłyby powodem dla obu klas. Tylko w przypadku, gdy klasa nadrzędna nie wnosi żadnego kodu do klas potomnych, twój argument jest poprawny.

Ponieważ Python może bezpośrednio znać możliwości dowolnego obiektu i ponieważ te możliwości są modyfikowalne poza definicją klasy, pomysł wykorzystania czysto abstrakcyjnego interfejsu do „informowania” programu o tym, jakie metody można wywołać, jest nieco bezcelowy. Ale to nie jedyny, ani nawet główny punkt dziedziczenia.

Larry Lustig
źródło
5

W C ++ / Java / etc polimorfizm jest spowodowany dziedziczeniem. Porzuć tę błędną wiarę, a otworzą się przed tobą dynamiczne języki.

Zasadniczo w Pythonie nie ma interfejsu tak bardzo, jak „zrozumienie, że pewne metody są wywoływalne”. Całkiem faliste i akademickie, nie? Oznacza to, że ponieważ nazywasz „mów”, wyraźnie oczekujesz, że obiekt powinien mieć metodę „mów”. Proste, co? Jest to bardzo Liskov-ian, ponieważ użytkownicy klasy definiują jej interfejs, dobrą koncepcję projektową, która prowadzi do zdrowszego TDD.

Pozostaje więc, jak grzecznie udało się uniknąć innego plakatu, sztuczka polegająca na dzieleniu się kodem. Możesz zapisać to samo zachowanie w każdej klasie „podrzędnej”, ale byłoby to zbędne. Łatwiejsze dziedziczenie lub łączenie funkcji, które są niezmienne w całej hierarchii dziedziczenia. Mniejszy kod DRY-er jest ogólnie lepszy.

agileotter
źródło
2

Nie widzę sensu w dziedziczeniu.

Za każdym razem, gdy korzystałem z dziedziczenia w prawdziwych systemach, spaliłem się, ponieważ doprowadziło to do splątanej sieci zależności lub po prostu zdałem sobie sprawę, że bez niego byłoby o wiele lepiej. Teraz unikam tego tak bardzo, jak to możliwe. Po prostu nigdy nie mam z tego pożytku.

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

Jamesowi Goslingowi zadano kiedyś na konferencji prasowej pytanie w rodzaju: „Gdybyś mógł wrócić i zrobić Javę inaczej, co byś pominął?”. Jego odpowiedzią były „Zajęcia”, do których doszedł śmiech. Jednak był poważny i wyjaśnił, że tak naprawdę to nie zajęcia były problemem, ale dziedziczenie.

Traktuję to jak uzależnienie od narkotyków - daje szybkie rozwiązanie, które jest dobre, ale w końcu to cię zepsuło. Rozumiem przez to, że jest to wygodny sposób ponownego wykorzystania kodu, ale wymusza niezdrowe powiązanie między klasą dziecka i rodzica. Zmiany rodzica mogą złamać dziecko. Dziecko jest zależne od rodzica w zakresie pewnych funkcji i nie może ich zmieniać. Dlatego funkcjonalność, którą zapewnia dziecko, jest również powiązana z rodzicem - możesz mieć tylko jedno i drugie.

Lepiej jest zapewnić jedną klasę dla klienta dla interfejsu, który implementuje interfejs, używając funkcjonalności innych obiektów, które są tworzone w czasie budowy. Robiąc to za pomocą odpowiednio zaprojektowanych interfejsów, można wyeliminować wszystkie sprzężenia, a my zapewniamy wysoce komponowalne API (to nic nowego - większość programistów już to robi, po prostu za mało). Zwróć uwagę, że klasa implementująca nie może po prostu ujawniać funkcjonalności, w przeciwnym razie klient powinien po prostu używać klas skomponowanych bezpośrednio - musi zrobić coś nowego, łącząc tę ​​funkcjonalność.

Obóz dziedziczenia argumentuje, że implementacje czystej delegacji cierpią, ponieważ wymagają wielu metod „klejenia”, które po prostu przekazują wartości przez „łańcuch” delegacji. Jest to jednak po prostu ponowne wymyślenie projektu przypominającego dziedziczenie przy użyciu delegacji. Programiści ze zbyt długim kontaktem z projektami opartymi na dziedziczeniu są szczególnie podatni na wpadnięcie w tę pułapkę, ponieważ nie zdając sobie z tego sprawy, pomyślą o tym, jak zaimplementowaliby coś przy użyciu dziedziczenia, a następnie przekształciliby to w delegację.

Właściwe oddzielenie problemów, takich jak powyższy kod, nie wymaga metod klejenia, ponieważ każdy krok w rzeczywistości dodaje wartość , więc nie są to w ogóle metody „klejowe” (jeśli nie dodają wartości, projekt jest wadliwy).

Sprowadza się do tego:

  • W przypadku kodu wielokrotnego użytku każda klasa powinna robić tylko jedną rzecz (i robić to dobrze).

  • Dziedziczenie tworzy klasy, które robią więcej niż jedną rzecz, ponieważ są pomieszane z klasami nadrzędnymi.

  • Dlatego używanie dziedziczenia sprawia, że ​​klasy, które są trudne do ponownego użycia.

Mike A.
źródło
1

Dziedziczenie można obejść w Pythonie i prawie każdym innym języku. Chodzi jednak o ponowne wykorzystanie kodu i jego uproszczenie.

To tylko semantyczna sztuczka, ale po zbudowaniu klas i klas podstawowych nie musisz nawet wiedzieć, co jest możliwe z twoim obiektem, aby zobaczyć, czy możesz to zrobić.

Powiedzmy, że masz psa, który jest podklasą Animal.

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

Jeśli wszystko, co wpisał użytkownik, jest dostępne, kod uruchomi odpowiednią metodę.

Używając tego możesz stworzyć dowolną kombinację hybrydy ssaka / gada / ptaka, jaką chcesz, a teraz możesz sprawić, że powie "Kora!" lecąc i wystawiając rozwidlony język, a on sobie z nim poradzi! Baw się dobrze!

mandroid
źródło
1

Kolejną małą kwestią jest to, że trzeci przykład opa, nie możesz wywołać isinstance (). Na przykład przekazanie twojego trzeciego przykładu do innego obiektu, który przyjmuje i wpisz wywołania "Animal", aby mówić na nim. Jeśli to zrobisz, nie musisz sprawdzać typu psa, kota i tak dalej. Nie jestem pewien, czy sprawdzanie instancji to naprawdę „Pythonic” z powodu późnego wiązania. Ale wtedy musiałbyś wprowadzić jakiś sposób, aby AnimalControl nie próbował wrzucać typów Cheeseburgerów do ciężarówki, ponieważ Cheeseburgery nie mówią.

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"
yedevtxt
źródło
0

Klasy w Pythonie to po prostu sposoby grupowania wielu funkcji i danych .. Różnią się one od klas w C ++ i tym podobnych.

Najczęściej widziałem dziedziczenie używane do nadpisywania metod superklasy. Na przykład, być może bardziej Pythonowe użycie dziedziczenia byłoby ...

from world.animals import Dog

class Cat(Dog):
    def speak(self):
        print "meow"

Oczywiście koty nie są typem psa, ale mam to (osoby trzeciej) Dogklasę, która działa doskonale, z wyjątkiem w speaksposób, który chcę zastąpić - to oszczędza ponownego wdrażania całą klasę, tak że miauczy. Ponownie, while Catnie jest typem Dog, ale kot dziedziczy wiele atrybutów.

O wiele lepszym (praktycznym) przykładem przesłonięcia metody lub atrybutu jest sposób zmiany klienta użytkownika dla urllib. Zasadniczo podklasujesz urllib.FancyURLopeneri zmieniasz atrybut wersji ( z dokumentacji ):

import urllib

class AppURLopener(urllib.FancyURLopener):
    version = "App/1.7"

urllib._urlopener = AppURLopener()

Innym sposobem używania wyjątków jest wyjątek, gdy dziedziczenie jest używane w bardziej „właściwy” sposób:

class AnimalError(Exception):
    pass

class AnimalBrokenLegError(AnimalError):
    pass

class AnimalSickError(AnimalError):
    pass

.. możesz następnie złapać, AnimalErroraby złapać wszystkie wyjątki, które dziedziczą po niej, lub konkretny, podobny AnimalBrokenLegError

dbr
źródło
6
Jestem… trochę zdezorientowany twoim pierwszym przykładem. Kiedy ostatnio sprawdzałem, koty nie są rodzajem psa, więc nie jestem pewien, jaki związek próbujesz pokazać. :-)
Ben Blank,
1
Mylisz się z zasadą Liskova: Kot NIE JEST psem. W tym przypadku użycie może być w porządku, ale co się stanie, jeśli klasa Dog zmieni się i pojawi się na przykład pole „Lead”, które jest bezsensowne dla kotów?
Dmitry Risenberg
1
Cóż, jeśli nie ma klasy bazowej Animal, alternatywą jest ponowne zaimplementowanie całości. Nie mówię, że jest to najlepsza praktyka (jeśli istnieje klasa podstawowa Animal, użyj jej), ale działa i jest powszechnie używany ( jest to zalecany sposób zmiany agenta użytkownika urllib, zgodnie z przykładem, który dodałem)
dbr