Kiedy należy używać klas w Pythonie?

177

Programuję w Pythonie od około dwóch lat; głównie dane (pandy, mpl, numpy), ale także skrypty automatyzacji i małe aplikacje internetowe. Staram się zostać lepszym programistą i poszerzyć swoją wiedzę o Pythonie, a jedną z rzeczy, która mnie niepokoi, jest to, że nigdy nie korzystałem z klasy (poza kopiowaniem losowego kodu kolby dla małych aplikacji internetowych). Ogólnie rozumiem, czym one są, ale nie potrafię zrozumieć, dlaczego potrzebowałbym ich zamiast prostej funkcji.

Aby dodać szczegółowości do mojego pytania: piszę mnóstwo zautomatyzowanych raportów, które zawsze obejmują pobieranie danych z wielu źródeł danych (mongo, sql, postgres, apis), przeprowadzanie dużej lub małej ilości danych i formatowanie, zapisywanie danych do csv / excel / html, wyślij go w e-mailu. Skrypty obejmują od ~ 250 do ~ 600 linii. Czy byłby jakiś powód, dla którego miałbym używać klas do tego i dlaczego?

metry
źródło
15
nie ma nic złego w kodowaniu bez klas, jeśli możesz lepiej zarządzać swoim kodem. Programiści OOP mają tendencję do wyolbrzymiania problemów z powodu ograniczeń wynikających z projektu języka lub powierzchownego zrozumienia różnych wzorców.
Jason Hu

Odpowiedzi:

133

Klasy są filarem programowania obiektowego . OOP jest bardzo zainteresowany organizacją kodu, możliwością ponownego użycia i hermetyzacją.

Po pierwsze, zastrzeżenie: OOP jest częściowo w przeciwieństwie do programowania funkcjonalnego , które jest innym paradygmatem często używanym w Pythonie. Nie każdy, kto programuje w Pythonie (a na pewno w większości języków) używa OOP. W Javie 8 można wiele zrobić, ale nie jest zorientowane obiektowo. Jeśli nie chcesz korzystać z OOP, nie rób tego. Jeśli piszesz tylko jednorazowe skrypty do przetwarzania danych, których nigdy więcej nie użyjesz, pisz dalej tak, jak jesteś.

Istnieje jednak wiele powodów, dla których warto używać OOP.

Niektóre powody:

  • Organizacja: OOP definiuje dobrze znane i standardowe sposoby opisywania i definiowania zarówno danych, jak i procedur w kodzie. Zarówno dane, jak i procedura mogą być przechowywane na różnych poziomach definicji (w różnych klasach) i istnieją standardowe sposoby mówienia o tych definicjach. Oznacza to, że jeśli używasz OOP w standardowy sposób, pomoże to Tobie i innym później zrozumieć, edytować i używać Twojego kodu. Ponadto, zamiast używać złożonego, arbitralnego mechanizmu przechowywania danych (dykty dyktowania lub listy, dykty lub listy dykt zbiorów itp.), Możesz nazwać fragmenty struktur danych i wygodnie się do nich odwoływać.

  • Stan: OOP pomaga definiować i śledzić stan. Na przykład w klasycznym przykładzie, jeśli tworzysz program, który przetwarza uczniów (na przykład program ocen), możesz przechowywać wszystkie potrzebne informacje o nich w jednym miejscu (imię i nazwisko, wiek, płeć, poziom kursy, oceny, nauczyciele, rówieśnicy, dieta, specjalne potrzeby itp.), a dane te są przechowywane tak długo, jak długo obiekt żyje i jest łatwo dostępny.

  • Hermetyzacja : w przypadku hermetyzacji procedura i dane są przechowywane razem. Metody (termin OOP dla funkcji) są zdefiniowane wraz z danymi, na których działają i które generują. W języku takim jak Java, który umożliwia kontrolę dostępu , lub w Pythonie, w zależności od tego, jak opisujesz swój publiczny interfejs API, oznacza to, że metody i dane mogą być ukryte przed użytkownikiem. Oznacza to, że jeśli potrzebujesz lub chcesz zmienić kod, możesz zrobić wszystko, co chcesz, z implementacją kodu, ale zachowaj te same publiczne interfejsy API.

  • Dziedziczenie : Dziedziczenie umożliwia zdefiniowanie danych i procedur w jednym miejscu (w jednej klasie), a następnie zastąpienie lub rozszerzenie tej funkcjonalności później. Na przykład w Pythonie często widzę ludzi tworzących podklasy dictklasy w celu dodania dodatkowej funkcjonalności. Typowa zmiana polega na nadpisaniu metody, która zgłasza wyjątek, gdy żądany jest klucz ze słownika, który nie istnieje, w celu podania wartości domyślnej na podstawie nieznanego klucza. Pozwala to na rozszerzenie własnego kodu teraz lub później, zezwolenie innym na rozszerzenie Twojego kodu i pozwala na rozszerzenie kodu innych osób.

  • Wielokrotne wykorzystanie: wszystkie te i inne powody pozwalają na większe możliwości ponownego wykorzystania kodu. Kod zorientowany obiektowo umożliwia jednorazowe napisanie solidnego (przetestowanego) kodu, a następnie jego wielokrotne używanie. Jeśli potrzebujesz dostosować coś do swojego konkretnego przypadku użycia, możesz dziedziczyć z istniejącej klasy i nadpisać istniejące zachowanie. Jeśli chcesz coś zmienić, możesz to wszystko zmienić, zachowując istniejące sygnatury metod publicznych i nikt nie jest mądrzejszy (miejmy nadzieję).

