В начале же статьи предупрежу:

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

Статья будет состоять из 4 частей:

  1. Как осуществляется поиск атрибутов в классах.

  2. Что есть метод и как он вызывается.

  3. Что есть функция и как она вызывается.

  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)

Давайте вернемся к результатам:

  1. Метод не вызывается как обычный атрибут класса, т.к происходит иное поведение (возвращение объекта функций, а не bound method даже при обращении к методу с помощью объекта класса), во вторых обращение экземпляра класса к методам такое же,как и у класса, т.к под капотом он все равно вызывался бы как через класс. Потому, такой вариант не является истинным.

  2. Метод не вызывается как обычный локальный атрибут объекта класса, т.к методы находятся в пространстве имен класса, а не в объекте класса.

И что мы получаем тогда? То что метод является - дескриптором...А точнее: Метод - это дескриптор не-данных.

Если вы мне не поверили, то:

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)()

И...Все! Моя статья уже подходит к концу! Потому... напоследок я хочу дать вам домашнее задания:

Добавить одну важную, но одновременно маленькую деталь, которую я не успел вписать в мою блок-схему и в прототип. И конечно же, скинуть улучшенный прототип в комментариях!

И так же, всем огромное спасибо за прочтение моей первой статьи! Огромная благодарность Павлу за помощь в ее написании и спасибо Бензу! Я очень надеюсь что я хоть как-то дал вам ответ!

Комментарии (8)


  1. Andrey_Solomatin
    12.01.2023 01:24

    Функция это такая полезная штука, от ней можно например наследовать класс.

    https://github.com/python/cpython/blob/main/Lib/typing.py#L2870

    def NamedTuple(typename, fields=None, /, **kwargs):
        """Typed version of namedtuple.
        Usage in Python versions >= 3.6::
            class Employee(NamedTuple):
                name: str
                id: int


    1. Germanets
      12.01.2023 03:00

      Что-то страшное вы показываете по ссылке, файлы на 3000+ строчек кода в этом десятилетии - это уже шок-контент..


      1. longclaps
        12.01.2023 08:08
        +5

        Это что-то страшное - исходник стандартной библиотеки.

        Простите её авторов за доставленный шок )


      1. Andrey_Solomatin
        12.01.2023 10:41
        +1

        Вам просто повезло с проектами.


    1. MentalBlood
      12.01.2023 10:18

      Разумеется


      def f():
          pass
      
      assert f.__class__.__name__ == 'function'


      1. Andrey_Solomatin
        12.01.2023 10:47

        Для наследования нужно еще немного магии добавить. Упрощённый пример кода из NamedTuple.

        https://gist.github.com/Cjkjvfnby/019311bb75e0fd4cc82614347e07398b


  1. PaveTranquil
    12.01.2023 17:45
    +1

    Кажется, у вас здесь что-то пропало.


    1. The-Idiot02 Автор
      12.01.2023 17:47

      Да.) Восстановил. Спасибо!)