Python 함수 지역 변수는 어떻게 실행되나요? Python 함수 변수 적용에 대한 간략한 분석
이 기사에서 제공하는 내용은 Python 함수의 로컬 변수를 실행하는 방법입니다. Python 함수 변수의 응용에 대한 간략한 분석은 특정 참고 가치가 있습니다. 도움이 필요한 친구가 참고할 수 있기를 바랍니다.
머리말
이틀간 CodeReview에 있을 때 이런 코드를 봤습니다
# 伪代码 import somelib class A(object): def load_project(self): self.project_code_to_name = {} for project in somelib.get_all_projects(): self.project_code_to_name[project] = project ...
의도는 매우 간단합니다. 즉, somelib.get_all_projects
에서 얻은 프로젝트를 self.project_code_to_name somelib.get_all_projects
获取的项目塞入的 self.project_code_to_name
然而印象中这个是有优化空间的,于是提出调整方案:
import somelib class A(object): def load_project(self): project_code_to_name = {} for project in somelib.get_all_projects(): project_code_to_name[project] = project self.project_code_to_name = project_code_to_name ...
方案很简单,就是先定义局部变量 project_code_to_name
,操作完,再赋值到self.project_code_to_name
。
在后面的测试,也确实发现这样是会好点,那么结果知道了,接下来肯定是想探索原因的!
局部变量
其实在网上很多地方,甚至很多书上都有讲过一个观点:访问局部变量速度要快很多,粗看好像好有道理,然后又看到下面贴了一大堆测试数据,虽然不知道是什么,但这是真的屌,记住再说,管他呢!
但是实际上这个观点还是有一定的局限性,并不是放诸四海皆准。所以先来理解下这句话吧,为什么大家都喜欢这样说。
先看段代码理解下什么是局部变量:
#coding: utf8 a = 1 def test(b): c = 'test' print a # 全局变量 print b # 局部变量 print c # 局部变量 test(3)
# 输出 1 3 test
简单来说,局部变量就是只作用于所在的函数域,超过作用域就被回收
理解了什么是局部变量,就需要谈谈 Python 函数 和 局部变量 的爱恨情仇,因为如果不搞清楚这个,是很难感受到到底快在哪里;
为避免枯燥,以上述的代码来阐述吧,顺便附上 test 函数执行 的 dis 的解析:
# CALL_FUNCTION 5 0 LOAD_CONST 1 ('test') 3 STORE_FAST 1 (c) 6 6 LOAD_GLOBAL 0 (a) 9 PRINT_ITEM 10 PRINT_NEWLINE 7 11 LOAD_FAST 0 (b) 14 PRINT_ITEM 15 PRINT_NEWLINE 8 16 LOAD_FAST 1 (c) 19 PRINT_ITEM 20 PRINT_NEWLINE 21 LOAD_CONST 0 (None) 24 RETURN_VALUE
在上图中比较清楚能看到 a、b、c 分别对应的指令块,每一块的第一行都是 LOAD_XXX
,顾名思义,是说明这些变量是从哪个地方获取的。
LOAD_GLOBAL
毫无疑问是全局,但是 LOAD_FAST
是什么鬼?似乎应该叫LOAD_LOCAL
吧?
然而事实就是这么神奇,人家就真的是叫 LOAD_FAST
,因为局部变量是从一个叫 fastlocals
的数组里面读,所以名字也就这样叫了(我猜的)。
那么主角来了,我们要重点理解这个,因为这个确实还挺有意思。
Python 函数执行
Python 函数的构建和运行,说复杂不复杂,说简单也不简单,因为它需要区分很多情况,比方说需要区分 函数 和 方法,再而区分是有无参数,有什么参数,有木有变长参数,有木有关键参数。
全部展开仔细讲是不可能的啦,不过可以简单图解下大致的流程(忽略参数变化细节):
一路顺流而下,直达 fast_function
,它在这里的调用是:
// ceval.c -> call_function x = fast_function(func, pp_stack, n, na, nk);
参数解释下:
func: 传入的
test
;pp_stack: 近似理解调用栈 (py方式);
na: 位置参数个数;
nk: 关键字个数;
n = na + 2 * nk;
那么下一步就看看 fast_function
要做什么吧。
初始化一波
定义 co 来存放 test 对象里面的
func_code
定义 globals 来存放 test 对象里面的
func_globals
(字典)定义 argdefs 来存放 test 对象里面的
func_defaults
(构建函数时的关键字参数默认值)
来个判断,如果 argdefs 为空
&& 传入的位置参数个数 == 函数定义时候的位置形参个数
&& 没有传入关键字参数
那就
用
当前线程状态
、co
、globals
来新建栈对象f
;定义
fastlocals
( fastlocals = f->f_localsplus; );把 传入的参数全部塞进去
fastlocals
那么问题来了,怎么塞?怎么找到传入了什么鬼参数:这个问题还是只能有 dis
来解答:
我们知道现在这步是在 CALL_FUNCTION
里面进行的,所以塞参数的动作,肯定是在此之前的,所以:
12 27 LOAD_NAME 2 (test) 30 LOAD_CONST 4 (3) 33 CALL_FUNCTION 1 36 POP_TOP 37 LOAD_CONST 1 (None) 40 RETURN_VALUE
在 CALL_FUNCTION
上面就看到 30 LOAD_CONST 4 (3)
,有兴趣的童鞋可以试下多传几个参数,就会发现传入的参数,是依次通过LOAD_CONST
// fast_function 函数 fastlocals = f->f_localsplus; stack = (*pp_stack) - n; for (i = 0; i 🎜계획은 매우 간단합니다. 즉, 먼저 로컬 변수 <code>project_code_to_name</code>을 선택한 다음 <code>self.project_code_to_name</code>에 값을 할당합니다. 🎜🎜후속 테스트에서 이것이 더 낫다는 것을 알았습니다. 이제 결과가 알려졌으니 다음에는 그 이유를 꼭 살펴보고 싶습니다! 🎜🎜🎜로컬 변수🎜🎜🎜사실 인터넷의 여러 곳, 심지어 많은 책에서도 언급되는 관점이 있습니다. 🎜로컬 변수에 액세스하는 것이 훨씬 빠릅니다🎜 언뜻 보기에 말이 되는 것 같습니다. 그러다가 아래 글을 봤는데, 테스트 데이터가 많네요. 뭔지는 모르겠지만 정말 멋지네요. 걱정하지 마세요. 🎜🎜그러나 사실 이 견해에는 여전히 일정한 한계가 있으며 보편적으로 적용 가능하지는 않습니다. 그럼 먼저 이 문장을 이해하고 왜 모두가 이 문장을 좋아하는지 알아보겠습니다. 🎜🎜지역 변수가 무엇인지 이해하려면 먼저 코드를 살펴보세요. 🎜<pre class="brush:php;toolbar:false"># CALL_FUNCTION 5 0 LOAD_CONST 1 ('test') 3 STORE_FAST 1 (c)
# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支: PREDICTED_WITH_ARG(STORE_FAST); TARGET(STORE_FAST) { v = POP(); SETLOCAL(oparg, v); FAST_DISPATCH(); } # 因为有涉及到宏,就顺便给出: #define GETLOCAL(i) (fastlocals[i]) #define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \ GETLOCAL(i) = value; \ Py_XDECREF(tmp); } while (0)
// 我感觉往回看的概率超低的,直接给出算了 def test(b): c = 'test' print b # 局部变量 print c # 局部变量
22 LOAD_FAST 1 (c)
LOAD_GLOBAL
은 의심할 여지없이 전역적이지만 LOAD_FAST
는 도대체 무엇인가요? LOAD_LOCAL
로 불러야 할 것 같죠? 🎜🎜그러나 진실은 너무나 마술적이어서 실제로는 LOAD_FAST
라고 불립니다. 지역 변수는 fastlocals
라는 배열에서 읽혀지기 때문에 이름도 그렇게 불립니다( 추측합니다). 🎜🎜이제 주인공이 왔으니 이것을 이해하는데 집중해야 합니다. 왜냐하면 이것은 정말 흥미롭기 때문입니다. 🎜🎜🎜파이썬 함수 실행🎜🎜🎜파이썬 함수의 구성과 연산은 복잡하다고 할 수도 있고 간단하다고 할 수도 있습니다. 함수나 메소드 등 여러 가지 상황을 구분해야 하고, 매개변수가 있는지 없는지 구분해야 하기 때문입니다. 가변 길이 매개변수가 있나요? 주요 매개변수가 있나요? 🎜🎜모든 것을 자세히 설명하는 것은 불가능하지만 일반적인 프로세스를 간략하게 설명할 수 있습니다(매개변수 변경 세부 사항 무시): 🎜🎜
fast_function
에 대한 호출은 다음과 같습니다. 🎜# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支: TARGET(LOAD_FAST) { x = GETLOCAL(oparg); if (x != NULL) { Py_INCREF(x); PUSH(x); FAST_DISPATCH(); } format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); break; }
- 🎜func: Incoming
test
;🎜 - 🎜pp_stack: 호출 스택에 대한 대략적인 이해(py 모드);🎜
- 🎜na: 위치 매개변수 수;🎜
- 🎜nk: 키워드 수;🎜
- 🎜n = na + 2 * nk;🎜
fast_function
이 Bar에서 수행해야 하는 작업을 확인하는 것입니다. . 🎜🎜wave 초기화🎜- 🎜co를 정의하여
func_code
를 테스트 개체에 저장합니다🎜 - 🎜전역 정의 테스트 개체에
func_globals
(사전)를 저장하려면🎜 - 🎜인수 정의를 정의하여
func_defaults
(함수 빌드 시 기본 키워드 매개변수)를 테스트에 저장하세요. object Value)🎜
argdefs가 비어 있는지
&& 전달된 위치 매개변수 수 == 함수가 실행될 때 위치 매개변수 수 정의되었습니다
&& 키워드 매개변수가 전달되지 않았습니다
🎜🎜Then🎜- 🎜
현재 스레드 상태 사용
,co
,globals
를 사용하여 새 스택 개체f
를 생성합니다.🎜 - 🎜
fastlocals 정의 code> ( fastlocals = f->f_localsplus; );🎜
- 🎜
fastlocals
🎜
dis
로만 답변할 수 있습니다. 🎜🎜우리는 이 단계가 CALL_FUNCTION
에서 수행된다는 것을 알고 있으므로 매개변수는 다음과 같습니다. 연결됨 이 작업은 이 이전에 수행되어야 하므로: 🎜class SS(object): def __init__(self): self.fuck = {} def test(self): print self.fuck
CALL_FUNCTION
에서 30 LOAD_CONST를 볼 수 있습니다. 몇 가지 매개변수를 전달하면 전달된 매개변수가 순서대로 로드되는 것을 확인할 수 있습니다. <code>LOAD_CONST
를 통해 매개변수를 찾는 방법에 대한 문제가 분명해집니다.// fast_function 函数 fastlocals = f->f_localsplus; stack = (*pp_stack) - n; for (i = 0; i <p>这里出现的 n 还记得怎么来的吗?回顾上面有个 <code>n = na + 2 * nk;</code> ,能想起什么吗?</p><p>其实这个地方就是简单的通过将 <code>pp_stack</code> 偏移 n 字节 找到一开始塞入参数的位置。</p><p>那么问题来了,如果 n 是 位置参数个数 + 关键字参数,那么 2 * nk 是什么意思?其实这答案很简单,那就是 关键字参数字节码 是属于带参数字节码, 是占 2字节。</p><p>到了这里,栈对象 <code>f</code> 的 <code>f_localsplus</code> 也登上历史舞台了,只是此时的它,还只是一个未经人事的少年,还需历练。</p><p>做好这些动作,终于来到真正执行函数的地方了: <code>PyEval_EvalFrameEx</code>,在这里,需要先交代下,有个和 <code>PyEval_EvalFrameEx</code> 很像的,叫 <code>PyEval_EvalCodeEx</code>,虽然长得像,但是人家干得活更多了。</p><p>请看回前面的 <code>fast_function</code> 开始那会有个判断,我们上面说得是判断成立的,也就是最简单的函数执行情况。如果函数传入多了关键字参数或者其他情况,那就复杂很多了,此时就需要由 <code>PyEval_EvalCodeEx</code> 处理一波,再执行 <code>PyEval_EvalFrameEx</code>。</p><p><code>PyEval_EvalFrameEx</code> 主要的工作就是解析字节码,像刚才的那些 <code>CALL_FUNCTION</code>,<code>LOAD_FAST</code> 等等,都是由它解析和处理的,它的本质就是一个死循环,然后里面有一堆 <code>swith - case</code>,这基本也就是 Python 的运行本质了。</p><h4 id="f-localsplus-存-和-取">f_localsplus 存 和 取</h4><p>讲了这么长的一堆,算是把 Python 最基本的 函数调用过程简单扫了个盲,现在才开始探索主题。。</p><p>为了简单阐述,直接引用名词:<code>fastlocals</code>, 其中 <code>fastlocals = f->f_localsplus</code></p><p>刚才只是简单看到了,Python 会把传入的参数,以此塞入 <code>fastlocals</code> 里面去,那么毋庸置疑,传入的位置参数,必然属于局部变量了,那么关键字参数呢?那肯定也是局部变量,因为它们都被特殊对待了嘛。</p><p>那么除了函数参数之外,必然还有函数内部的赋值咯? 这块字节码也一早在上面给出了:</p><pre class="brush:php;toolbar:false"># CALL_FUNCTION 5 0 LOAD_CONST 1 ('test') 3 STORE_FAST 1 (c)
这里出现了新的字节码 STORE_FAST
,一起来看看实现把:
# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支: PREDICTED_WITH_ARG(STORE_FAST); TARGET(STORE_FAST) { v = POP(); SETLOCAL(oparg, v); FAST_DISPATCH(); } # 因为有涉及到宏,就顺便给出: #define GETLOCAL(i) (fastlocals[i]) #define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \ GETLOCAL(i) = value; \ Py_XDECREF(tmp); } while (0)
简单解释就是,将 POP() 获得的值 v,塞到 fastlocals 的 oparg 位置上。此处,v 是 "test", oparg 就是 1。用图表示就是:
有童鞋可能会突然懵了,为什么突然来了个 b
?我们又需要回到上面看 test 函数是怎样定义的:
// 我感觉往回看的概率超低的,直接给出算了 def test(b): c = 'test' print b # 局部变量 print c # 局部变量
看到函数定义其实都应该知道了,因为 b
是传的参数啊,老早就塞进去了~
那存储知道了,那么怎么取呢?同样也是这段代码的字节码:
22 LOAD_FAST 1 (c)
虽然这个用脚趾头想想都知道原理是啥,但公平起见还是给出相应的代码:
# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支: TARGET(LOAD_FAST) { x = GETLOCAL(oparg); if (x != NULL) { Py_INCREF(x); PUSH(x); FAST_DISPATCH(); } format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); break; }
直接用 GETLOCAL
通过索引在数组里取值了。
到了这里,应该也算是把 f_localsplus
讲明白了。这个地方不难,其实一般而言是不会被提及到这个,因为一般来说忽略即可了,但是如果说想在性能方面讲究点,那么这个小知识就不得忽视了。
变量使用姿势
因为是面向对象,所以我们都习惯了通过 class
的方式,对于下面的使用方式,也是随手就来:
class SS(object): def __init__(self): self.fuck = {} def test(self): print self.fuck
这种方式一般是没什么问题的,也很规范。到那时如果是下面的操作,那就有问题了:
class SS(object): def __init__(self): self.fuck = {} def test(self): num = 10 for i in range(num): self.fuck[i] = i
这段代码的性能损耗,会随着 num 的值增大而增大, 如果下面循环中还要涉及到更多类属性的读取、修改等等,那影响就更大了
这个类属性如果换成 全局变量,也会存在类似的问题,只是说在操作类属性会比操作全局变量要频繁得多。
我们直接看看两者的差距有多大把?
import timeit class SS(object): def test(self): num = 100 self.fuck = {} # 为了公平,每次执行都同样初始化新的 {} for i in range(num): self.fuck[i] = i def test_local(self): num = 100 fuck = {} # 为了公平,每次执行都同样初始化新的 {} for i in range(num): fuck[i] = i self.fuck = fuck s = SS() print timeit.timeit(stmt=s.test_local) print timeit.timeit(stmt=s.test)
通过上图可以看出,随着 num 的值越大,for 循环的次数就越多,那么两者的差距也就越大了。
那么为什么会这样,也是在字节码可以看出写端倪:
// s.test >> 28 FOR_ITER 19 (to 50) 31 STORE_FAST 2 (i) 8 34 LOAD_FAST 2 (i) 37 LOAD_FAST 0 (self) 40 LOAD_ATTR 0 (hehe) 43 LOAD_FAST 2 (i) 46 STORE_SUBSCR 47 JUMP_ABSOLUTE 28 >> 50 POP_BLOCK // s.test_local >> 25 FOR_ITER 16 (to 44) 28 STORE_FAST 3 (i) 14 31 LOAD_FAST 3 (i) 34 LOAD_FAST 2 (hehe) 37 LOAD_FAST 3 (i) 40 STORE_SUBSCR 41 JUMP_ABSOLUTE 25 >> 44 POP_BLOCK 15 >> 45 LOAD_FAST 2 (hehe) 48 LOAD_FAST 0 (self) 51 STORE_ATTR 1 (hehe)
上面两段就是两个方法的 for block
内容,大家对比下就会知道, s.test
相比于 s.test_local
, 多了个 LOAD_ATTR
放在 FOR_ITER
和 POP_BLOCK
之间。
这说明什么呢? 这说明,在每次循环时,s.test
都需要 LOAD_ATTR
,很自然的,我们需要看看这个是干什么的:
TARGET(LOAD_ATTR) { w = GETITEM(names, oparg); v = TOP(); x = PyObject_GetAttr(v, w); Py_DECREF(v); SET_TOP(x); if (x != NULL) DISPATCH(); break; } # 相关宏定义 #define GETITEM(v, i) PyTuple_GetItem((v), (i))
这里出现了一个陌生的变量 name
, 这是什么?其实这个就是每个 codeobject 所维护的一个 名字数组,基本上每个块所使用到的字符串,都会在这里面存着,同样也是有序的:
// PyCodeObject 结构体成员 PyObject *co_names; /* list of strings (names used) */
那么 LOAD_ATTR
的任务就很清晰了:先从名字列表里面取出字符串,结果就是 "hehe", 然后通过 PyObject_GetAttr 去查找,在这里就是在 s 实例中去查找。
且不说查找效率如何,光多了这一步,都能失之毫厘差之千里了,当然这是在频繁操作次数比较多的情况下。
所以我们在一些会频繁操作 类/实例属性
的情况下,应该是先把 属性
取出来存到 局部变量
,然后用 局部变量
来完成操作。最后视情况把变动更新到属性
上。
最后
其实相比变量,在函数和方法的使用上面更有学问,更值得探索,因为那个原理和表面看起来差别更大,下次有机会再探讨。平时工作多注意下,才能使得我们的 PY 能够稍微快点点点点点。
相关推荐:
위 내용은 Python 함수 지역 변수는 어떻게 실행되나요? Python 함수 변수 적용에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











