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



Ладно, покопаемся.

Получение технических

Язык вокруг ООП может показаться пугающим. Вы видели часть этого языка в примере из части 1, но давайте сделаем его более конкретным. Во-первых, давайте начнем, пожалуй, с самого основного вопроса: в чем разница между class и object?

  • Классыопределение данных и процедур, доступных для данной структуры. Другими словами, класс определяет, к каким данным он относится, и какие процедуры (методы) можно использовать с этими данными.
  • Объекты — конкретные экземпляры классов. Например, в приведенном выше примере вы определили класс Rectangle и создали экземпляр этого класса для создания объекта Rectangle (например, Rectangle(10, 5)).

Также есть некоторые важные различия между class методами и переменными и instance методами и переменными, которые могут повлиять на поведение вашего кода:

  • Переменные экземпляра –это элементы данных, «принадлежащие» каждому экземпляру класса (т. е. объекту, например, переменные length и width в классе Rectangle).
  • Переменные класса — это элементы данных, «принадлежащие» всем экземплярам класса — существует единственная копия для всех экземпляров этого класса.

Чтобы немного прояснить это, рассмотрим разницу между ними:

class NewCircle(Shape): 
    pi = math.pi 
    def __init__(self, radius: float) -> None: 
        self.radius = radius 
    def area(self) -> float: 
        return self.pi * self.radius ** 2

В данном случае pi — это переменная класса, а radius — это переменная экземпляра. На практике pi является общим для всех классов, поэтому, если вы сделаете следующее:

a, b = NewCircle(1), NewCircle(1) 
print(a.area(), b.area()) # 3.141592653589793 3.141592653589793 NewCircle.pi = 3.14 # this changes `pi` on both `a` and `b`. print(a.area(), b.area()) # 3.14 3.14

Вы увидите, что обновление переменной класса с помощью NewCircle.pi изменяет площадь обоих кругов, тогда как:

a, b = NewCircle(1), NewCircle(1) 
print(a.area(), b.area()) # 3.141592653589793 3.141592653589793 
a.pi = 3 # update only the copy of `pi` on the instance `a`. print(a.area(), b.area()) # 3 3.141592653589793

Будет только изменять pi на копии pi, принадлежащей a экземпляру этого класса.

Как насчет методов — вещей, которые работают с данными? Как вы видели в Части 1, методы можно рассматривать как функции, которые являются членами (то есть принадлежат) класса. Есть два особенно важных примера, которые отражают приведенные выше определения переменных:

  • Методы экземпляра. Подобно переменным экземпляра, методы экземпляра «принадлежат» отдельным объектам. Эти методы могут обращаться к данным и методам, инкапсулированным в объект, включая другие методы, переменные экземпляра и переменные класса.
  • Методы класса. Напротив, методы класса – это методы, которые доступны для всех экземпляров класса, но могут обращаться только к другим методам класса и экземплярам класса этого класса.

Методы класса (а также несколько других разновидностей) поддерживаются в Python, но понимание их использования в Python требует четкого понимания нескольких функций языка Python среднего уровня (включая декораторы), так что пример и их обсуждение оставим в будущем посте.

Теперь, чтобы официально представить некоторые из больших идей ООП.

Инкапсуляция

В примере в Часть 1 вы видели определение класса Rectangle. Напомним, у вас было:

class Rectangle(Shape): 
    def __init__(self, length: float, width: float) -> None: 
        self.length = length 
        self.width = width 
    def area(self) -> float: 
        return self.length * self.width 
    def perimeter(self) -> float: 
        return (self.length + self.width) * 2.0

В ООП инкапсуляция относится к объединению данных и функций (методов) в единую структуру (класс). На практике инкапсуляция используется для скрытия состояния объекта. Часть этого сокрытия информации включает в себя определение того, как можно получить доступ к конкретным переменным и методам, чтобы ограничить неправильное использование и обеспечить стабильность. Это источник концепции общедоступных, защищенных и частных «модификаторов доступа», используемых в некоторых языках. Давайте посмотрим, что это значит.

В приведенном здесь примере все переменные экземпляра и методы в Rectangle можно описать как общедоступные — они «видимы» (доступны) для любого кода, взаимодействующего с любым экземпляром Rectangle. Однако что произойдет, если вы решите, что не хотите, чтобы ваши пользователи вмешивались в переменные экземпляра length и width после того, как вы создали экземпляр Rectangle. Один из подходов может заключаться в том, чтобы сделать ваши переменные и методы защищенными или личными. Это предотвратило бы (или препятствовало в некоторых случаях в Python) тому, кто использует Rectangle напрямую, от доступа к length и width. Конкретно, вы можете определить членов класса как имеющих один из трех уровней доступа:

  • Общедоступный — видимый для любого кода, использующего класс.
  • Защищено — видимо только для класса, в котором определен член, и любых подклассов этого класса.
  • Частный —виден только классу, который определил член.

