najlepsze praktyki dotyczące funkcji fabryki w Pythonie

30

Załóżmy, że mam plik foo.pyzawierający klasę Foo:

class Foo(object):
   def __init__(self, data):
      ...

Teraz chcę dodać funkcję, która tworzy Fooobiekt w określony sposób z surowych danych źródłowych. Czy powinienem umieścić go jako metodę statyczną w Foo, czy jako inną osobną funkcję?

class Foo(object):
   def __init__(self, data):
      ...
# option 1:
   @staticmethod
   def fromSourceData(sourceData):
      return Foo(processData(sourceData))

# option 2:
def makeFoo(sourceData):
   return Foo(processData(sourceData))

Nie wiem, czy ważniejsza jest wygoda dla użytkowników:

foo1 = foo.makeFoo(sourceData)

lub czy ważniejsze jest zachowanie wyraźnego sprzężenia między metodą a klasą:

foo1 = foo.Foo.fromSourceData(sourceData)
Jason S.
źródło

Odpowiedzi:

45

Zamiast tego należy wybrać między funkcją fabryczną a trzecią opcją; metoda klasy:

class Foo(object):
   def __init__(self, data):
      ...

   @classmethod
   def fromSourceData(klass, sourceData):
      return klass(processData(sourceData))

Fabryki Classmethod mają tę dodatkową zaletę, że są dziedziczne. Możesz teraz utworzyć podklasę Foo, a odziedziczona metoda fabryczna wytworzyłaby instancje tej podklasy.

Mając wybór między funkcją a metodą klasową, wybrałbym metodę klasową. Dokumentuje całkiem wyraźnie, co rodzaj obiektu fabryka będzie produkować, bez konieczności dokonywania to wyraźne w nazwie funkcji. Staje się to znacznie wyraźniejsze, gdy masz nie tylko wiele metod fabrycznych, ale także wiele klas.

Porównaj następujące dwie alternatywy:

foo.Foo.fromFrobnar(...)
foo.Foo.fromSpamNEggs(...)
foo.Foo.someComputedVersion()
foo.Bar.fromFrobnar(...)
foo.Bar.someComputedVersion()

vs.

foo.createFooFromFrobnar()
foo.createFooFromSpamNEggs()
foo.createSomeComputedVersionFoo()
foo.createBarFromFrobnar()
foo.createSomeComputedVersionBar()

Co więcej, użytkownik końcowy może zaimportować tylko Fooklasę i używać jej do różnych sposobów jej tworzenia, ponieważ wszystkie metody fabryczne znajdują się w jednym miejscu:

from foo import Foo
Foo()
Foo.fromFrobnar(...)
Foo.fromSpamNEggs(...)
Foo.someComputedVersion()

datetimeModuł stdlib intensywnie korzysta z fabryk metod klas, a jego interfejs API jest dla niego znacznie bardziej przejrzysty.

Martijn Pieters
źródło
Świetna odpowiedź. Aby uzyskać więcej informacji @classmethod, zobacz Znaczenie @classmethod i @staticmethod dla początkujących?
nealmcb
Duży nacisk na funkcjonalność użytkownika końcowego
drtf
1

W Pythonie zazwyczaj wolę hierarchię klas niż metody fabryczne. Coś jak:

class Foo(object):
    def __init__(self, data):
        pass

    def method(self, param):
        pass

class SpecialFoo(Foo):
    def __init__(self, param1, param2):
        # Some processing.
        super().__init__(data)

class FromFile(Foo):
    def __init__(self, path):
        # Some processing.
        super().__init__(data)

Powiedzmy, że ustawię całą hierarchię (i tylko tę) w module foos.py. Klasa podstawowa Foozwykle implementuje wszystkie metody (i jako taki interfejs) i zwykle nie jest konstruowana bezpośrednio przez użytkowników. Podklasy to sposób na zastąpienie konstruktora podstawowego i określenie, jak Fooma zostać zbudowany. Następnie utworzę a Foo, wywołując odpowiedni konstruktor, na przykład:

foo1 = mypackage.foos.SpecialFoo(param1, param2)
foo2 = mypackage.foos.FromFile('/path/to/foo')

Uważam, że jest bardziej elegancki i elastyczny niż fabryki zbudowane z metod statycznych, metod klasowych lub funkcji na poziomie modułu.

mdeff
źródło