PHP는 주로 절차 적 프로그래밍이지만 객체 지향 프로그래밍 (OOP)도 지원합니다. Python은 OOP, 기능 및 절차 프로그래밍을 포함한 다양한 패러다임을 지원합니다. PHP는 웹 개발에 적합하며 Python은 데이터 분석 및 기계 학습과 같은 다양한 응용 프로그램에 적합합니다.

PHP는 웹 개발 및 빠른 프로토 타이핑에 적합하며 Python은 데이터 과학 및 기계 학습에 적합합니다. 1.PHP는 간단한 구문과 함께 동적 웹 개발에 사용되며 빠른 개발에 적합합니다. 2. Python은 간결한 구문을 가지고 있으며 여러 분야에 적합하며 강력한 라이브러리 생태계가 있습니다.

Python은 부드러운 학습 곡선과 간결한 구문으로 초보자에게 더 적합합니다. JavaScript는 가파른 학습 곡선과 유연한 구문으로 프론트 엔드 개발에 적합합니다. 1. Python Syntax는 직관적이며 데이터 과학 및 백엔드 개발에 적합합니다. 2. JavaScript는 유연하며 프론트 엔드 및 서버 측 프로그래밍에서 널리 사용됩니다.

VS 코드는 파이썬을 작성하는 데 사용될 수 있으며 파이썬 애플리케이션을 개발하기에 이상적인 도구가되는 많은 기능을 제공합니다. 사용자는 다음을 수행 할 수 있습니다. Python 확장 기능을 설치하여 코드 완료, 구문 강조 및 디버깅과 같은 기능을 얻습니다. 디버거를 사용하여 코드를 단계별로 추적하고 오류를 찾아 수정하십시오. 버전 제어를 위해 git을 통합합니다. 코드 서식 도구를 사용하여 코드 일관성을 유지하십시오. 라인 도구를 사용하여 잠재적 인 문제를 미리 발견하십시오.