Ponownie, istnieje kilka powodów, aby nie używać OOP i nie musisz tego robić. Ale na szczęście w przypadku języka takiego jak Python, możesz używać tylko trochę lub dużo, to zależy od Ciebie.

Przykład użycia studenta (brak gwarancji jakości kodu, tylko przykład):

Zorientowany obiektowo

class Student(object):
    def __init__(self, name, age, gender, level, grades=None):
        self.name = name
        self.age = age
        self.gender = gender
        self.level = level
        self.grades = grades or {}

    def setGrade(self, course, grade):
        self.grades[course] = grade

    def getGrade(self, course):
        return self.grades[course]

    def getGPA(self):
        return sum(self.grades.values())/len(self.grades)

# Define some students
john = Student("John", 12, "male", 6, {"math":3.3})
jane = Student("Jane", 12, "female", 6, {"math":3.5})

# Now we can get to the grades easily
print(john.getGPA())
print(jane.getGPA())

Standardowy dykt

def calculateGPA(gradeDict):
    return sum(gradeDict.values())/len(gradeDict)

students = {}
# We can set the keys to variables so we might minimize typos
name, age, gender, level, grades = "name", "age", "gender", "level", "grades"
john, jane = "john", "jane"
math = "math"
students[john] = {}
students[john][age] = 12
students[john][gender] = "male"
students[john][level] = 6
students[john][grades] = {math:3.3}

students[jane] = {}
students[jane][age] = 12
students[jane][gender] = "female"
students[jane][level] = 6
students[jane][grades] = {math:3.5}

# At this point, we need to remember who the students are and where the grades are stored. Not a huge deal, but avoided by OOP.
print(calculateGPA(students[john][grades]))
print(calculateGPA(students[jane][grades]))
dantiston
źródło
Ze względu na „wydajność” hermetyzacja Pythona jest często czystsza w przypadku generatorów i menedżerów kontekstu niż w przypadku klas.
Dmitry Rubanovich
4
@meter dodałem przykład. Mam nadzieję, że to pomoże. Uwaga tutaj jest taka, że ​​zamiast polegać na kluczach twoich dykt o poprawnej nazwie, interpreter Pythona tworzy to ograniczenie za ciebie, jeśli pomylisz się i zmusi cię do użycia zdefiniowanych metod (chociaż nie zdefiniowanych pól (chociaż Java i inne Języki OOP nie pozwalają na definiowanie pól poza klasami, takimi jak Python)).
dantiston
5
@meter również jako przykład hermetyzacji: powiedzmy, że dzisiaj ta implementacja jest w porządku, ponieważ potrzebuję tylko raz w semestrze uzyskać GPA dla 50 000 studentów na mojej uczelni. Teraz jutro otrzymamy stypendium i musimy co sekundę dawać aktualny GPA każdemu uczniowi (oczywiście nikt by o to nie prosił, ale tylko po to, aby było to trudne obliczeniowo). Moglibyśmy wtedy „zapamiętać” GPA i obliczyć go tylko wtedy, gdy się zmieni (na przykład ustawiając zmienną w metodzie setGrade), a inne zwracać wersję z pamięci podręcznej. Użytkownik nadal używa getGPA (), ale implementacja uległa zmianie.
dantiston
4
@dantiston, ten przykład wymaga collections.namedtuple. Możesz utworzyć nowy typ Student = collections.namedtuple ("Student", "imię, wiek, płeć, poziom, oceny"). Następnie możesz utworzyć instancje john = Student („Jan”, 12, „mężczyzna”, oceny = {'matematyka': 3.5}, poziom = 6). Zauważ, że używasz zarówno argumentów pozycyjnych, jak i nazwanych, tak samo jak przy tworzeniu klasy. To jest typ danych, który został już zaimplementowany w Pythonie. Następnie możesz odwołać się do john [0] lub john.name, aby uzyskać pierwszy element krotki. Możesz teraz uzyskać oceny jana jako john.grades.values ​​(). I to już dla ciebie zrobione.
Dmitry Rubanovich
2
dla mnie hermetyzacja jest wystarczającym powodem, aby zawsze używać OOP. Trudno mi dostrzec, że wartość NIE używa OOP dla żadnego rozsądnego projektu kodowania. Chyba potrzebuję odpowiedzi na odwrotne pytanie :)
San Jay
23

Ilekroć musisz utrzymać stan swoich funkcji i nie można tego osiągnąć za pomocą generatorów (funkcji, które dają, a nie zwracają). Generatory zachowują swój własny stan.

