Python callable, types, inspect

Updated:

문제

spyder IDE에서 pandas DataFrame의 attributes를 살펴보면 아래 그림처럼 data attributes는 기호 a, method나 function은 기호 f로 표시하여 나타내 준다.

loc과 같이 DataFrame에서 자주 사용하는 data attributes는 columns, dtypes, iloc, index, T, values등이 있다.

여기서 이런 궁금증이 생겼다. spyder IDE는 data attribute와 method (또는 function)를 어떻게 구분하는 걸까? 가장 단순히 생각하면 callable 인지 아닌지로 판별하는 것이다. 아래는 spyder IDE에서 af라고 표시해주는 몇몇 DataFrame attributes에 대해 callable 여부를 판별하는 코드다.

import pandas as pd

attrs_a = ['columns', 'dtypes', 'iloc', 'index', 'loc', 'T', 'values']
attrs_f = ['abs', 'apply', 'combine', 'dropna', 'isnull', 'sample']
df = pd.DataFrame({'symbol': ['a']*len(attrs_a) + ['f']*len(attrs_f),
                   'attribute': attrs_a + attrs_f})
func = lambda attr, obj: callable(getattr(obj, attr))

# callable(pd.DataFrame.attr)
df['pd.DataFrame'] = df['attribute'].apply(func, obj=pd.DataFrame)

# callable(DataFrameInstance.attr)
ins = pd.DataFrame()
df['instance'] = df['attribute'].apply(func, obj=ins)

# 최종 결과
df.set_index(['symbol', 'attribute'])

출력 결과는 아래와 같다.

                  pd.DataFrame  instance
symbol attribute                        
a      columns           False     False
       dtypes            False     False
       iloc              False      True
       index             False     False
       loc               False      True
       T                 False     False
       values            False     False
f      abs                True      True
       apply              True      True
       combine            True      True
       dropna             True      True
       isnull             True      True
       sample             True      True

위 결과 중 예상하지 못했던 것은 DataFrame instance의 lociloc attributes가 callable이라는 점이다. lociloc은 아래와 같은 방식으로 DataFrame을 indexing하는데 사용하는 object인데 놀랍게도 callable이다.

df.loc[:, ['attribute', 'instance']]
df.iloc[:3,1:3]

callable(df.loc)  # True
callable(df.iloc) # True

DataFrame instance의 lociloc은 왜 callable 일까? 그리고 callable인데도 불구하고 spyder IDE에서는 왜 기호 f라고 나타내지 않고, a라고 표시한 걸까?

DataFrame instance의 loc과 iloc은 callable

pandas 문서나 도움말 어디에도 lociloc을 호출하는 것에 관한 내용은 찾을 수 없었다. 호출 방식은 아래와 같다. 인수로 axis를 나타내는 정수나 문자열을 입력 할 수 있고 _locIndexer 객체가 리턴된다.

df = pd.DataFrame(np.arange(6).reshape(3, 2))
df.loc()
df.loc(0)
df.loc(1)
df.loc('index')
df.loc('columns')

Indexer가 리턴되므로 indexing을 할 수 있는 것 같다. 재밌는 점은 axis 인수를 입력하지 않으면 2D Indexer이고, axis 인수를 입력하면 입력한 axis 만의 indexer인 것이다.

df.loc()[:, [0, 1]]
df.loc(0)[1]
df.loc(1)[:1]

어떤 쓸모가 있는지, 왜 만들어져 있는지는 모르겠지만, DataFrame instance의 loc, iloc attributes는 callable이다.

그러면 spyder IDE에서는 loc과 iloc에 대해 어떻게 a라고 표시했을까?

조사

위 문제에 대해서 이것저것 검색하던 중에 stack overflow에서 재밌는 글을 보았다.

namespace에 있는 어떤 name이 function인지 아닌지를 어떻게 판별하는가 인데, 여기서 질문자의 의도가 function이 그냥 callable object를 의미하는지 아니면 user-defined function 또는 builtin function을 의미하는지에 따라 답변이 갈리는 내용이다. 이 답변들에서 callable 함수와 types 모듈, inspect 모듈에 대한 흥미로운 정보가 있었는데 그것들에 대해 먼저 리뷰한다.

