파이썬에서 메모리 사용량을 어떻게 프로파일합니까?


230

나는 최근 알고리즘에 관심을 가지게되었고 순진한 구현을 작성한 다음 다양한 방식으로 최적화함으로써 알고리즘을 탐색하기 시작했습니다.

나는 이미 프로파일 링 런타임을위한 표준 파이썬 모듈에 익숙하다. (대부분의 경우 IPython에서 timeit magic 함수가 충분하다는 것을 알았지 만) 메모리 사용에 관심이있어서 트레이드 오프를 탐색 할 수있다. 예를 들어, 이전에 계산 된 값의 테이블을 캐싱하는 비용과 필요에 따라 다시 계산하는 비용). 주어진 기능의 메모리 사용량을 프로파일 링하는 모듈이 있습니까?


의 중복 어떤 파이썬 메모리 프로파일을 권장? . 2019 년 이럴 가장 좋은 대답은 memory_profiler
vladkha

답변:


118

이것은 이미 여기에 답변되었습니다 : Python memory profiler

기본적으로 당신은 그런 식으로 일합니다 ( Guppy-PE 에서 인용 ) :

>>> from guppy import hpy; h=hpy()
>>> h.heap()
Partition of a set of 48477 objects. Total size = 3265516 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  25773  53  1612820  49   1612820  49 str
     1  11699  24   483960  15   2096780  64 tuple
     2    174   0   241584   7   2338364  72 dict of module
     3   3478   7   222592   7   2560956  78 types.CodeType
     4   3296   7   184576   6   2745532  84 function
     5    401   1   175112   5   2920644  89 dict of class
     6    108   0    81888   3   3002532  92 dict (no owner)
     7    114   0    79632   2   3082164  94 dict of type
     8    117   0    51336   2   3133500  96 type
     9    667   1    24012   1   3157512  97 __builtin__.wrapper_descriptor
<76 more rows. Type e.g. '_.more' to view.>
>>> h.iso(1,[],{})
Partition of a set of 3 objects. Total size = 176 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0      1  33      136  77       136  77 dict (no owner)
     1      1  33       28  16       164  93 list
     2      1  33       12   7       176 100 int
>>> x=[]
>>> h.iso(x).sp
 0: h.Root.i0_modules['__main__'].__dict__['x']
>>> 

6
공식적인 구피 문서는 최소한의 것입니다. 다른 자원에 대해서는 이 예제heapy 에세이를 참조하십시오 .
tutuDajuju

13
Guppy는 더 이상 유지되지 않는 것 같습니다. 따라서이 답변은 다운 그레이드되고 다른 답변 중 하나가 대신 수락됩니다.
robguinness

1
@robguinness 다운 그레이드는 다운 투표를 의미합니까? 그것은 한 시점에서 가치가 있었기 때문에 공평하지 않은 것 같습니다. 상단의 편집 내용이 더 이상 X 이유에 유효하지 않으며 대신 Y 또는 Z 답변이 표시된다고 생각합니다. 이 행동 과정이 더 적절하다고 생각합니다.
WinEunuuchs2Unix

1
물론, 그것도 효과가 있지만, 어떻게 든 인정되고 가장 높은 투표 응답에 여전히 효과가 있고 유지되는 솔루션이 있다면 좋을 것입니다.
robguinness

92

Python 3.4에는 새로운 모듈이 포함되어 있습니다 tracemalloc. 메모리를 가장 많이 할당하는 코드에 대한 자세한 통계를 제공합니다. 다음은 메모리를 할당하는 맨 위 세 줄을 표시하는 예입니다.

from collections import Counter
import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


tracemalloc.start()

counts = Counter()
fname = '/usr/share/dict/american-english'
with open(fname) as words:
    words = list(words)
    for word in words:
        prefix = word[:3]
        counts[prefix] += 1
print('Top prefixes:', counts.most_common(3))

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

결과는 다음과 같습니다.

Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
Top 3 lines
#1: scratches/memory_test.py:37: 6527.1 KiB
    words = list(words)
#2: scratches/memory_test.py:39: 247.7 KiB
    prefix = word[:3]
#3: scratches/memory_test.py:40: 193.0 KiB
    counts[prefix] += 1
4 other: 4.3 KiB
Total allocated size: 6972.1 KiB

메모리 누수가 언제 누수가 아닌가?

