Определить, был ли класс определен декларативно или функционально - возможно? - PullRequest
0 голосов
/ 05 ноября 2018

Вот простой класс, созданный декларативно:

class Person:
    def say_hello(self):
        print("hello")

А вот аналогичный класс, но он был определен путем вызова метакласса вручную:

def say_hello(self):
    print("sayolala")

say_hello.__qualname__ = 'Person.say_hello'

TalentedPerson = type('Person', (), {'say_hello': say_hello})

Мне интересно узнать, неразличимы ли они. Можно ли обнаружить такое отличие от самого объекта класса?

>>> def was_defined_declaratively(cls):
...     # dragons
...
>>> was_defined_declaratively(Person)
True
>>> was_defined_declaratively(TalentedPerson)
False

Ответы [ 3 ]

0 голосов
/ 05 ноября 2018

Возможно - несколько.

inspect.getsource(TalentedPerson) потерпит неудачу с OSError, тогда как с Person это удастся. Это работает, только если у вас нет класса с таким именем в файле, где он был определен:

Если ваш файл состоит из обоих этих определений, и TalentedPerson также считает, что это Person, то inspect.getsource просто найдет определение Person.

Очевидно, что это зависит от исходного кода, который все еще присутствует и может быть найден с помощью inspect - это не будет работать с скомпилированным кодом, например в REPL, могут быть обмануты, и это своего рода обман. Фактические объекты кода не отличаются AFAIK.

0 голосов
/ 07 ноября 2018

Это не должно иметь никакого значения. Даже если мы поищем больше атрибутов, которые могут отличаться, можно будет добавить эти атрибуты в динамически создаваемый класс.

Теперь, даже без исходного файла (из которого такие вещи, как inspect.getsource могут пробиться, но см. Ниже), операторы тела класса должны иметь соответствующий объект "код", который выполняется в какой-то момент. Динамически созданный класс не будет иметь тела кода (но если вместо вызова type(...) вы вызываете types.new_class, вы также можете иметь собственный объект кода для динамического класса - так, что касается моего первого утверждения: это должно быть можно сделать оба класса неразличимыми.

Что касается определения местоположения объекта кода, не полагаясь на исходный файл (который, кроме inspect.getsource, может быть достигнут через атрибут .__code__ метода, который аннотирует co_filename и co_fistlineno (я полагаю, нужно было бы проанализируйте файл и найдите оператор class над co_firstlineno затем)

И да, вот оно: для данного модуля вы можете использовать module.__loader__.get_code('full.path.tomodule') - это вернет code_object. Этот объект имеет атрибут co_consts, который представляет собой последовательность со всеми константами, скомпилированными в этом модуле - среди них объекты кода для самих тел классов. И у них есть номер строки и объекты кода для вложенных объявленных методов.

Итак, наивная реализация может быть:

import sys, types

def was_defined_declarative(cls):
    module_name = cls.__module__
    module = sys.modules[module_name]
    module_code = module.__loader__.get_code(module_name)
    return any(
        code_obj.co_name == cls.__name__ 
        for code_obj in module_code.co_consts 
        if isinstance(code_obj, types.CodeType)
    )

Для простых случаев. Если вам нужно проверить, находится ли тело класса внутри другой функции или вложено в другое тело класса, вам нужно выполнить рекурсивный поиск по всем объектам кода .co_consts атрибут в файле> То же самое, если вы найдете, что безопаснее проверить на наличие каких-либо атрибуты за cls.__name__, чтобы утверждать, что вы получили правильный класс.

И снова, хотя это будет работать для классов с "хорошим поведением", можно динамически создавать все эти атрибуты, если это необходимо, но для этого в конечном итоге потребуется заменить объект кода для модуля в sys.__modules__ - он запускается получить немного более обременительно, чем просто предоставить __qualname__ методам.

обновление Эта версия сравнивает все строки, определенные внутри всех методов класса-кандидата. Это будет работать с классом данного примера - можно добиться большей точности, сравнивая другие члены класса, такие как атрибуты класса, и другие атрибуты метода, такие как имена переменных, и, возможно, даже байт-код. (По какой-то причине объект кода для методов в объекте кода модуля и в теле класса - это разные экземпляры, хотя code_objects должен быть неизменным).

Я оставлю реализацию выше, которая сравнивает только имена классов, так как это должно быть лучше для понимания происходящего.

def was_defined_declarative(cls):
    module_name = cls.__module__
    module = sys.modules[module_name]
    module_code = module.__loader__.get_code(module_name)
    cls_methods = set(obj for obj in cls.__dict__.values() if isinstance(obj, types.FunctionType))
    cls_meth_strings = [string for method in cls_methods for string in method.__code__.co_consts  if isinstance(string, str)] 

    for candidate_code_obj in module_code.co_consts:
        if not isinstance(candidate_code_obj, types.CodeType):
            continue
        if candidate_code_obj.co_name != cls.__name__:
            continue
        candidate_meth_strings = [string  for method_code in candidate_code_obj.co_consts if isinstance(method_code, types.CodeType) for string in method_code.co_consts if isinstance(string, str)]
        if candidate_meth_strings == cls_meth_strings:
            return True
    return False
0 голосов
/ 05 ноября 2018

Невозможно обнаружить такую ​​разницу во время выполнения с python. Вы можете проверить файлы с помощью стороннего приложения, но не на языке, так как независимо от того, как вы определяете ваши классы, они должны быть сведены к объектам, которыми интерпретатор знает, как управлять.

Все остальное является синтаксисом сахар и его смерть на этапе предварительной обработки операций над текстом.

Весь метапрограммирование - это техника, которая позволяет вам приблизиться к работе компилятора / интерпретатора. Выявление некоторых черт типа и предоставление вам возможности работать с типом с помощью кода.

...