callable 고찰

위 stack overflow 글이 좀 오래된 글이라 지금과는 약간 다른 내용이 있다. 당시 python 2 또는 3가 버전이 낮거나 또는 python 2 기능이 python 3에 포팅 되지 않았거나 또는 버그가 있거나 했던 것 같다. 우선 그런 것에 대해 현재 Python 3.7에서 어떤 결과가 나오는지 점검하고 넘어간다.

어떤 객체가 callable 인지 체크하는 방법에 대해 3가지를 보여준다. 첫 번째는 builtin function callable을 사용하는 것이다. 이 방법이 제일 간단하고 좋은 방법이다.

f = lambda x: x
s = 'abcd'

callable(f)   # True
callable(len) # True
callable(s)   # False

두 번째 방법은 객체가 __call__ attribute를 가지고 있는지 체크하는 것이다. 결과는 callable 함수를 사용한 것과 같다.

hasattr(f, '__call__')   # True
hasattr(len, '__call__') # True
hasattr(s, '__call__')   # False

그러나 callable 객체임을 판단하는 기준이 __call__ attribute를 가지고 있는지 체크하는 방법이 조금 위험하다는 것을 stack overflow 글의 SingleNegationElimination 이라는 사람이 보여주는데 꾀 흥미롭다.

# 빈 class를 만들고
class C:
    pass
# class instance에 fake __call__ attribute를 추가
c = C()
c.__call__ = lambda *args: 'called'

hasattr(c, '__call__')  # True
callable(c) # False
c() # TypeError: 'C' object is not callable

사실 위와 같이 fake __call__를 만드는 경우는 없겠지만 어쨌든 callable 여부를 __call__ attribute의 존재 유무로 체크하는 건 안좋아 보인다. callable 함수는 내부적으로 tp_call을 체크한다는 언급이 있는데 너무 깊은 내용이라 여기서는 그냥 넘어간다.

세 번째 방법은 callable object의 Base class의 instance인지를 체크하는 방법이다. Base class는 collections.abc.Callable 이다.

import collections
isinstance(f, collections.abc.Callable)   # True
isinstance(len, collections.abc.Callable) # True
isinstance(s, collections.abc.Callable)   # False
isinstance(c, collections.abc.Callable)   # False

아마도 stack overflow 글을 작성할 때는 이 방법이 버그가 있어서 마지막 결과가 True가 나왔던 것 같다. 그런데 이제는 callable 함수를 사용한 것과 동일한 결과를 준다.

types 모듈

Python builtin type에는 int, float, str, list, tuple, dict, set, frozenset 뿐 아니라 function, builtin_function_or_method, module 등이 있다. 그런 builtin type 중 int, float, str, list, tuple, dict, set, frozenset에 대한 constructor 함수가 모두 builtin 영역에 있다.

function, builtin_function_or_method, module 등과 같이 builtin 영역에 없는 constructor는 types 모듈에 정의되어 있다. 따라서 어떤 object가 function 또는 builtin_function_or_method 또는 module 등의 instance 인지는 isinstance 함수와 constructor를 사용하여 판단할 수 있다.

예를 들어 사용자 정의함수는 function (FunctionType) 이지만, builtin_function_or_method (BuiltinFunctionType)는 아니다.

def func():
    pass
isinstance(func, types.FunctionType) # True
isinstance(func, types.BuiltinFunctionType) # False

반면, builtin 함수들은 function (FunctionType) 이 아니라, builtin_function_or_method (BuiltinFunctionType)이다.

isinstance(open, types.FunctionType) # False
isinstance(open, types.BuiltinFunctionType) # True

모듈은 당연히 ModuleType이다.

isinstance(types, types.ModuleType) # True

types module을 보면 아래 목록과 같이 함수와 관련된 types을 볼 수 있다.

  • types.FunctionType, types.LambdaType: 사용자 정의함수와 lambda 함수에 대한 type
  • types.MethodType: 사용자 정의 클래스 인스턴스의 메소드 type
  • types.BuiltinFunctionType, types.BuiltinMethodType: built-in 함수들과 C로 작성된 클래스 메소드 type