이 예제는 계산이 끝날 때 메모리가 계속 유지되는 경우에 유용하지만 많은 메모리를 할당 한 다음 모두 해제하는 코드가있는 경우가 있습니다. 기술적으로 메모리 누수가 아니지만 생각보다 많은 메모리를 사용하고 있습니다. 메모리가 모두 해제 될 때 어떻게 메모리 사용량을 추적 할 수 있습니까? 코드 인 경우 실행 중에 스냅 샷을 찍기 위해 디버깅 코드를 추가 할 수 있습니다. 그렇지 않은 경우 기본 스레드가 실행되는 동안 백그라운드 스레드를 시작하여 메모리 사용량을 모니터링 할 수 있습니다.

다음은 코드가 모두 count_prefixes()함수 로 이동 된 이전 예 입니다. 해당 함수가 반환되면 모든 메모리가 해제됩니다. 또한 sleep()장기 실행 계산을 시뮬레이션하기 위해 일부 호출을 추가했습니다 .

from collections import Counter
import linecache
import os
import tracemalloc
from time import sleep


def count_prefixes():
    sleep(2)  # Start up time.
    counts = Counter()
    fname = '/usr/share/dict/american-english'
    with open(fname) as words:
        words = list(words)
        for word in words:
            prefix = word[:3]
            counts[prefix] += 1
            sleep(0.0001)
    most_common = counts.most_common(3)
    sleep(3)  # Shut down time.
    return most_common


def main():
    tracemalloc.start()

    most_common = count_prefixes()
    print('Top prefixes:', most_common)

    snapshot = tracemalloc.take_snapshot()
    display_top(snapshot)


def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


main()

해당 버전을 실행하면 메모리 사용량이 6MB에서 4KB로 줄어 들었습니다. 기능이 끝나면 메모리가 모두 해제 되었기 때문입니다.

Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
Top 3 lines
#1: collections/__init__.py:537: 0.7 KiB
    self.update(*args, **kwds)
#2: collections/__init__.py:555: 0.6 KiB
    return _heapq.nlargest(n, self.items(), key=_itemgetter(1))
#3: python3.6/heapq.py:569: 0.5 KiB
    result = [(key(elem), i, elem) for i, elem in zip(range(0, -n, -1), it)]
10 other: 2.2 KiB
Total allocated size: 4.0 KiB

다음은 메모리 사용량을 모니터링하기 위해 두 번째 스레드를 시작하는 다른 답변에서 영감을 얻은 버전 입니다.

from collections import Counter
import linecache
import os
import tracemalloc
from datetime import datetime
from queue import Queue, Empty
from resource import getrusage, RUSAGE_SELF
from threading import Thread
from time import sleep

def memory_monitor(command_queue: Queue, poll_interval=1):
    tracemalloc.start()
    old_max = 0
    snapshot = None
    while True:
        try:
            command_queue.get(timeout=poll_interval)
            if snapshot is not None:
                print(datetime.now())
                display_top(snapshot)

            return
        except Empty:
            max_rss = getrusage(RUSAGE_SELF).ru_maxrss
            if max_rss > old_max:
                old_max = max_rss
                snapshot = tracemalloc.take_snapshot()
                print(datetime.now(), 'max RSS', max_rss)


def count_prefixes():
    sleep(2)  # Start up time.
    counts = Counter()
    fname = '/usr/share/dict/american-english'
    with open(fname) as words:
        words = list(words)
        for word in words:
            prefix = word[:3]
            counts[prefix] += 1
            sleep(0.0001)
    most_common = counts.most_common(3)
    sleep(3)  # Shut down time.
    return most_common


def main():
    queue = Queue()
    poll_interval = 0.1
    monitor_thread = Thread(target=memory_monitor, args=(queue, poll_interval))
    monitor_thread.start()
    try:
        most_common = count_prefixes()
        print('Top prefixes:', most_common)
    finally:
        queue.put('stop')
        monitor_thread.join()


def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


main()

resource모듈을 사용하면 현재 메모리 사용량을 확인하고 최대 메모리 사용량에서 스냅 샷을 저장할 수 있습니다. 대기열은 주 스레드가 보고서를 인쇄하고 종료 할시기를 메모리 모니터 스레드에 알려줍니다. 실행되면 list()호출 에 사용중인 메모리가 표시됩니다 .