Jeśli chcesz przesłonić którykolwiek ze standardowych operatorów, potrzebujesz klasy.

Zawsze, gdy używasz wzorca Visitor, będziesz potrzebować klas. Każdy inny wzorzec projektowy można osiągnąć bardziej efektywnie i czysto za pomocą generatorów, menedżerów kontekstu (które są również lepiej implementowane jako generatory niż jako klasy) i typów POD (słowniki, listy i krotki itp.).

Jeśli chcesz napisać kod „pythonowy”, powinieneś preferować menedżery kontekstu i generatory od klas. Będzie czystszy.

Jeśli chcesz rozszerzyć funkcjonalność, prawie zawsze będziesz w stanie to osiągnąć poprzez zawieranie, a nie dziedziczenie.

Jak każda zasada ma wyjątek. Jeśli chcesz szybko hermetyzować funkcjonalność (tj. Napisać kod testowy zamiast kodu wielokrotnego użytku na poziomie biblioteki), możesz hermetyzować stan w klasie. Będzie to proste i nie będzie wymagało wielokrotnego użytku.

Jeśli potrzebujesz destruktora w stylu C ++ (RIIA), zdecydowanie NIE chcesz używać klas. Chcesz menedżerów kontekstu.

Dmitrij Rubanowicz
źródło
1
Zamknięcia @DmitryRubanovich nie są implementowane przez generatory w Pythonie.
Eli Korvigo,
1
@DmitryRubanovich Miałem na myśli „zamknięcia są implementowane jako generatory w Pythonie”, co nie jest prawdą. Zamknięcia są znacznie bardziej elastyczne. Generatory są zobowiązane do zwracania Generatorinstancji (specjalnego iteratora), podczas gdy zamknięcia mogą mieć dowolny podpis. Zasadniczo możesz uniknąć zajęć przez większość czasu, tworząc domknięcia. A domknięcia to nie tylko „funkcje zdefiniowane w kontekście innych funkcji”.
Eli Korvigo,
3
@Eli Korvigo, w rzeczywistości generatory są znaczącym skokiem syntaktycznym. Tworzą abstrakcję kolejki w taki sam sposób, w jaki funkcje są abstrakcjami stosu. Większość przepływu danych może być połączona ze sobą z prymitywów stosu / kolejki.
Dmitry Rubanovich,
1
@DmitryRubanovich mówimy tutaj o jabłkach i pomarańczach. Mówię, że generatory są przydatne w bardzo ograniczonej liczbie przypadków i nie można ich w żaden sposób traktować jako substytutu dla wywołań stanowych ogólnego przeznaczenia. Mówisz mi, jakie są świetne, nie zaprzeczając moim tezom.
Eli Korvigo,
1
@Eli Korvigo, a ja mówię, że wywołania to tylko uogólnienia funkcji. Które same w sobie są cukrem syntaktycznym w przetwarzaniu stosów. Podczas gdy generatory są cukrem syntaktycznym w przetwarzaniu kolejek. Ale to właśnie ta poprawa składni pozwala na łatwe tworzenie bardziej skomplikowanych konstrukcji z bardziej przejrzystą składnią. Przy okazji, '.next ()' prawie nigdy nie jest używany.
Dmitry Rubanovich,
11

Myślę, że robisz to dobrze. Zajęcia są rozsądne, gdy trzeba symulować jakąś logikę biznesową lub trudne rzeczywiste procesy z trudnymi relacjami. Jako przykład:

  • Kilka funkcji ze stanem udostępniania
  • Więcej niż jedna kopia tych samych zmiennych stanu
  • Aby rozszerzyć zachowanie istniejącej funkcjonalności

Proponuję również obejrzenie tego klasycznego wideo

valignatev
źródło
3
Nie ma potrzeby używania klasy, gdy funkcja wywołania zwrotnego wymaga trwałego stanu w Pythonie. Użycie wydajności Pythona zamiast zwrotu powoduje, że funkcja jest ponownie wprowadzana.
Dmitry Rubanovich
4

Klasa definiuje byt ze świata rzeczywistego. Jeśli pracujesz nad czymś, co istnieje indywidualnie i ma własną logikę, która jest odrębna od innych, powinieneś utworzyć dla tego klasę. Na przykład klasa, która hermetyzuje łączność z bazą danych.

Jeśli tak nie jest, nie ma potrzeby tworzenia klasy

Ashutosh
źródło
0

To zależy od Twojego pomysłu i projektu. jeśli jesteś dobrym projektantem, to OOP wyjdą naturalnie w postaci różnych wzorców projektowych. W przypadku prostego skryptu przetwarzanie OOP może być narzutem. Po prostu rozważ podstawowe zalety OOP, takie jak wielokrotnego użytku i rozszerzalne, i upewnij się, czy są potrzebne, czy nie. OOP sprawiają, że złożone rzeczy stają się prostsze, a prostsze - złożone. Po prostu utrzymuje rzeczy w prosty sposób w dowolny sposób, używając OOP lub nie używając OOP. co jest prostsze, użyj tego.

Mohit Thakur
źródło