Опять же, другие языки также могут иметь дополнительные модификаторы доступа (или отсутствия доступа). Как это работает в Python? Вот измененная версия вашего фрагмента Rectangle выше:

class Rectangle(Shape): 
    def __init__(self, length: float, width: float) -> None: 
         self._length = length 
         self.__width = width 
    def area(self) -> float: 
         return self._length * self.__width 
 
    def perimeter(self) -> float: 
         return (self._length + self.__width) * 2.0

Этот фрагмент соответствует соглашениям Python, которые теперь указывают, что переменная экземпляра _length является защищенным элементом (т. е. доступным для подклассов), а __widthчастнымчленом (т. е. доступным только для Rectangle). Это означает, что если вы создадите class Square(Rectangle), этот новый класс вообще не сможет использовать переменную width.

Кроме того, вы не должны обращаться к переменной _length в экземплярах Rectangle (например, Rectangle(10, 5)._length). Если вы используете линтер, вы заметите, что он выдаст вам предупреждения, если вы попытаетесь нарушить эти правила. Более того, хотя Python не использует ни защищенные, ни частныечлены традиционным способом, многие языки делают это, и возможность контролировать доступ к указанным членам (даже в более ограниченный подход) может быть полезной особенностью дизайна.

Например, может быть полезно разбить промежуточные этапы вычислений на отдельные защищенныеметоды, но предоставить вашим пользователям только один общедоступныйметод. Другими словами: скройте сведения о реализации, к которым конечные пользователи не должны иметь доступа, и отобразите только те, к которым они должны иметь доступ.

Следовательно, часто бывает полезно сделать только минимальное подмножество методов общедоступными (или, наоборот, вы должны по умолчанию делать переменные и методы защищенными , если только у вас нет особых причин для этого). сделать их общедоступными). Это помогает сделать способы взаимодействия пользователей с классом как можно более узкими, что, в свою очередь, уменьшает поверхностную область API, которую вы им предоставляете, что, как правило, снижает усилия по разработке, необходимые для поддержки и обслуживания. этот API.

Полиморфизм

Давайте еще раз вернемся к рефакторингу примера Shape из Части 1:

class Shape: 
    def area(self) -> float: ... 

class Rectangle(Shape): 
    def __init__(self, length: float, width: float) -> None: ... 
    def area(self) -> float: ... 

class Triangle(Shape): 
    def __init__(self, base: float, height: float) -> None: ... 
    def area(self) -> float: ... 
shapes = [Rectangle(5, 10), Triangle(1, 2)] 
area = 0 
for shape in shapes: 
    area += shape.area()

Этот фрагмент отражает несколько ключевых идей, связанных с концепцией полиморфизма. Технически полиморфизм относится к концепции, согласно которой объекты разных типов могут предоставлять один и тот же интерфейс. В приведенном здесь примере кода Rectangle и Triangle предоставляют один и тот же метод(ы), поэтому код, вызывающий эти методы, может быть безразличен к типу объекта, над которым он работает. Другими словами, вашему циклу по списку shapes нужна только гарантия того, что объекты, с которыми он работает, реализуют интерфейс Shape, и если они это сделают, он всегда будет работать нормально.

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

Конкретно возьмем популярную библиотеку машинного обучения (ML), такую ​​как Scikit-Learn. Если вы когда-либо использовали эту библиотеку, вы, несомненно, знакомы с классическими методами fit и predict (среди прочих), которые характеризуют модели в библиотеке. Этот интерфейс прост и понятен, хотя иногда и ограничен (определяя, чем что-то является, вы также в конечном итоге определяете, чем оно не является, в конце концов) и позволяет пользователям создавать Конвейеры машинного обучения, которые используют его, не беспокоясь о конкретном варианте модели, используемом конвейером (действительно, это именно то, что делают Scikit-Learn Pipelines!).

… определяя, чем что-тоявляется, вы также часто заканчиваете тем, что определяете, чем ононе является, в конце концов.

