В качестве примера рассмотрим следующее:
class FooMeta(type):
def __len__(cls):
return 9000
class GoodBar(metaclass=FooMeta):
def __len__(self):
return 9001
class BadBar(metaclass=FooMeta):
@classmethod
def __len__(cls):
return 9002
len(GoodBar) -> 9000
len(GoodBar()) -> 9001
GoodBar.__len__() -> TypeError (missing 1 required positional argument)
GoodBar().__len__() -> 9001
len(BadBar) -> 9000 (!!!)
len(BadBar()) -> 9002
BadBar.__len__() -> 9002
BadBar().__len__() -> 9002
Проблема в том, что len(BadBar)
возвращает 9000 вместо 9002, что является предполагаемым поведением.
Это поведение (отчасти) задокументировано в Модель данных Python — поиск специального метода, но там ничего не говорится о методах классов, и я не очень понимаю взаимодействие с декоратором @classmethod
.
Помимо очевидного решения метакласса (т. е. заменить/расширить FooMeta
), есть ли способ переопределить или расширить функцию метакласса, чтобы len(BadBar) -> 9002
?
Чтобы уточнить, в моем конкретном случае использования я не могу редактировать метакласс, и я не хочу создавать его подкласс и/или создавать свой собственный метакласс, если только это не является единственный возможный способ для этого.
@martineau Я знаю, но я хотел бы избежать этого, если это возможно.
Как вы уже упоминали, len()
найдет __len__
тип объекта, а не сам объект. Итак, len(BadBar)
вызывается как type(BadBar).__len__(BadBar)
, метод FooMeta
__len__
получает экземпляр, который здесь BadBar
, в качестве первого параметра.
class FooMeta(type):
def __len__(cls):
print(cls is BadBar)
return 9000
class BadBar(metaclass=FooMeta):
@classmethod
def __len__(cls):
return 9002
print(len(BadBar)) # True - 9000
Вот как это работает: если вы что-то сделаете с метаклассом __len__
, это повлияет на другие классы с этим метаклассом. Нельзя обобщать.
У вас может быть другой конкретный метакласс для BadBar
и вызвать cls.__len__
внутри __len__
метакласса, если он есть, в противном случае вернуть значение по умолчанию.
class BadBar_meta(type):
def __len__(cls):
if hasattr(cls, "__len__"):
return cls.__len__()
return 9000
class BadBar(metaclass=BadBar_meta):
@classmethod
def __len__(cls):
return 9002
print(len(BadBar)) # 9002
Или, если вы просто хотите переопределить только методы классов классов, вы можете проверить, не определил ли класс cls
уже __len__
как метод класса, если это так, вызовите его вместо этого.
class FooMeta(type):
def __len__(cls):
m = cls.__len__
if hasattr(m, "__self__"):
return cls.__len__()
return 9000
class GoodBar(metaclass=FooMeta):
def __len__(self):
return 9001
class BadBar(metaclass=FooMeta):
@classmethod
def __len__(cls):
return 9002
print(len(GoodBar)) # 9000
print(len(BadBar)) # 9002
Здесь — более надежные подходы к поиску методов класса.
Если у вас нет доступа к метаклассу, вы можете украсить метакласс и исправить обезьяну:
class FooMeta(type):
def __len__(cls):
return 9000
class GoodBar(metaclass=FooMeta):
def __len__(self):
return 9001
class BadBar(metaclass=FooMeta):
@classmethod
def __len__(cls):
return 9002
def decorator(mcls):
org_len = mcls.__len__
def custom_len(cls):
# check too see if it is a classmethod
if hasattr(cls.__len__, "__self__"):
return cls.__len__()
return org_len(cls)
FooMeta.__len__ = custom_len
return mcls
FooMeta = decorator(FooMeta)
print(len(GoodBar)) # 9000
print(len(BadBar)) # 9002
Я экспериментировал с подобным подходом, и, вероятно, это имеет смысл, но вам нужно быть очень осторожным с явной проверкой, так как я думаю, что вы можете в конечном итоге вызвать функцию __len__
экземпляра, а не класса, если они оба определенный.
@EliasMi Мой вопрос нацелен на случаи, когда вы не хотите или не можете изменять метакласс (это мой недостаток, который не четко указан в исходном вопросе). В любом случае, это полезно знать, если действительно нет другого способа сделать это :)
__len__
, определенный в классе, всегда будет игнорироваться при использовании len(...)
для самого класса: при выполнении его операторов и методов, таких как «хеш», «иттер», «длина», можно грубо сказать, что они имеют «статус оператора», Python всегда получить соответствующий метод из целевого класса путем прямого доступа к структуре памяти класса. Эти методы dunder имеют «физический» слот в структуре памяти для класса: если метод существует в классе вашего экземпляра (и в этом случае «экземплярами» являются классы «GoodBar» и «BadBar», экземпляры « FooMeta"), или один из его суперклассов, иначе оператор не работает.
Итак, это рассуждение применимо к len(GoodBar())
: он будет вызывать __len__
, определенный в классе GoodBar()
, а len(GoodBar)
и len(BadBar)
будут вызывать __len__
, определенный в их классе, FooMeta
I don't really understand the interaction with the @classmethod decorator.
Декоратор «classmethod» создает специальный дескриптор из декорированной функции, поэтому, когда он извлекается через «getattr» из класса, к которому он также привязан, Python создает «частичный» объект с уже имеющимся аргументом «cls». . Точно так же, как извлечение обычного метода из экземпляра создает объект с предварительно привязанным «я»:
Обе вещи передаются через протокол «дескриптор», что означает, что и обычный метод, и метод класса извлекаются путем вызова его метода __get__
. Этот метод принимает 3 параметра: «я», сам дескриптор, «экземпляр», экземпляр, к которому он привязан, и «владелец»: класс, к которому он привязан. Дело в том, что для обычных методов (функций), когда вторым (экземплярным) параметром для __get__
является None
, возвращается сама функция. @classmethod
оборачивает функцию в объект с другим __get__
: тот, который возвращает эквивалент partial(method, cls)
, независимо от второго параметра __get__
.
Другими словами, этот простой чистый код Python воспроизводит работу декоратора classmethod
:
class myclassmethod:
def __init__(self, meth):
self.meth = meth
def __get__(self, instance, owner):
return lambda *args, **kwargs: self.meth(owner, *args, **kwargs)
Вот почему вы видите такое же поведение при явном вызове метода класса с помощью klass.__get__()
и klass().__get__()
: экземпляр игнорируется.
TL;DR: len(klass)
всегда будет проходить через слот метакласса, а klass.__len__()
извлекать __len__
через механизм getattr, а затем правильно связывать метод класса перед его вызовом.
Aside from the obvious metaclass solution (ie, replace/extend FooMeta) is there a way to override or extend the metaclass function so that len(BadBar) -> 9002?
(...) To clarify, in my specific use case I can't edit the metaclass, and I don't want to subclass it and/or make my own metaclass, unless it is the only possible way of doing this.
Другого пути нет. len(BadBar)
всегда будет проходить через метакласс __len__
.
Однако расширение метакласса может оказаться не таким уж болезненным.
Это можно сделать простым вызовом type
, передав новый __len__
метод:
In [13]: class BadBar(metaclass=type("", (FooMeta,), {"__len__": lambda cls:9002})):
...: pass
In [14]: len(BadBar)
Out[14]: 9002
Только если BadBar позже будет объединен в множественном наследовании с другой иерархией классов с пользовательским метаклассом другой, вам придется беспокоиться. Даже если есть другие классы, которые имеют FooMeta
в качестве метакласса, приведенный выше фрагмент будет работать: динамически созданный метакласс будет метаклассом для нового подкласса, как «наиболее производный подкласс».
Однако, если существует иерархия подклассов и у них разные метаклассы, даже если они созданы этим методом, вам придется объединить оба метакласса в общий subclass_of_the_metaclasses перед созданием нового «обычного» подкласса.
Если это так, обратите внимание, что у вас может быть один единственный параметризируемый метакласс, расширяющий исходный (однако от этого не увернуться)
class SubMeta(FooMeta):
def __new__(mcls, name, bases, ns, *,class_len):
cls = super().__new__(mcls, name, bases, ns)
cls._class_len = class_len
return cls
def __len__(cls):
return cls._class_len if hasattr(cls, "_class_len") else super().__len__()
И:
In [19]: class Foo2(metaclass=SubMeta, class_len=9002): pass
In [20]: len(Foo2)
Out[20]: 9002
Вы можете получить один метакласс из другого и переопределить методы базового класса.