2018-05-29 10:34:34.441334 max RSS 10188
2018-05-29 10:34:36.475707 max RSS 23588
2018-05-29 10:34:36.616524 max RSS 38104
2018-05-29 10:34:36.772978 max RSS 45924
2018-05-29 10:34:36.929688 max RSS 46824
2018-05-29 10:34:37.087554 max RSS 46852
Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
2018-05-29 10:34:56.281262
Top 3 lines
#1: scratches/scratch.py:36: 6527.0 KiB
    words = list(words)
#2: scratches/scratch.py:38: 16.4 KiB
    prefix = word[:3]
#3: scratches/scratch.py:39: 10.1 KiB
    counts[prefix] += 1
19 other: 10.8 KiB
Total allocated size: 6564.3 KiB

Linux를 사용하는 경우 모듈 /proc/self/statm보다 유용 할 수 있습니다 resource.


이것은 훌륭하지만 "count_prefixes ()"내부의 함수가 반환 될 때 간격 동안 만 스냅 샷을 인쇄하는 것 같습니다. 즉, 함수 long_running()내부와 같이 오래 실행되는 호출이 count_prefixes()있는 경우 최대 RSS 값은 long_running()반환 될 때까지 인쇄되지 않습니다 . 아니면 내가 착각합니까?
robguinness

@robguinness라고 착각 한 것 같습니다. memory_monitor()는 별도의 스레드에서 실행 count_prefixes()중이므로 다른 하나에 영향을 줄 수있는 유일한 방법은 GIL과 내가 전달한 메시지 큐뿐입니다 memory_monitor(). count_prefixes()호출 할 때 sleep()스레드 컨텍스트를 전환하도록 권장합니다. 귀하의 경우 long_running()실제로 매우 오래 걸리는되지 당신이 나오기 전까지, 다음 스레드 컨텍스트 전환 할 수 sleep()있는 통화 다시 count_prefixes(). 이해가되지 않으면 새 질문을 게시하고 여기에서 질문에 연결하십시오.
돈 커크비

감사. 새 질문을 게시하고 여기에 링크를 추가하겠습니다. (나는 코드의 독점적 인 부분을 공유 할 수 있기 때문에, 나는 데 문제의 예까지 작업을해야합니다.)
robguinness

31

객체의 메모리 사용량 만보고 싶다면 ( 다른 질문에 대답하십시오 )

모듈 을 포함하는 Pympler 라는 asizeof 모듈이 있습니다.

다음과 같이 사용하십시오 :

from pympler import asizeof
asizeof.asizeof(my_object)

달리 sys.getsizeof, 그것은 당신이 직접 만든 개체에 작동합니다 .

>>> asizeof.asizeof(tuple('bcd'))
200
>>> asizeof.asizeof({'foo': 'bar', 'baz': 'bar'})
400
>>> asizeof.asizeof({})
280
>>> asizeof.asizeof({'foo':'bar'})
360
>>> asizeof.asizeof('foo')
40
>>> asizeof.asizeof(Bar())
352
>>> asizeof.asizeof(Bar().__dict__)
280
>>> help(asizeof.asizeof)
Help on function asizeof in module pympler.asizeof:

asizeof(*objs, **opts)
    Return the combined size in bytes of all objects passed as positional arguments.

1
이 크기는 RSS와 관련이 있습니까?
pg2455

1
@mousecoder : en.wikipedia.org/wiki/RSS_(disambiguation)의 어느 RSS ? 웹 피드? 어떻게?
serv-inc

2
@ serv-inc Resident set size , 비록 Pympler의 소스에서만 언급 할 수 있지만 그 언급은 직접적으로 연결되지 않은 것 같습니다.asizeof
jkmartindale

1
@mousecoder가보고 한 메모리는 asizeofRSS에 기여할 수 있습니다. "관련"이라는 다른 의미가 무엇인지 잘 모르겠습니다.
OrangeDog

1
@ serv-inc 가능한 경우는 매우 구체적 일 수 있습니다. 그러나 하나의 큰 다차원 사전을 측정하는 유스 케이스의 경우 tracemalloc솔루션이 훨씬 빠릅니다
ulkas

22

폭로:

  • Linux에만 해당
  • 현재 프로세스에서 사용한 메모리를 전체 기능으로보고 합니다.

그러나 단순성으로 인해 좋습니다.

