Python: удаление атрибута класса в подклассе
У меня есть подкласс, и я хочу, чтобы он не включал атрибут класса, который присутствует в базовом классе.
Я пробовал это, но он не работает:
>>> class A(object):
... x = 5
>>> class B(A):
... del x
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
class B(A):
File "<pyshell#1>", line 2, in B
del x
NameError: name 'x' is not defined
Как я могу это сделать?
Ответы
Ответ 1
Подумайте, почему вы хотите это сделать; вы, вероятно, этого не делаете. Полагайте, что B не наследуется от A.
Идея подклассификации - специализировать объект. В частности, дети класса должны быть действительными экземплярами родительского класса:
>>> class foo(dict): pass
>>> isinstance(foo(), dict)
... True
Если вы реализуете это поведение (например, x = property(lambda: AttributeError)
), вы нарушаете концепцию подкласса, и это плохо.
Ответ 2
Вы можете использовать delattr(class, field_name)
чтобы удалить его из определения класса.
Полный пример:
# python 3
class A:
def __init__(self):
self.field_to_keep = "keep this in B"
self.field_to_delete = "don't want this in B"
class B(A):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
delattr(self, 'field_to_delete')
Ответ 3
Вам не нужно его удалять. Просто отмените это.
class B(A):
x = None
или просто не ссылайтесь на него.
Или рассмотрим другой дизайн (атрибут экземпляра?).
Ответ 4
Возможно, вы могли бы установить x
как property
и поднять AttributeError всякий раз, когда кто-то пытается получить к нему доступ.
>>> class C:
x = 5
>>> class D(C):
def foo(self):
raise AttributeError
x = property(foo)
>>> d = D()
>>> print(d.x)
File "<pyshell#17>", line 3, in foo
raise AttributeError
AttributeError
Ответ 5
Ни один из ответов не сработал для меня.
Например, delattr(SubClass, "attrname")
(или его точный эквивалент, del SubClass.attrname
) не будет "скрывать" родительский метод, потому что это не то, как работает разрешение метода. Вместо этого произойдет сбой с AttributeError('attrname',)
, поскольку у подкласса нет attrname
. И, конечно же, замена атрибута на None
фактически не удаляет его.
Давайте рассмотрим этот базовый класс:
class Spam(object):
# Also try with 'expect = True' and with a '@property' decorator
def expect(self):
return "This is pretty much expected"
Я знаю только два способа подкласса, скрывая атрибут expect
:
-
Использование класса дескриптора, который вызывает AttributeError
из __get__
. При поиске атрибутов будет исключение, обычно неотличимое от ошибки поиска.
Самый простой способ - просто объявить свойство, которое вызывает AttributeError
. По сути, это то, что предложил @JBernardo.
class SpanishInquisition(Spam):
@property
def expect(self):
raise AttributeError("Nobody expects the Spanish Inquisition!")
assert hasattr(Spam, "expect") == True
# assert hasattr(SpanishInquisition, "expect") == False # Fails!
assert hasattr(SpanishInquisition(), "expect") == False
Однако это работает только для экземпляров, а не для классов (hasattr(SpanishInquisition, "expect") == True
утверждение будет нарушено).
Если вы хотите, чтобы все приведенные выше утверждения выполнялись, используйте это:
class AttributeHider(object):
def __get__(self, instance, owner):
raise AttributeError("This is not the attribute you're looking for")
class SpanishInquisition(Spam):
expect = AttributeHider()
assert hasattr(Spam, "expect") == True
assert hasattr(SpanishInquisition, "expect") == False # Works!
assert hasattr(SpanishInquisition(), "expect") == False
Я считаю, что это самый элегантный метод, так как код понятен, универсален и компактен. Конечно, нужно действительно думать дважды, если удаление атрибута является то, что они действительно хотят.
-
Переопределение поиска атрибутов с __getattribute__
магического метода __getattribute__
. Вы можете сделать это либо в подклассе (или в миксине, как в примере ниже, как я хотел написать это только один раз), и это будет скрывать атрибут в экземплярах подкласса. Если вы также хотите скрыть метод от подкласса, вам нужно использовать метаклассы.
class ExpectMethodHider(object):
def __getattribute__(self, name):
if name == "expect":
raise AttributeError("Nobody expects the Spanish Inquisition!")
return super().__getattribute__(name)
class ExpectMethodHidingMetaclass(ExpectMethodHider, type):
pass
# I've used Python 3.x here, thus the syntax.
# For Python 2.x use __metaclass__ = ExpectMethodHidingMetaclass
class SpanishInquisition(ExpectMethodHider, Spam,
metaclass=ExpectMethodHidingMetaclass):
pass
assert hasattr(Spam, "expect") == True
assert hasattr(SpanishInquisition, "expect") == False
assert hasattr(SpanishInquisition(), "expect") == False
Это выглядит хуже (более многословно и менее обобщенно), чем описанный выше метод, но можно рассмотреть и этот подход.
Обратите внимание, что это не работает со специальными ("волшебными") методами (например, __len__
), потому что они обходят __getproperty__
. Обратитесь к разделу Поиск специальных методов документации Python для получения более подробной информации. Если это то, что вам нужно отменить, просто переопределите его и вызовите реализацию object
, пропустив родительский object
.
Излишне говорить, что это относится только к "классам нового стиля" (классам, наследуемым от object
), поскольку там не поддерживаются магические методы и протоколы дескрипторов. Надеюсь, это в прошлом.
Ответ 6
У меня тоже была проблема, и я подумал, что у меня есть веская причина удалить атрибут класса в подклассе: мой суперкласс (назовите его A) имеет свойство только для чтения, которое предоставило значение атрибута, но в моем подклассе (назовите его B) атрибут был переменной экземпляра чтения/записи. Я обнаружил, что Python вызывал функцию свойства, хотя я думал, что переменная экземпляра должна была ее переопределить. Я мог бы сделать отдельную функцию геттера, которая будет использоваться для доступа к базовому свойству, но это казалось ненужным и неэлегантным загромождением пространства имен интерфейса (как будто это действительно имеет значение).
Как оказалось, ответ заключался в том, чтобы создать новый абстрактный суперкласс (назовите его S) с исходными общими атрибутами A и получить A и B из S. Поскольку Python имеет утиную печать, это не имеет большого значения что B не расширяет A, я все еще могу использовать их в тех же местах, поскольку они неявно реализуют один и тот же интерфейс.
Ответ 7
Попытка сделать это, вероятно, плохая идея, но...
Кажется, что это не происходит с помощью "правильного" наследования из-за того, как работает поиск B.x
по умолчанию. При получении B.x
x
сначала просматривается в B
, и если он не найден там, он ищет в A
, но, с другой стороны, при установке или удалении B.x
будет выполняться поиск только B
. Так, например,
>>> class A:
>>> x = 5
>>> class B(A):
>>> pass
>>> B.x
5
>>> del B.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: class B has no attribute 'x'
>>> B.x = 6
>>> B.x
6
>>> del B.x
>>> B.x
5
Здесь мы видим, что сначала мы, кажется, не можем удалить B.x
, поскольку он не существует (A.x
существует, и это то, что получает, когда вы оцениваете B.x
). Однако, установив B.x
в 6, будет существовать B.x
, его можно получить с помощью B.x
и удалить с помощью del B.x
, с помощью которого оно перестает существовать, после чего снова A.x
будет отправлено в ответ на B.x
> .
То, что вы могли делать с другой стороны, - использовать метаклассы, чтобы сделать B.x
raise AttributeError
:
class NoX(type):
@property
def x(self):
raise AttributeError("We don't like X")
class A(object):
x = [42]
class B(A, metaclass=NoX):
pass
print(A.x)
print(B.x)
Теперь, конечно, пуристы могут кричать, что это нарушает LSP, но это не так просто. Все это сводится к тому, что если вы считаете, что вы создали подтип, сделав это. Методы issubclass
и isinstance
говорят "да", но LSP говорит "нет" (и многие программисты предполагают "да", поскольку вы наследуете от A
).
LSP означает, что если B
является подтипом A
, тогда мы могли бы использовать B
всякий раз, когда мы могли бы использовать A
, но поскольку мы не можем этого сделать при выполнении этой конструкции, мы можем заключить, что B
фактически не является подтипом A
, и поэтому LSP не нарушается.