아래는 BuiltinFunctionType, FunctionType, LambdaType, MethodType에 대해서, 클래스 이름으로 접근하는 메소드, 인스턴스에서 접근하는 메소드, lambda 함수, 사용자 정의 함수, 내장 함수에 대해 isinstance를 조사하는 코드다.

import pandas as pd

mapfun = lambda attr, obj: isinstance(obj, getattr(types, attr))
ts = ['BuiltinFunctionType', 'FunctionType', 'LambdaType', 'MethodType']
df = pd.DataFrame({'Type':ts})

# test object 1. Cls.run 
class Cls: 
    def run(self):
        pass    
# test object 2. ins.run
ins = Cls()
# test object 3. lambda function
lam = lambda x: x
# test object 4. user-defined function
def fun(): pass
# test object 5. built-in function
bui = open

df['Cls.run'] = df['Type'].apply(mapfun, obj=Cls.run)
df['ins.run'] = df['Type'].apply(mapfun, obj=ins.run)
df['lambda'] = df['Type'].apply(mapfun, obj=lam)
df['userfun'] = df['Type'].apply(mapfun, obj=fun)
df['builtinfun'] = df['Type'].apply(mapfun, obj=bui)
df.set_index('Type')

결과는 다음과 같다.

                     Cls.run  ins.run  lambda  userfun  builtinfun
Type                                                              
BuiltinFunctionType    False    False   False    False        True
FunctionType            True    False    True     True       False
LambdaType              True    False    True     True       False
MethodType             False     True   False    False       False

클래스 이름으로 접근하는 메소드는 인수가 하나인 함수에 불과하다. 그래서 lambda와 userfun과 마찬가지로 FunctionType, LambdaType이 True가 나온다. 인스턴스에서 접근하는 메소드는 MethodType만 True다. builtin 함수는 역시 BuiltinFunctionType만 True가 된다.

이제 다시 pandas DataFrame instance의 loc, iloc 문제로 돌아가자. callable 객체였지만 spyder에서 a로 표시했던 lociloc이 어떤 type에 속하는지 알아보자. 그리고 비교 대상으로 callable 객체이면서 spyder에서 f로 표시했던 apply, combine이 어디에 속하는지 알아보자.

ins = pd.DataFrame()
df['loc'] = df['Type'].apply(mapfun, obj=ins.loc)
df['iloc'] = df['Type'].apply(mapfun, obj=ins.iloc)
df['apply'] = df['Type'].apply(mapfun, obj=ins.apply)
df['combine'] = df['Type'].apply(mapfun, obj=ins.combine)
df.set_index('Type')

결과는 다음과 같다.

                       loc   iloc  apply  combine
Type                                             
BuiltinFunctionType  False  False  False    False
FunctionType         False  False  False    False
LambdaType           False  False  False    False
MethodType           False  False   True     True

위 결과를 보면 lociloc attribute는 어떤 Type에도 속하지 않는다. 즉 함수도 아니고 메소드도 아니라는 말이다. 단지 callable object다. (__call__ attribute도 가지고 있다.) 단순히 lociloc의 Type을 말하자면 pandas에서 정의한 _LocIndexer Type이다. 그리고 그것의 mro도 확인해 보면 좋다.

>>> type(ins.loc)
pandas.core.indexing._LocIndexer

>>> ins.loc.__class__.mro()
[pandas.core.indexing._LocIndexer,
 pandas.core.indexing._LocationIndexer,
 pandas.core.indexing._NDFrameIndexer,
 pandas._libs.indexing._NDFrameIndexerBase,
 object]

types 모듈에 대해서 알아본 것의 의미를 다시 생각해 보면, spyder에서 a라고 표시하는 locf라고 표시하는 apply를 어떻게 구분하는가에 대해 아래와 같이 어떤 type에 속하는지로 판단하는게 아닌가 생각한다.

# isinstance에 체크할 타입을 튜플로 한꺼번 입력할 수 있다.
chktypes = (types.BuiltinFunctionType, types.FunctionType, types.MethodType)
isinstance(ins.loc, chktypes)   # False
isinstance(ins.apply, chktypes) # True