import resource
def using(point=""):
    usage=resource.getrusage(resource.RUSAGE_SELF)
    return '''%s: usertime=%s systime=%s mem=%s mb
           '''%(point,usage[0],usage[1],
                usage[2]/1024.0 )

using("Label")진행 상황을보고 싶은 곳에 삽입 하십시오. 예를 들어

print(using("before"))
wrk = ["wasting mem"] * 1000000
print(using("after"))

>>> before: usertime=2.117053 systime=1.703466 mem=53.97265625 mb
>>> after: usertime=2.12023 systime=1.70708 mem=60.8828125 mb

6
"주어진 함수의 메모리 사용량"을 참조하십시오.
Glaslos

보고 usage[2]당신을 찾고 ru_maxrss있는 과정의 부분 인 주민 . 프로세스가 디스크로, 심지어 부분적으로 스왑 된 경우에는별로 도움이되지 않습니다.
Louis

8
resourceWindows에서 작동하지 않는 유닉스 전용 모듈입니다.
Martin

1
의 단위 ru_maxrss(즉, usage[2])는 페이지가 아니라 kB이므로 해당 숫자에을 곱할 필요가 없습니다 resource.getpagesize().
Tey '

1
이것은 나를 위해 아무것도 인쇄하지 않았습니다.
quantumpotato

7

수락 된 답변과 그다음으로 가장 높은 투표 응답이 내 의견으로는 몇 가지 문제가 있기 때문에 작지만 중요한 수정 사항이있는 Ihor B.의 답변에 밀접하게 기반한 답변을 하나 더 제공하고 싶습니다.

이 솔루션은 사용자가 프로파일 실행할 수 있습니다 중 하나 와 함수 호출을 래핑하여 profile, 기능을 호출 하거나 와 함수 / 메소드를 장식으로 @profile장식.

첫 번째 기법은 소스를 망치지 않고 일부 타사 코드를 프로파일 링 할 때 유용하지만, 두 번째 기법은 "깨끗한"기능이며 함수 / 방법의 소스를 수정하지 않아도 더 잘 작동합니다. 프로필을 작성하고 싶습니다.

RSS, VMS 및 공유 메모리를 얻을 수 있도록 출력도 수정했습니다. 나는 "이전"과 "이후"값에 신경 쓰지 않고 델타 만 신경 쓰지 않으므로 값을 제거했습니다 (Ihor B.의 답변과 비교하는 경우).

프로파일 링 코드

# profile.py
import time
import os
import psutil
import inspect


def elapsed_since(start):
    #return time.strftime("%H:%M:%S", time.gmtime(time.time() - start))
    elapsed = time.time() - start
    if elapsed < 1:
        return str(round(elapsed*1000,2)) + "ms"
    if elapsed < 60:
        return str(round(elapsed, 2)) + "s"
    if elapsed < 3600:
        return str(round(elapsed/60, 2)) + "min"
    else:
        return str(round(elapsed / 3600, 2)) + "hrs"


def get_process_memory():
    process = psutil.Process(os.getpid())
    mi = process.memory_info()
    return mi.rss, mi.vms, mi.shared


def format_bytes(bytes):
    if abs(bytes) < 1000:
        return str(bytes)+"B"
    elif abs(bytes) < 1e6:
        return str(round(bytes/1e3,2)) + "kB"
    elif abs(bytes) < 1e9:
        return str(round(bytes / 1e6, 2)) + "MB"
    else:
        return str(round(bytes / 1e9, 2)) + "GB"


def profile(func, *args, **kwargs):
    def wrapper(*args, **kwargs):
        rss_before, vms_before, shared_before = get_process_memory()
        start = time.time()
        result = func(*args, **kwargs)
        elapsed_time = elapsed_since(start)
        rss_after, vms_after, shared_after = get_process_memory()
        print("Profiling: {:>20}  RSS: {:>8} | VMS: {:>8} | SHR {"
              ":>8} | time: {:>8}"
            .format("<" + func.__name__ + ">",
                    format_bytes(rss_after - rss_before),
                    format_bytes(vms_after - vms_before),
                    format_bytes(shared_after - shared_before),
                    elapsed_time))
        return result
    if inspect.isfunction(func):
        return wrapper
    elif inspect.ismethod(func):
        return wrapper(*args,**kwargs)

위의 코드가 다음과 같이 저장되었다고 가정하면 사용 예 profile.py:

from profile import profile
from time import sleep
from sklearn import datasets # Just an example of 3rd party function call