Следовательно, другие поставщики могут реализовывать версии своих собственных моделей, которые соответствуют этому интерфейсу, который, в свою очередь, может быть немедленно использован в любом конвейере, настроенном для использования моделей Scikit-Learn. Возможно, вы помните, что другие популярные библиотеки, такие как LightGBM, XGBoost и Tensorflow, предоставляют интерфейсы, совместимые со Scikit-Learn. Это одна из причин, почему существует такая динамичная экосистема инструментов, совместимых с Scikit-Learn, и почему этот факт так полезен (и важен) с инженерной точки зрения: он помогает вам отделить логику от того, что вы на самом деле делаете с моделью из сведений о реализации конкретного варианта модели. Это возможно (частично) за счет полиморфизма.

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

Наследование

Третьей важной особенностью ООП является наследование. Ключевая идея здесь заключается в том, что наследование позволяет вам выражать отношения является типом между классами. Например, в примере Shape, который вы видели в Части 1, вы можете выразить отношение class Triangle(Shape) следующим образом: Triangle является типом Shape. Точно так же вы можете выразить class RightTriangle(Triangle) как: RightTriangle является типом Triangle. Затем вы можете заметить, что строите иерархию классов. В случае этого простого примера у вас есть что-то вроде:

Обычно корневой узел в таких иерархических структурах (в данном случае Shape) называется базовым классом. Также довольно часто эти классы бывают абстрактными: они не определяют свою собственную реализацию, а вместо этого определяют интерфейс (и, возможно, частичную реализацию). Абстрактный класс не предназначен для непосредственного создания: он предназначен для создания подклассов. Многие языки активно применяют этот факт и не позволяют вам напрямую создать экземпляр абстрактного класса. Такого поведения можно добиться и в Python. Методы, определенные в этих классах, которые не обеспечивают реализацию (например, area в примере), называются абстрактными методами (или эквивалентно в некоторых языках/контекстах как виртуальные методы).

Чтобы сделать все это немного более конкретным, абстрактный класс можно определить как:

  • Абстрактный класс — класс с одним или несколькими абстрактными методами.

Итак, почему это полезно? Наследование (теоретически) позволяет вам легко расширять и изменять классы, что, в свою очередь, может упростить добавление функций и функций в ваш код. Возьмем приведенный выше пример: вы видели, как класс Triangle был расширен, чтобы быстро и легко реализовать новый метод perimeter для двух новых типов Triangle без необходимости «касаться» родительского класса Triangle.

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

Копать глубже

Этот пост на самом деле лишь коснулся поверхности ООП: это большая область с огромным набором инструментов, идей и реализаций, которые лежат в основе его использования в современном программном проекте. Если вы решите углубиться в мир ООП, вы заметите много общего между языками определенных линий (например, C++, Java), а также немало различий. Некоторые языки и инструменты преднамеренно принимают определенные подмножества функций, обсуждаемых здесь, в то время как другие также реализуют более сложные версии. Если вы потратите время на изучение этих идей — особенно на разных языках, чтобы помочь вам сравнить и сопоставить идеи и подходы — вы обнаружите, что ООП станет бесценным инструментом в вашем наборе инструментов для программирования. Тем не мение…

Слово предупреждения

До сих пор вы видели, как можно использовать ООП, чтобы помочь структурировать и решать проблемы. В опытных руках это мощный инструмент. Однако при неизбирательном использовании ООП может быть проблемным. Неуместное/чрезмерное использование концепций и возможностей ООП может очень легко сыграть против вас. Как это всегда бывает при изучении новых знаний и навыков, люди, плохо знакомые с концепциями ООП, часто попадают в ловушку, установленную «законом молотка»: когда у вас есть молоток, все выглядит как гвоздь.

… люди, плохо знакомые с концепциями ООП, часто попадают в ловушку, установленную «законом молотка»: когда у вас есть молоток, все выглядит как гвоздь.

Практически те же функции инкапсуляции, полиморфизма и наследования, которые вы видели выше, могут увеличить сложность и затруднить отладку, производительность и поддержку вашего кода, если вы используете их без осторожности и предусмотрительности. . Например, чрезмерно сложные и/или плохо спроектированные иерархии классов часто приводят к тому, что команды разработчиков запутываются в узлах — их иерархии классов могут превратиться в большие запутанные структуры, о которых сложно рассуждать и которые технически сложно расширять.

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

Заключительные мысли

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

… хорошее понимание ООП также поможет вам лучше понять и рассуждать о поведении и структуре многих популярных программных сред.

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

дальнейшее чтение





Первоначально опубликовано на https://mark.douthwaite.io 9 октября 2020 г.