Когда я должен использовать классы в Python?

177

Я программировал на Python около двух лет; в основном данные (pandas, mpl, numpy), а также сценарии автоматизации и небольшие веб-приложения. Я пытаюсь стать лучшим программистом и расширить свои знания Python, и одна из вещей, которые меня беспокоят, это то, что я никогда не использовал классы (за исключением копирования случайного кода фляги для небольших веб-приложений). Я обычно понимаю, что это такое, но не могу понять, почему они нужны мне для выполнения простой функции.

Чтобы добавить конкретности в мой вопрос: я пишу тонны автоматических отчетов, которые всегда включают извлечение данных из нескольких источников данных (mongo, sql, postgres, apis), выполнение большого или небольшого изменения и форматирования данных, запись данных в csv / excel / HTML, отправить его по электронной почте. Сценарии варьируются от ~ 250 строк до ~ 600 строк. Будет ли для меня какая-то причина использовать классы для этого и почему?

metersk
источник
15
нет ничего плохого в коде без классов, если вы можете управлять своим кодом лучше. Программисты ООП, как правило, преувеличивают проблемы из-за ограничений дизайна языка или поверхностного понимания различных шаблонов.
Джейсон Ху

Ответы:

134

Классы являются опорой объектно-ориентированного программирования . ООП очень заботится об организации кода, возможности его повторного использования и инкапсуляции.

Во-первых, отказ от ответственности: ООП частично контрастирует с функциональным программированием , которое является другой парадигмой, часто используемой в Python. Не каждый, кто программирует на Python (или, конечно, большинство языков), использует ООП. В Java 8 вы можете многое сделать, если вы не очень объектно-ориентированы. Если вы не хотите использовать ООП, то не делайте этого. Если вы просто пишете одноразовые сценарии для обработки данных, которые вы никогда не будете использовать снова, продолжайте писать так, как есть.

Однако есть много причин использовать ООП.

Несколько причин:

  • Организация: ООП определяет хорошо известные и стандартные способы описания и определения как данных, так и процедур в коде. Как данные, так и процедуры могут храниться на разных уровнях определения (в разных классах), и существуют стандартные способы говорить об этих определениях. То есть, если вы используете ООП стандартным способом, это поможет вашему самому себе и другим понять, отредактировать и использовать ваш код. Кроме того, вместо использования сложного, произвольного механизма хранения данных (диктов диктов или списков, диктов или списков диктов множеств или чего-либо еще), вы можете называть фрагменты структур данных и удобно ссылаться на них.

  • Состояние: ООП помогает вам определять и отслеживать состояние. Например, в классическом примере, если вы создаете программу, которая обрабатывает учащихся (например, программу оценки), вы можете хранить всю необходимую вам информацию о них в одном месте (имя, возраст, пол, уровень образования, курсы, оценки, учителя, сверстники, диета, особые потребности и т. д.), и эти данные сохраняются, пока объект жив, и легко доступны.

  • Инкапсуляция . С помощью инкапсуляции процедура и данные хранятся вместе. Методы (термин ООП для функций) определяются прямо рядом с данными, с которыми они работают и производят. В таком языке, как Java, который допускает управление доступом , или в Python, в зависимости от того, как вы описываете свой публичный API, это означает, что методы и данные могут быть скрыты от пользователя. Это означает, что если вам нужно или вы хотите изменить код, вы можете делать все, что захотите, для реализации кода, но оставляйте общедоступные API-интерфейсы такими же.

  • Наследование : Наследование позволяет вам определять данные и процедуры в одном месте (в одном классе), а затем переопределять или расширять эту функциональность позже. Например, в Python я часто вижу людей, создающих подклассы dictкласса, чтобы добавить дополнительную функциональность. Общее изменение переопределяет метод, который выдает исключение, когда ключ запрашивается из словаря, который не существует, чтобы дать значение по умолчанию на основе неизвестного ключа. Это позволяет вам расширять свой собственный код сейчас или позже, позволяет другим расширять ваш код и позволяет расширять код других людей.

  • Возможность многократного использования. Все эти и другие причины обеспечивают большую возможность повторного использования кода. Объектно-ориентированный код позволяет вам написать сплошной (проверенный) код один раз, а затем использовать снова и снова. Если вам нужно что-то настроить для конкретного случая использования, вы можете наследовать от существующего класса и перезаписать существующее поведение. Если вам нужно что-то изменить, вы можете изменить все это, поддерживая существующие сигнатуры открытых методов, и никто не станет мудрее (надеюсь).

Опять же, есть несколько причин не использовать ООП, и вам это не нужно. Но, к счастью, с таким языком, как Python, вы можете использовать немного или много, это ваше дело.

Пример использования студентом (без гарантии качества кода, только пример):

Объектно-ориентированный

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())

