В начале же статьи предупрежу:
Эта статья предназначена только для тех людей, которым хочется узнать, чем на самом деле является функция в python....НО! Предупреждаю, я не буду лезть в сурсы питона. Эта статья была создана только для обычных вроде меня программистов.
Статья будет состоять из 4 частей:
Как осуществляется поиск атрибутов в классах.
Что есть метод и как он вызывается.
Что есть функция и как она вызывается.
Вывод
1. Как осуществляется поиск атрибутов в классах
Мы знаем, что при обращении к атрибуту вызывается дандер-метод __getattribute__, который в свою очередь пытается возвратить наш атрибут, но! Если он его не находит атрибут, то он вызывает исключение AttributeError, если __getattr__ не определен в нашем классе. Это понятно, но мы не знаем, что происходит под капотом. И поэтому я решил создать свою интерпретацию поиска атрибутов.
Ссылка на мою блок-схему
Как видите, тут не так все просто как нам казалось) Срабатывается куча проверок, чтобы наконец-то либо возвратить наш атрибут или вызвать исключение.
Давайте с помощью этой блок-схемы, реализуем свой прототип __getattribute__ и посмотрим, будет ли он работать так же как и оригинал.
Вот что вышло:
def __getattribute__(self, item):
''' Прототип дандер-метода __getattribute__.'''
for cls in Class.__mro__:
if item in cls.__dict__: # есть ли атрибут в пространстве имен класса.
item_class_dict = type(cls.__dict__[item]).__dict__ # Пространства имен класса нашего атрибута
if "__get__" in item_class_dict: # Является ли атрибут, дескриптором.
return cls.__dict__[item].__get__(self, cls)
return cls.__dict__[item]
else:
if item in self.__dict__: # есть ли атрибут в пространстве имен экземпляра класса
return self.__dict__[item]
if "__getattr__" in self.__class__.__dict__: # Есть ли у класса, дандер-метод __getattr__
return self.__getattr__(item)
raise AttributeError
И давайте наконец его испробуем!
Создадим класс Pet, который наследуется от класса Animal, где первый атрибут будет объектом дескриптора данных и 2 обычных переменных экземпляра класса это animal и age:
class Animal:
eyes = 2
legs = 4
def eat(self, food):
return f"Yum, yum, yum ({food})"
class Descriptor:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if isinstance(value, str):
instance.__dict__[self.name] = value
else:
raise TypeError("Название, должно быть строкой!")
class Pet(Animal):
name = Descriptor()
def __init__(self, name, animal, age=1):
self.name = name
self.animal = animal
self.age = age
def __getattribute__(self, item):
''' Прототип дандер-метода __getattribute__.'''
for cls in Pet.__mro__:
if item in cls.__dict__: # есть ли атрибут в пространстве имен класса.
item_class_dict = type(cls.__dict__[item]).__dict__ # Пространства имен класса нашего атрибута
if "__get__" in item_class_dict: # Является ли атрибут, дескриптором.
return cls.__dict__[item].__get__(self, cls)
return cls.__dict__[item]
else:
if item in self.__dict__: # есть ли атрибут в пространстве имен экземпляра класса
return self.__dict__[item]
if "__getattr__" in self.__class__.__dict__: # Есть ли у класса, дандер-метод __getattr__
return self.__getattr__(item)
raise AttributeError
def meow(self):
return "Meowwww!!!!" if self.animal.lower() == "cat" else "You're pet isn't cat!"
cat_kit = Pet("Kit", "Cat", 17)
print(cat_kit.name) # Kit
print(cat_kit.animal) # Cat
print(cat_kit.meow()) # Meowwww!
print(cat_kit.meow) # <bound method Pet.meow of <__main__.Pet object at 0x7f940fe2f1f0>>
print(cat_kit.eyes) # 2
print(cat_kit.eat("Kitekat")) # "Yum, yum, yum (Kitekat)"
print(cat_kit.eat) # <bound method Animal.eat of <__main__.Pet object at 0x7f940fe2f1f0>>
cat_kit.dog
И как вы видите, у нас все сработало!) Наш прототип __getattribute__ отлично возвращает методы, дескрипторы данных, дескрипторы не-данных, локальные атрибуты (если так можно назвать), атрибуты класса.) (Небольшое уточнение: под возвращением атрибутов, я имею ввиду возвращения их через экземпляр класса, т.к через класс все атрибуты возвращаются с помощью вызова __getattribute__ у метакласса (type))
Давайте на этом примере я объясню как все происходит. К примеру, второй вызов нашего прототипа для возвращения локального атрибута animal. Тут все очень просто. Происходит 3 итерации нашего цикла, где мы проверяем, является ли animal чьим-то атрибутом класса. Конечно же нет, потому у нас выполняется конструкция в else, где мы проверяем есть ли animal в пространстве имен экземпляра класса. Есть. И мы его возвращаем.
А теперь давайте перейдем к дескриптору данных, то есть к атрибуту name. Происходит 1 итерация, где мы проверяем, есть ли он в пространстве имен класса Pet. Да, есть. Дальше, является ли он дескриптором данных. Является. Потому мы с помощью дандер метода __get__ возвращаем его.
Так же не забываем, что наш прототип проходит по всему MRO класса, как у оригинала, он может возвращать все методы род.класса и так же его атрибуты (как показано на примере). Но некоторые из вас спросят, как вызвался атрибут eyes класса Animal к примеру? Для начала узнаем как выглядит MRO у класса Pet.
Вот так:
[<class 'main.Pet'>, <class 'main.A'>, <class 'object'>]
В первую очередь происходит первая итерация нашего цикла в прототипе __getattribute__, где cls это класс Pet. Он не проходит проверку if item in cls.__dict__, потому что у Pet нет такого атрибута. Потому, происходит вторая итерация, уже Animal и при той проверке, она возвращает True поэтому мы возвращаем атрибут класса Animal.
Теперь, вы уже знаете как примерно происходит поиск атрибутов, как под капотом вызываются дескриптора данных и дескрипторы не-данных, локальные атрибуты, атрибуты класса и т.д. Вы даже можете создать свой прототип __getattribute__. Поэтому, давайте перейдем ко второй части статьи.
2. Что такое метод и как он вызывается
Вы уже наверно подумали, почему же я не объяснил про вызов метода meow с помощью нашего прототипа, точнее...как он вызвался с помощью него?
Давайте по порядку и издалека. Какие свойства имеют методы? Ну, они возвращают объект bound method при обращении через экземпляр класса, а так же первым аргументом всегда у них является опять-таки экземпляр класса. Отлично! С этим мы разобрались.
Теперь...Давайте, чтобы доказать вам, что метод является дескриптором не-данных я вам объясню почему метод не вызывается как обычный атрибут класса (Class.__dict__[item]) или как обычный локальный атрибут объекта класса (obj.__dict__[item])
Во первых, если бы методы вызывались как обычный атрибут класса, мы бы точно не смогли бы возвращать объект bound method через экземпляр класса при обращений к ним, потому что метод возвращал бы объект функций (как и класс). И тут вы зададите вопрос, а почему? Ну потому что в любом случае он бы под капотом вызывался бы как атрибут класса (Class.__dict__[item]), если бы мы даже обращались к нему с помощью объекта класса. Вот небольшой пример:
class Foo:
def get_one(self):
return 1
def __getattribute__(self, item):
return Foo.__dict__[item]
a = Foo()
print(Foo.get_one) # <function Foo.get_one at 0x7f5f7c9efd00>
print(a.get_one) # <function Foo.get_one at 0x7f5f7c9efd00>, а должен был возвратить <bound method Foo.get_one of <__main__.Foo object at 0x7ffb25aca800>>
print(Foo.get_one(a)) # 1
print(a.get_one(a)) # 1
print(a.get_one()) # TypeError: Foo.get_one() missing 1 required positional argument: 'self'
print(Foo.get_one is a.get_one) # True
А теперь, давайте попробуем второй вариант. Запустим такой код:
class Email:
def __init__(self):
self.lst_packages = []
def add_package(self, package):
if isinstance(package, dict) is False:
raise TypeError("Предмет должен иметь адрес и товар!!!")
if package not in self.lst_packages:
self.lst_packages.append(package)
return "Посылка была добавлена!"
return "В нашей почте такая посылка уже есть!"
def get_packages(self):
return self.lst_packages
def __getattribute__(self, item):
if item == '__dict__':
return Email.__dict__[item].__get__(self, Email)
return self.__dict__[item]
mail_ru = Email()
print(mail_ru.lst_packages) # []
print(mail_ru.get_packages()) # KeyError: 'get_packages'
И к сожалению мы получаем исключение KeyError.
Тут мне кажется и так было понятно что вариант с объектом класса не сработает, только из-за того что методы находятся в пространстве имен класса, а не в экземпляре естественно. И прежде чем я отвечу на вопрос что вообще делает тут такое:
if item == '__dict__':
return Email.__dict__[item].__get__(self, Email)
Давайте вернемся к результатам:
Метод не вызывается как обычный атрибут класса, т.к происходит иное поведение (возвращение объекта функций, а не bound method даже при обращении к методу с помощью объекта класса), во вторых обращение экземпляра класса к методам такое же,как и у класса, т.к под капотом он все равно вызывался бы как через класс. Потому, такой вариант не является истинным.
Метод не вызывается как обычный локальный атрибут объекта класса, т.к методы находятся в пространстве имен класса, а не в объекте класса.
И что мы получаем тогда? То что метод является - дескриптором...А точнее: Метод - это дескриптор не-данных.
Если вы мне не поверили, то:
print('__get__' in type(cat_kit.meow).__dict__) # True
print('__set__' in type(cat_kit.meow).__dict__) # False
print('__delete__' in type(cat_kit.meow).__dict__) # False
Кстати, если уж мы вспомнили о классе Pet и его методе meow, то давайте заодно попробуем вызвать его как дескриптор:
class Descriptor:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return 1
class Pet:
foobar = Descriptor()
def __getattribute__(self, item):
''' Прототип дандер-метода __getattribute__. Возвращает'''
for cls in Pet.__mro__:
if item in cls.__dict__: # есть ли атрибут в пространстве имен класса.
item_class_dict = type(cls.__dict__[item]).__dict__ # Пространства имен класса нашего атрибута
if "__get__" in item_class_dict: # Является ли атрибут, дескриптором.
print(f"THE DESCRIPTOR!!!! {item}")
return cls.__dict__[item].__get__(self, cls)
return cls.__dict__[item]
else:
if item in self.__dict__: # есть ли атрибут в пространстве имен экземпляра класса
return self.__dict__[item]
if "__getattr__" in self.__class__.__dict__: # Есть ли у класса, дандер-метод __getattr__
return self.__getattr__(item)
raise AttributeError
def meow(self):
return "Meowwww!!!"
cat_kit = Pet()
print(cat_kit.meow()) # THE DESCRIPTOR!!!!!! Meowwww!!!
print(cat_kit.foobar) # THE DESCRIPTOR!!!!!! 1
# cls.__dict__[item].__get__(instance, owner)
print(Pet.__dict__["meow"].__get__(cat_kit, Pet).__call__()) # Meowwww!!!
print(Pet.__dict__["meow"].__get__(cat_kit, Pet)) # <bound method Pet.meow of <__main__.Pet object at 0x7f0dd1c03df0>>
print(Pet.__dict__["foobar"].__get__(cat_kit, Pet)) # 1
Как видите, все сработало на ура!
Теперь, давайте вернемся к той части кода в классе Email:
def __getattribute__(self, item):
if item == '__dict__':
return Email.__dict__[item].__get__(self, Email)
return self.__dict__[item]
Почему тут присутствует проверка item на значение __dict__ и почему я вызываю у него метод __get__? Ну-у, скорее всего потому что он является - дескриптором :)...И кстати дескриптором данных, он имеет и __get__ и __set__ и __delete__.
И вы наверно в ответ скажите, а когда мы вообще к нему обращаемся? Вот здесь self.__dict__ опять вызывается __getattribute__,в котором параметр item имеет значение __dict__.
И узнав что __dict__ является дескриптором, вы одновременно узнали как он возвращается у объектов.
print(Pet.__dict__['__dict__'].__get__(cat_kit, Pet)) # {"eyes": 4}
print(Pet.__dict__['__dict__'].__get__(dog, Pet)) # {"legs": 4}
И кстати, у классов же атрибут __dict__ не является дескриптором.
3. Что такое функция и как она вызывается
Теперь, мы можем приступить к самой главной части статье.
Мы знаем что функция и методы (здесь, под методами я подразумеваю именно функцию класса, а не объект класса method) являются объектами класса function.
Потому не сложно понять, что функция так же является дескриптором не-данных и еще не забываем, что метод по сути является функцией класса, грубо говоря. И тут задается вопрос, а может на самом деле функции вызывается как метод? Не совсем...Но мы можем ее так вызывать:
class Foo:
pass
f = Foo()
def get_one(self):
return 1
print(get_one.__get__(f, Foo)) # <bound method get_one of <__main__.Foo object at 0x7fe4c157b850>>
print(get_one.__get__(f, Foo).__call__()) # 1
print("get_one" in Foo.__dict__) # False
Тут удивляться нечему. Потому что вместо того, чтобы обращаться к методу через пространство имен класса (потому что по другому мы не смогли бы), мы попросту прямо обращаемся к нашей обычной функции и возвращаем ее как связанный метод.
Но все равно, на самом деле функции так не вызываются. Давайте опять вспомним, что функция является объектом класса function, где тело функций содержится в дандер-методе __call__ который принадлежит естественно к классу функций. И чтобы вызвать этот метод...Что нужно сделать?) Правильно! Обратиться к дандер-методу __get__ где экземпляром класса будет сама наша функция, а класс - класс функций. Давайте же это реализуем!
def foo():
return 1
Function = foo.__class__
print(Function.__dict__['__call__'].__get__(foo, Function) == foo.__call__) # True
print(Function.__dict__['__call__'].__get__(foo, Function).__call__()) # 1
print(foo.__call__()) # 1
И вот так, мы узнали как на самом деле "вызываются" функции) Но у некоторых может возникнуть еще один вопрос, "а почему у функций есть метод __get__, если мы его по сути не используем для вызова?" Ответ очень прост: на случае если мы обратимся к функции как к атрибуту.
4. Вывод
Мы узнали как примерно происходит поиск атрибутов в классах, что функция и метод являются дескрипторами не-данных. Мы узнали как на самом деле вызываются методы:
cls.__dict__["method"].__get__(instance, cls)()
Как на самом деле вызываются функции:
cls_function.__dict__["__call__"].__get__(func, cls_function)()
И...Все! Моя статья уже подходит к концу! Потому... напоследок я хочу дать вам домашнее задания:
Добавить одну важную, но одновременно маленькую деталь, которую я не успел вписать в мою блок-схему и в прототип. И конечно же, скинуть улучшенный прототип в комментариях!
И так же, всем огромное спасибо за прочтение моей первой статьи! Огромная благодарность Павлу за помощь в ее написании и спасибо Бензу! Я очень надеюсь что я хоть как-то дал вам ответ!
Andrey_Solomatin
Функция это такая полезная штука, от ней можно например наследовать класс.
https://github.com/python/cpython/blob/main/Lib/typing.py#L2870
Germanets
Что-то страшное вы показываете по ссылке, файлы на 3000+ строчек кода в этом десятилетии - это уже шок-контент..
longclaps
Это что-то страшное - исходник стандартной библиотеки.
Простите её авторов за доставленный шок )
Andrey_Solomatin
Вам просто повезло с проектами.
MentalBlood
Разумеется
Andrey_Solomatin
Для наследования нужно еще немного магии добавить. Упрощённый пример кода из
NamedTuple
.https://gist.github.com/Cjkjvfnby/019311bb75e0fd4cc82614347e07398b