
В начале же статьи предупрежу:
Эта статья предназначена только для тех людей, которым хочется узнать, чем на самом деле является функция в python….НО! Предупреждаю, я не буду лезть в сурсы питона. Эта статья была создана только для обычных вроде меня программистов.
Статья будет состоять из 4 частей:
-
Как осуществляется поиск атрибутов в классах.
-
Что есть метод и как он вызывается.
-
Что есть функция и как она вызывается.
-
Вывод
1. Как осуществляется поиск атрибутов в классах
Мы знаем, что при обращении к атрибуту вызывается дандер-метод __getattribute__, который в свою очередь пытается возвратить наш атрибут, но! Если он его не находит атрибут, то он вызывает исключение AttributeError, если __getattr__ не определен в нашем классе. Это понятно, но мы не знаем, что происходит под капотом. И поэтому я решил создать свою интерпретацию поиска атрибутов.

Как видите, тут не так все просто как нам казалось) Срабатывается куча проверок, чтобы наконец-то либо возвратить наш атрибут либо вызвать исключение.
Давайте с помощью этой блок-схемы, реализуем свой прототип __getattribute__ и посмотрим, будет ли он работать так же.
Вот что вышло:
И давайте наконец его испробуем!
Создадим класс 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'
Тут мне кажется и так было понятно что вариант с объектом класса не сработает, только из-за того что методы находятся в пространстве имен класса, а не в экземпляре естественно. И прежде чем я отвечу на вопрос что вообще делает тут такое:
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__["name"].__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. Что такое функция и как она вызывается
Теперь, мы можем приступить к самой главной части статье.
Мы знаем что функция и методы являются объектами класса 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)()
И…Все! Моя статья уже подходит к концу! Потому… напоследок я хочу дать вам домашнее задания:
Добавить одну важную, но одновременно маленькую деталь, которую я не успел вписать в мою блок-схему и в прототип. И конечно же, скинуть улучшенный прототип в комментариях!
И так же, всем огромное спасибо за прочтение моей первой статьи! Огромная благодарность Павлу за помощь в ее написании и спасибо Бензу! Я очень надеюсь что я хоть как-то дал вам ответ!
ссылка на оригинал статьи https://habr.com/ru/post/710186/
Добавить комментарий