types 모듈 응용의 또 다른 예로 Builtin 함수 목록 얻기다. How to get the list of all built in functions in Python에서 참고한 내용이다. 여기 출력되는 함수 목록은 파이썬 공식 매뉴얼의 Built-in Functions보다 적다. 파이썬 공식 매뉴얼의 Built-in Functions에는 엄밀히 말하면 types.BuiltinFunctionType에 속하지 않는 것들이 있다.

import types
import builtins

builtinfuns = [name for name, obj in vars(builtins).items()
                    if isinstance(obj, types.BuiltinFunctionType)]
builtinfuns
len(builtinfuns)

inspect 모듈

inspect 모듈에는 객체에 대한 정보를 파악하는 여러 유용한 기능들이 있다. 자세한 내용은 파이썬 공식 문서 inspect - Inspect live objects를 참고해도 좋지만, 기능에 대한 좀 더 빠른 리뷰는 journaldev python inspect module을 참고하라.

inspect 모듈에는 isbuiltin, isclass, isfunction, ismethod, ismodule 같이 객체를 분류하는데 유용한 함수들을 제공한다. builtin 함수, lambda 함수, 그리고 pandas 모듈, pandas.DataFrame 클래스, DataFrame instance의 loc, apply에 대해서 inspect의 분류 함수를 적용한 코드는 아래와 같다.

import inspect
import pandas as pd

mapfun = lambda attr, obj: getattr(inspect, attr)(obj)
isfuns = ['isbuiltin', 'isclass', 'isfunction', 'ismethod', 'ismodule']
df = pd.DataFrame({'inspect.': isfuns})

testobjs = {'builtin': len, 
            'lambda': lambda x: x, 
            'pd': pd, 
            'pd.DataFrame': pd.DataFrame,
            'pd.DFins.loc': pd.DataFrame().loc, 
            'pd.DFins.apply': pd.DataFrame().apply}
for name, obj in testobjs.items():
    df[name] = df['inspect.'].apply(mapfun, obj=obj)

df.set_index('inspect.')

결과는 다음과 같다.

            builtin  lambda     pd  pd.DataFrame  pd.DFins.loc  pd.DFins.apply
inspect.                                                                      
isbuiltin      True   False  False         False         False           False
isclass       False   False  False          True         False           False
isfunction    False    True  False         False         False           False
ismethod      False   False  False         False         False            True
ismodule      False   False   True         False         False           False

위와 같이 inspect 모듈을 사용하면 매우 쉽게 객체를 분류할 수 있다. loc에 대해서도 의도대로 분류한다. journaldev python inspect module에도 언급된 것처럼 inspect 모듈이 통합개발환경 (IDE) 에디터에서 객체 분류에 사용되는 모듈이 아닌가 생각된다.

내용 추가 (2020-03-29): spyder IDE에서 attribute를 분류하는 방법

spyder IDE에서 attribute를 a, f, c로 분류하는 것에 대해 더 깊이 고민해 보았고 그 결과는 attributes 분류 알고리즘에 자세히 정리하였다. 결과만 간단히 적으면 다음과 같다.

def get_symbol(obj):
    if inspect.isroutine(obj):
        return 'f'
    elif inspect.isclass(obj):
        return 'c'
    elif inspect.isdatadescriptor(obj):
        return 'a'
    else:
        return 'a'

# tests
assert get_symbol(pd.DataFrame.__delattr__) == 'f' 
assert get_symbol(pd.DataFrame.plot) == 'c'
assert get_symbol(pd.DataFrame.loc) == 'a'
assert get_symbol(pd.DataFrame._AXIS_ALIASES) == 'a'

정리

  • 객체의 callable 여부만 확인하려면 callable 함수를 사용
    • 그러나 어떤 객체가 callable 이더라도, 그 객체가 function, method, builtin functions 같은 부류에 속하지 않을 수 있음
  • 객체를 class, function, method, builtin 등으로 분류하려면 inspect 모듈을 사용
  • types 모듈에는 builtin 영역에 없는 python 내장 types있고 동적으로 그것들을 생성하는 constructor가 있음
    • isinstace 함수로 그런 객체의 type을 체크를 할 수 있지만, 분류에는 inspect 모듈이 더 좋음

Leave a comment