Стандартный Dict

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
источник
Из-за «доходности» инкапсуляция Python часто чище с генераторами и менеджерами контекста, чем с классами.
Дмитрий Рубанович
4
@meter Я добавил пример. Я надеюсь, что это помогает. Обратите внимание, что вместо того, чтобы полагаться на то, что ключи ваших кодов имеют правильное имя, интерпретатор Python делает это ограничение для вас, если вы ошибаетесь и заставляет вас использовать определенные методы (хотя и не определенные поля (хотя Java и другие). Языки ООП не позволяют вам определять поля вне классов, таких как Python)).
Дантистон
5
@meter также, как пример инкапсуляции: допустим, сегодня эта реализация хороша, потому что мне нужно только получить GPA для 50 000 студентов в моем университете один раз в семестр. Теперь завтра мы получаем грант и должны давать текущий средний балл каждому студенту каждую секунду (конечно, никто бы об этом не просил, но только для того, чтобы сделать его сложным в вычислительном отношении). Затем мы можем «запомнить» GPA и вычислять его только при его изменении (например, путем установки переменной в методе setGrade), а другие возвращать кэшированную версию. Пользователь по-прежнему использует getGPA (), но реализация изменилась.
Дантистон
4
@ Dantiston, этот пример нуждается в коллекциях. Вы можете создать новый тип Student = collection.namedtuple («Студент», «Имя, возраст, пол, уровень, оценки»). И тогда вы можете создавать экземпляры john = Student («Джон», 12 лет, «мужчина», оценки = {'math': 3.5}, уровень = 6). Обратите внимание, что вы используете как позиционные, так и именованные аргументы так же, как при создании класса. Это тип данных, который уже реализован для вас в Python. Затем вы можете обратиться к john [0] или john.name, чтобы получить 1-й элемент кортежа. Теперь вы можете получить оценки Джона как john.grades.values ​​(). И это уже сделано для вас.
Дмитрий Рубанович
2
для меня инкапсуляция - достаточно веская причина всегда использовать ООП. Я изо всех сил пытаюсь увидеть, что значение НЕ использует ООП для какого-либо разумного размера проекта кодирования. Я думаю, мне нужны ответы на обратный вопрос :)
Сан Джей
23

Всякий раз, когда вам нужно поддерживать состояние ваших функций, и это не может быть достигнуто с помощью генераторов (функций, которые дают, а не возвращают). Генераторы поддерживают свое собственное состояние.

Если вы хотите переопределить любой из стандартных операторов, вам нужен класс.

Когда бы вы ни использовали шаблон Visitor, вам понадобятся классы. Любой другой шаблон проектирования может быть выполнен более эффективно и четко с помощью генераторов, менеджеров контекста (которые также лучше реализованы как генераторы, чем как классы) и типов POD (словари, списки и кортежи и т. Д.).

Если вы хотите написать «pythonic» код, вы должны предпочесть контекстные менеджеры и генераторы классам. Это будет чище.

Если вы хотите расширить функциональность, вы почти всегда сможете выполнить ее с помощью сдерживания, а не наследования.

Как и в любом правиле, в этом есть исключение. Если вы хотите быстро инкапсулировать функциональность (т. Е. Писать тестовый код, а не повторно используемый код на уровне библиотеки), вы можете инкапсулировать состояние в классе. Это будет просто и не будет нуждаться в повторном использовании.

Если вам нужен деструктор стиля C ++ (RIIA), вы определенно НЕ хотите использовать классы. Вы хотите, чтобы контекстные менеджеры.

Дмитрий Рубанович
источник
1
Закрытия @DmitryRubanovich не реализованы через генераторы в Python.
Эли Корвиго
1
@DmitryRubanovich Я имел в виду «замыкания реализованы как генераторы в Python», что не соответствует действительности. Закрытия гораздо более гибкие. Генераторы обязаны возвращать Generatorэкземпляр (специальный итератор), а замыкания могут иметь любую подпись. В основном вы можете избегать занятий, создавая замыкания. И замыкания - это не просто «функции, определенные в контексте других функций».
Эли Корвиго
3
@Eli Korvigo, на самом деле, генераторы являются значительным скачком синтаксиса. Они создают абстракцию очереди так же, как функции являются абстракциями стека. И большая часть потока данных может быть объединена из примитивов стека / очереди.
Дмитрий Рубанович
1
@DmitryRubanovich Мы здесь говорим о яблоках и апельсинах. Я говорю, что генераторы полезны в очень ограниченном числе случаев и никоим образом не могут рассматриваться в качестве замены общедоступных вызовов общего назначения. Вы говорите мне, как они хороши, не противореча моим точкам зрения.
Эли Корвиго
1
@ Эли Корвиго, и я говорю, что вызываемые элементы - это только обобщения функций. Которые сами по себе являются синтаксическим сахаром над обработкой стеков. Пока генераторы являются синтаксическим сахаром над обработкой очередей. Но именно это улучшение синтаксиса позволяет легко создавать более сложные конструкции с более четким синтаксисом. '.next ()' почти никогда не используется, кстати.
Дмитрий Рубанович
11

Я думаю, что вы делаете это правильно. Классы разумны, когда вам нужно смоделировать некоторую бизнес-логику или сложные реальные процессы с трудными отношениями. Как пример:

  • Несколько функций с общим состоянием
  • Более одной копии одинаковых переменных состояния
  • Расширить поведение существующего функционала

Я также предлагаю вам посмотреть это классическое видео

valignatev
источник
3
Нет необходимости использовать класс, когда функции обратного вызова требуется постоянное состояние в Python. Использование yield Python вместо return делает функцию повторной.
Дмитрий Рубанович
4

Класс определяет сущность реального мира. Если вы работаете над чем-то, что существует индивидуально и имеет свою собственную логику, которая отделена от других, вы должны создать для нее класс. Например, класс, который инкапсулирует соединение с базой данных.

Если это не так, нет необходимости создавать класс

Ashutosh
источник
0

Это зависит от вашей идеи и дизайна. если вы хороший дизайнер, то ООП естественно появятся в виде различных шаблонов проектирования. Для простого уровня обработки сценариев ООП могут быть накладными. Просто рассмотрите основные преимущества ООП, такие как многоразовые и расширяемые, и убедитесь, что они нужны или нет. ООП делают сложные вещи проще, а вещи проще. Просто все упрощается с использованием ООП или без использования ООП. который когда-либо проще использовать это.

Мохит Тхакур
источник