VS 코드는 Windows 8에서 실행될 수 있지만 경험은 크지 않을 수 있습니다. 먼저 시스템이 최신 패치로 업데이트되었는지 확인한 다음 시스템 아키텍처와 일치하는 VS 코드 설치 패키지를 다운로드하여 프롬프트대로 설치하십시오. 설치 후 일부 확장은 Windows 8과 호환되지 않을 수 있으며 대체 확장을 찾거나 가상 시스템에서 새로운 Windows 시스템을 사용해야합니다. 필요한 연장을 설치하여 제대로 작동하는지 확인하십시오. Windows 8에서는 VS 코드가 가능하지만 더 나은 개발 경험과 보안을 위해 새로운 Windows 시스템으로 업그레이드하는 것이 좋습니다.

PHP는 1994 년에 시작되었으며 Rasmuslerdorf에 의해 개발되었습니다. 원래 웹 사이트 방문자를 추적하는 데 사용되었으며 점차 서버 측 스크립팅 언어로 진화했으며 웹 개발에 널리 사용되었습니다. Python은 1980 년대 후반 Guidovan Rossum에 의해 개발되었으며 1991 년에 처음 출시되었습니다. 코드 가독성과 단순성을 강조하며 과학 컴퓨팅, 데이터 분석 및 기타 분야에 적합합니다.

vs 코드에서는 다음 단계를 통해 터미널에서 프로그램을 실행할 수 있습니다. 코드를 준비하고 통합 터미널을 열어 코드 디렉토리가 터미널 작업 디렉토리와 일치하는지 확인하십시오. 프로그래밍 언어 (예 : Python의 Python Your_file_name.py)에 따라 실행 명령을 선택하여 성공적으로 실행되는지 여부를 확인하고 오류를 해결하십시오. 디버거를 사용하여 디버깅 효율을 향상시킵니다.

VS 코드 확장은 악의적 인 코드 숨기기, 취약성 악용 및 합법적 인 확장으로 자위하는 등 악성 위험을 초래합니다. 악의적 인 확장을 식별하는 방법에는 게시자 확인, 주석 읽기, 코드 확인 및주의해서 설치가 포함됩니다. 보안 조치에는 보안 인식, 좋은 습관, 정기적 인 업데이트 및 바이러스 백신 소프트웨어도 포함됩니다.
