Ведущее подчеркивание в именах классов Python

Этот вопрос основывается на этом предыдущем вопросе, а также этот, но затрагивает более тонкую точку, в частности, что считается "внутренним классом"?

Вот моя ситуация. Я создаю библиотеку Python для управления файлами MS Office, в которых многие из классов не предназначены для создания извне, но многие из их методов являются важными частями API. Например:

class _Slide(object):
    def add_shape(self):
        ...

_Slide не должен быть создан извне, поскольку конечным пользователем библиотеки вы получаете новый слайд, вызывая Presentation.add_slide(). Однако, как только у вас есть слайд, вы обязательно захотите позвонить add_shape(). Таким образом, этот метод является частью API, но конструктор - нет. Эта ситуация возникает десятки раз в библиотеке, потому что только класс Presentation имеет внешний конструктор /API.

PEP8 является двусмысленным в этой точке, ссылаясь на "внутренние классы", не уточняя, что считается внутренним классом. В этом случае, я полагаю, можно сказать, что класс "частично внутренне".

Проблема состоит в том, что у меня есть только две записи для различения по крайней мере трех разных ситуаций:

  • Конструктор и методы "public/API" класса доступны для использования.
  • Конструктор не должен использоваться, но методы "public/API" этого класса.
  • Класс действительно является внутренним, не имеет публичного API, и я оставляю за собой право изменять или удалять его в будущей версии.

Я отмечаю, что с точки зрения коммуникации существует конфликт между четким выражением внешнего API (аудитория конечного пользователя библиотеки) и внутреннего API (аудитория разработчика). Для конечного пользователя вызов _Slide() является verboten/at-own-risk. Однако он является счастливым членом внутреннего API и отличается от _random_helper_method(), который должен вызываться только из _Slide().

У вас есть точка зрения на этот вопрос, который может мне помочь? Является ли конвенция диктуем, что я использую свои боеприпасы "одного лидера-подчёркивания" в именах классов, чтобы бороться за ясность в API конечных пользователей, или я могу чувствовать себя хорошо о том, чтобы зарезервировать его для общения со мной и другими разработчиками о том, когда API класса действительно закрытый и не используемый, например, объектами вне модуля, в котором он живет, поскольку это может быть изменена деталь реализации.


ОБНОВЛЕНИЕ:. После нескольких лет дальнейшего размышления я согласился с соглашением об использовании главного подчеркивания при назначении классов имен, которые не предназначены для доступа к классу вне их модуля (файл). Такой доступ обычно заключается в создании экземпляра объекта этого класса или для доступа к методу класса и т.д.

Это предоставляет пользователям модуль (часто сам, конечно): базовый индикатор: "Если в заявлении импорта появляется имя класса с ведущим подчеркиванием, вы делаете что-то неправильно".

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

Это соглашение также имеет побочное преимущество, не включающее эти классы при создании инструкции from module import *. Хотя я никогда не использую их в своем собственном коде и не рекомендую их избегать, это соответствует поведению, потому что эти идентификаторы классов не предназначены для частичного интерфейса модуля.

Это моя личная "лучшая практика" после нескольких лет испытаний, а не, конечно, "правильный" путь. Ваш пробег может отличаться.

Ответ 1

Я не могу сказать только из описания в вашем вопросе, но из дополнительной информации, предоставленной вами в комментарии, я думаю, что ваш класс Slide фактически открыт.

Это верно, несмотря на то, что экземпляры будут создаваться только косвенно, вызывая метод add_slide() для Presentation, потому что вызывающий пользователь будет свободен (и, более вероятно, необходим), чтобы вызвать методы экземпляра для последующего манипулирования им, На мой взгляд, по-настоящему частный класс будут доступны только с помощью методов класса, который "владел" им.

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

Ответ 2

Я думаю, что ответ martineau - хороший, безусловно, самый простой и, без сомнения, самый pythonic.

Это, однако, ни в коем случае не единственный вариант.

Часто используемый метод состоит в том, чтобы определять общедоступные методы как часть типа интерфейса; например, zope.interface.Interface широко используется в скрученной структуре. Современный питон, вероятно, использовал бы abc.ABCMeta для того же эффекта. По существу, публичный метод Presentation.add_slide документируется как возвращающий некоторый экземпляр AbstractSlide, который имеет более общие методы; но так как невозможно напрямую создать экземпляр AbstractSlide, нет никакого общедоступного способа его создания.

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

Другой вариант; поскольку только общедоступный способ создания экземпляров класса слайдов осуществляется через Presentation.add_slide; вы могли бы сделать это буквально конструктором. Такие, вероятно, потребуют некоторых метаклассов shenanegans, что-то вроде этого должно делать.

from functools import partial

class InstanceConstructor(type):
    def __get__(cls, instance, owner):
        if instance is not None:
            return partial(cls, instance)
        return cls

И просто определите поведение add_slide в __init__ класса:

>>> class Presentation(object):
...     def __init__(self):
...         self.slides = []
...
...     class add_slide(object):
...         __metaclass__ = InstanceConstructor
...
...         def __init__(slide, presentation, color):
...             slide.color = color
...             presentation.slides.append(slide)
...
>>> p = Presentation()
>>> p.add_slide('red')
<instance_ctor.add_slide object at ...>
>>> p.slides
[<instance_ctor.add_slide object at ...>]
>>> p.slides[0].color
'red'

Ответ 3

Что такое "внутренний" класс, это вопрос конвенции, поэтому PEP не определял этот термин, но если вы подвергаете его своим пользователям (предоставляя методы, которые возвращают этот класс, или полагаются на то, что этот класс прошел) это не совсем "внутренне" вообще.

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

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

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

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

Ответ 4

__all__ = ['Presentation'] в модуле скрываются все классы от таких функций, как help(), кроме класса Presentation. Но разработчики все еще имеют доступ к конструкторам вызовов и методам извне.