# Method 1
run_profiling = profile(datasets.load_digits)
data = run_profiling()

# Method 2
@profile
def my_function():
    # do some stuff
    a_list = []
    for i in range(1,100000):
        a_list.append(i)
    return a_list


res = my_function()

결과는 아래와 비슷합니다.

Profiling:        <load_digits>  RSS:   5.07MB | VMS:   4.91MB | SHR  73.73kB | time:  89.99ms
Profiling:        <my_function>  RSS:   1.06MB | VMS:   1.35MB | SHR       0B | time:   8.43ms

몇 가지 중요한 최종 메모 :

  1. 이 프로파일 링 방법은 다른 많은 것들이 기계에서 발생할 수 있기 때문에 대략적인 것입니다. 가비지 수집 및 기타 요인으로 인해 델타가 0 일 수도 있습니다.
  2. 알려지지 않은 이유로 매우 짧은 함수 호출 (예 : 1 또는 2ms)은 메모리 사용량이 0으로 표시됩니다. 메모리 통계가 얼마나 자주 업데이트되는지에 대한 하드웨어 / OS (Linux의 기본 랩톱에서 테스트)의 제한 사항이라고 생각합니다.
  3. 예제를 단순하게 유지하기 위해 함수 인수를 사용하지 않았지만 예상대로 작동해야 profile(my_function, arg)합니다.my_function(arg)

7

아래는 간단한 함수 데코레이터로, 함수 호출 전, 함수 호출 후 프로세스가 소비 한 메모리 양과 차이점을 추적 할 수 있습니다.

import time
import os
import psutil


def elapsed_since(start):
    return time.strftime("%H:%M:%S", time.gmtime(time.time() - start))


def get_process_memory():
    process = psutil.Process(os.getpid())
    return process.get_memory_info().rss


def profile(func):
    def wrapper(*args, **kwargs):
        mem_before = get_process_memory()
        start = time.time()
        result = func(*args, **kwargs)
        elapsed_time = elapsed_since(start)
        mem_after = get_process_memory()
        print("{}: memory before: {:,}, after: {:,}, consumed: {:,}; exec time: {}".format(
            func.__name__,
            mem_before, mem_after, mem_after - mem_before,
            elapsed_time))
        return result
    return wrapper

모든 세부 사항을 설명하는 내 블로그 가 있습니다. ( 보관 된 링크 )


4
그것은해야 process.memory_info().rss하지 process.get_memory_info().rss적어도 우분투와 파이썬 3.6. 관련 stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki 2018 년

1
당신은 3.x에 대한 권리입니다. 고객이 최신 버전이 아닌 Python 2.7을 사용하고 있습니다.
Ihor B.

4

아마도 도움이 될 것입니다 :
< 추가 참조 >

pip install gprof2dot
sudo apt-get install graphviz

gprof2dot -f pstats profile_for_func1_001 | dot -Tpng -o profile.png

def profileit(name):
    """
    @profileit("profile_for_func1_001")
    """
    def inner(func):
        def wrapper(*args, **kwargs):
            prof = cProfile.Profile()
            retval = prof.runcall(func, *args, **kwargs)
            # Note use of name from outer scope
            prof.dump_stats(name)
            return retval
        return wrapper
    return inner

@profileit("profile_for_func1_001")
def func1(...)

1

memory_profile을 사용하여 코드 블록 / 함수의 메모리 사용량을 계산하고 함수의 결과를 리턴하는 간단한 예 :

import memory_profiler as mp

def fun(n):
    tmp = []
    for i in range(n):
        tmp.extend(list(range(i*i)))
    return "XXXXX"

코드를 실행하기 전에 메모리 사용량을 계산 한 다음 코드 중 최대 사용량을 계산하십시오.

start_mem = mp.memory_usage(max_usage=True)
res = mp.memory_usage(proc=(fun, [100]), max_usage=True, retval=True) 
print('start mem', start_mem)
print('max mem', res[0][0])
print('used mem', res[0][0]-start_mem)
print('fun output', res[1])

기능을 실행하는 동안 샘플링 지점에서 사용량을 계산합니다.

res = mp.memory_usage((fun, [100]), interval=.001, retval=True)
print('min mem', min(res[0]))
print('max mem', max(res[0]))
print('used mem', max(res[0])-min(res[0]))
print('fun output', res[1])

크레딧 : @skeept

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.