"최소한의 놀라움" 및 가변 디폴트
Python을 오랫동안 만지작거리던 사람은 다음 문제로 인해 물린 적이 있다(또는 갈기갈기 찢어진 적이 있습니다.
def foo(a=[]):
a.append(5)
return a
Python 이 Python novices의 한 요소만 합니다.[5]
그 대신 결과는 매우 다르며 (초보자에게는) 매우 놀랍습니다.
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
제 매니저는 이 기능을 처음 접한 적이 있는데, 이 기능을 언어의 "극적 디자인 결함"이라고 불렀습니다.나는 그 행동에 근본적인 설명이 있다고 대답했고, 만약 당신이 내면을 이해하지 못한다면 그것은 정말로 매우 곤혹스럽고 예상 밖이라고 대답했다.그러나 다음 질문에는 대답할 수 없었습니다. 함수 실행이 아닌 함수 정의에서 default 인수를 바인딩하는 이유는 무엇입니까?경험된 행동이 실용적일지는 의문입니다(버그를 키우지 않고 C에서 정적 변수를 실제로 사용한 사람은 누구입니까?)
편집:
바젝은 흥미로운 예를 들었다.귀하의 의견과 특히 Utaal의 의견과 함께 다음과 같은 사항을 자세히 설명했습니다.
>>> def a():
... print("a executed")
... return []
...
>>>
>>> def b(x=a()):
... x.append(5)
... print(x)
...
a executed
>>> b()
[5]
>>> b()
[5, 5]
저에게 있어 설계 결정은 파라미터의 범위를 어디에 둘 것인가와 관련된 것으로 보입니다. 즉, 함수의 내부인가, 함수와 함께 있는가?
에서 바인딩을 하는 것은 '바인딩'을 합니다.x
정의되어 있지 깊은 을 호출했을, 는 됩니다.def
라인은 정의 시 바인딩의 일부(함수 객체의)가 발생하고 함수 호출 시 일부(기본 파라미터의 구성)가 발생한다는 점에서 "확장"됩니다.
실제 동작은 보다 일관성이 있습니다.즉, 그 라인이 실행될 때 그 라인의 모든 것이 평가됩니다.즉, 함수 정의에서 평가됩니다.
사실 이것은 디자인상의 결함도 아니고, 내부나 퍼포먼스 때문도 아닙니다.이는 단순히 Python의 함수가 코드 조각뿐만 아니라 퍼스트 클래스 객체라는 사실에서 비롯됩니다.
이렇게 생각하면 함수는 그 정의에 따라 평가되는 객체입니다.기본 파라미터는 일종의 "멤버 데이터"이기 때문에 그 상태가 다른 객체와 마찬가지로 콜 간에 바뀔 수 있습니다.
어쨌든 effbot(Fredrik Lundh)은 Python의 Default Parameter Values에서 이 동작의 이유에 대해 매우 잘 설명합니다.매우 명확하다는 것을 깨달았습니다.그리고 함수 오브젝트의 구조에 대해 더 잘 알고 싶다면 읽어보기를 권장합니다.
다음과 같은 코드가 있다고 가정합니다.
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
은 첫 입니다.("apples", "bananas", "loganberries")
하지만 나중에 코드에서 제가 이런 일을 한다고 가정해봅시다
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
디폴트 파라미터가 함수 선언이 아닌 함수 실행으로 바인드되어 있다면 과일이 변경되었음을 알게 되면 (매우 나쁜 방법으로) 놀랄 것입니다.은, 「IMO」가 「IMO」라고 하는 입니다.foo
이치노
진짜 문제는 가변 변수에 있습니다.모든 언어에는 어느 정도 이 문제가 있습니다.여기 질문이 있습니다.Java에 다음 코드가 있다고 가정합니다.
StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) ); // does this work?
그럼 제 는 제, 음, 음, 음의 을 사용하나요?StringBuffer
키를 지도에 배치했을 때 또는 키를 참조로 저장합니까?어느 쪽이든, 누군가는 놀라워한다; 그 물건을 밖으로 꺼내려고 했던 사람은Map
입력한 것과 동일한 값을 사용하거나 사용하는 키가 맵에 넣었을 때 사용한 개체와 문자 그대로 동일한 개체인데도 개체를 검색할 수 없는 것처럼 보이는 사용자(실제로 이것이 Python이 변환 가능한 내장 데이터 유형을 사전 키로 사용하는 것을 허용하지 않는 이유입니다).
당신의 예는 Python의 신참자들이 놀라움을 금치 못할 좋은 경우 중 하나입니다.하지만 만약 우리가 이것을 "고정"한다면, 그것은 그들이 대신 물리는 다른 상황을 만들어 낼 뿐이고, 그것은 훨씬 더 직관적이지 않을 것입니다.게다가 이것은 가변 변수를 다룰 때 항상 해당됩니다.누군가 쓰고 있는 코드에 따라 직관적으로 어느 한쪽 또는 반대쪽의 동작을 예상할 수 있는 경우가 항상 있습니다.
저는 개인적으로 Python의 현재 접근 방식이 마음에 듭니다.기본 함수 인수는 함수가 정의되어 있을 때 평가되며 해당 객체는 항상 기본입니다.빈 목록을 사용하여 특수 케이스를 만들 수 있을 것 같은데, 그런 특수 케이스를 사용하면 훨씬 더 놀라움을 줄 수 있을 거야. 역호환성은 말할 것도 없고.
문서의 관련 부분:
기본 파라미터 값은 함수 정의가 실행될 때 왼쪽에서 오른쪽으로 평가됩니다.즉, 함수를 정의할 때 식을 한 번 평가하여 각 콜에 대해 동일한 "사전 계산된" 값이 사용됨을 의미합니다.이는 기본 파라미터가 목록이나 사전과 같은 가변 객체일 때 특히 중요합니다. 함수가 개체를 수정하는 경우(예를 들어 목록에 항목을 추가하는 경우) 기본값은 실제로 변경됩니다.이것은 일반적으로 의도된 것이 아니다.이 문제를 해결하는 방법은
None
디폴트로서 기능 본문에서 명시적으로 테스트합니다.예를 들어 다음과 같습니다.def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of the zoo") return penguin
Python 인터프리터 내부 작업에 대해 아무것도 모르기 때문에(또한 컴파일러나 인터프리터 전문가도 아닙니다), 만약 제가 어떤 것이든 불감정적이거나 불가능한 것을 제안해도 저를 비난하지 마세요.
python 객체가 변형이 가능한 경우 기본 인수 등을 설계할 때 이 점을 고려해야 한다고 생각합니다.목록을 인스턴스화할 때:
a = []
참조되는 새 목록을 얻을 수 있습니다.a
.
?가합니까?a=[]
def x(a=[]):
호출이 아닌 함수 정의에서 새 목록을 인스턴스화합니까?사용자가 인수를 제공하지 않으면 새 목록을 인스턴스화하여 발신자가 작성한 것처럼 사용합니다.저는 이것이 애매하다고 생각합니다.
def x(a=datetime.datetime.now()):
user, user, " " " " " 。a
합니다.x
로 기본 "첫assignment」는 「assignment」로 되어 있습니다).datetime.now()
을 참조)한편 사용자가 정의 시간 매핑을 원하는 경우 다음과 같이 쓸 수 있습니다.
b = datetime.datetime.now()
def x(a=b):
알아, 알아. 이제 끝이야.또는 Python은 정의 시간 바인딩을 강제하기 위한 키워드를 제공할 수 있습니다.
def x(static a=b):
그 이유는 단순히 코드가 실행될 때 바인딩이 완료되고 함수 정의가 실행되기 때문입니다.함수가 정의되어 있는 경우.
비교:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
이 코드는 완전히 같은 예기치 않은 사건으로 인해 발생합니다. 바나나는 클래스 속성이기 때문에 여기에 항목을 추가하면 해당 클래스의 모든 인스턴스에 추가됩니다.이유는 똑같다.
단지 "How It Works"일 뿐이며, 함수 케이스에서 다르게 동작시키는 것은 복잡할 수 있습니다.클래스 케이스에서는 클래스 코드를 유지한 채 오브젝트 생성 시 실행해야 하기 때문에 오브젝트 인스턴스화가 불가능하거나 적어도 속도가 많이 느려질 수 있습니다.
네, 의외예요.하지만 일단 돈이 떨어지면, Python의 일반적인 작동 방식과 완벽하게 맞아떨어집니다.사실, 이것은 좋은 교육 보조 도구입니다. 그리고 왜 이런 일이 일어나는지 알게 되면, 여러분은 비단뱀을 훨씬 더 잘 울게 될 것입니다.
그것은 좋은 Python 튜토리얼에서 두드러지게 다루어져야 한다고 말했다.왜냐하면 당신이 언급했듯이, 모든 사람들이 조만간 이 문제에 직면하기 때문입니다.
당신은 왜 반성하지 않나요?
Python이 제공하는 통찰력 있는 자기성찰(Inspective Inspection)을 수행한 사람이 아무도 없다는 사실에 정말 놀랐습니다.2
★★★★★★★★★★★★★★★★★」3
적용)을 클릭합니다.
함수가 때마침func
다음과 같이 합니다.
>>> def func(a = []):
... a.append(5)
을 발견했을 때 컴파일하여 Python을 .code
이 함수의 객체.이 컴파일 스텝이 실행되는 동안 Python은 평가*한 후 기본 인수(여기서는 빈 목록)를 함수 오브젝트 자체에 저장합니다.상위의 답변과 같이:a
이제 함수의 구성원으로 간주할 수 있습니다.func
.
이제 목록을 함수 개체 내에서 확장하는 방법을 검토하기 위해 이전과 이후를 검토하겠습니다.사용하고 있다Python 3.x
Python Python 2」를 사용).__defaults__
★★★★★★★★★★★★★★★★★」func_defaults
Python 2 、 Python 2 、 Python 2 、 Python 2 、 Python 2 、 Python 2 。
실행 전 기능:
>>> def func(a = []):
... a.append(5)
...
은 이 한 후 기본 (Python)를.a = []
여기서) 함수 객체의 Atribute에 그것들을 주입합니다(관련 섹션: Callables).
>>> func.__defaults__
([],)
빈 는 " "의 .__defaults__
★★★★★★ 。
실행 후 기능:
이제 다음 기능을 실행합니다.
>>> func()
__defaults__
시다:
>>> func.__defaults__
([5],)
깜짝 놀랐어요?개체 내부의 값이 변경됩니다!함수에 대한 연속 호출이 포함된 함수에 추가됩니다.list
★★★★★★★★★★★★★★★★★★:
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
즉, 이 'flaw'가 발생하는 이유는 기본 인수가 함수 객체의 일부이기 때문입니다.이상할 거 없어, 그냥 좀 의외일 뿐이야.
에 대처하기 위한 방법은 '이러한 방법'을하는 것입니다.None
한 후 본문에서 합니다.
def func(a = None):
# or: a = [] if a is None else a
if a is None:
a = []
매번 되지 않은 빈 .a
.
기재되어 있는 됩니다.__defaults__
함수에 사용되는 것과 동일합니다.func
다시 돌려주시면 .id
a
기능 본체 내부에서 사용됩니다.다음 이 과 비교해보세요.__defaults__
(위치)[0]
__defaults__
어떻게 알 수 이러한 리스트인스턴스를 실제로 참조하고 있는 것을 알 수 있습니다.
>>> def func(a = []):
... a.append(5)
... return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
모두 자기 성찰의 힘으로!
* 함수를 컴파일하는 동안 Python이 기본 인수를 평가하는지 확인하려면 다음을 실행합니다.
def bar(a=input('Did you just see me without calling the function?')):
pass # use raw_input in Py2
input()
되어 함수의 됩니다.bar
들어졌졌다다
예전에는 런타임에 개체를 만드는 것이 더 나은 방법이라고 생각했습니다.지금은 잘 모르겠어요. 왜냐하면 당신은 몇 가지 유용한 기능을 잃었지만, 단순히 초보자의 혼란을 막기 위해서라도 그럴 가치가 있을 수 있어요.이렇게 하면 다음과 같은 단점이 있습니다.
1. 퍼포먼스
def foo(arg=something_expensive_to_compute())):
...
콜 타임 평가가 사용되고 있는 경우, 인수 없이 함수를 사용할 때마다 고가의 함수가 호출됩니다.각 콜에 고액의 요금을 지불하거나 수동으로 값을 외부에 캐싱하여 네임스페이스를 오염시키고 세부사항을 추가할 수 있습니다.
2. 바운드 파라미터의 강제 적용
유용한 트릭은 람다가 생성될 때 람다의 매개변수를 변수의 현재 바인딩에 바인딩하는 것입니다.예를 들어 다음과 같습니다.
funcs = [ lambda i=i: i for i in range(10)]
0, 1, 2, 3...을 반환하는 함수 목록이 반환됩니다..i
i의 호출 시간 값에 따라 모든 함수가 반환된 목록을 얻을 수 있습니다.9
.
그렇지 않으면 이를 구현하는 유일한 방법은 i 바운드로 추가 폐쇄를 작성하는 것입니다. 즉, 다음과 같습니다.
def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]
3. 자기성찰
코드를 고려합니다.
def foo(a='test', b=100, c=[]):
print a,b,c
및는 '인수'를 하여 얻을 수 .inspect
module,즉 '모듈'입니다.
>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))
이 정보는 문서 생성, 메타프로그래밍, 데코레이터 등에 매우 유용합니다.
다음으로 디폴트의 동작을 다음과 같이 변경할 수 있다고 합니다.
_undefined = object() # sentinel value
def foo(a=_undefined, b=_undefined, c=_undefined)
if a is _undefined: a='test'
if b is _undefined: b=100
if c is _undefined: c=[]
그러나 우리는 성찰하고 기본 주장이 무엇인지 볼 수 있는 능력을 잃었습니다.물체는 만들어지지 않았기 때문에 실제로 함수를 호출하지 않고는 구할 수 없습니다.우리가 할 수 있는 최선의 방법은 소스코드를 저장하고 문자열로 되돌리는 것입니다.
파이썬 방어 5점
심플성:동작은 다음과 같은 점에서 간단합니다.대부분의 사람들은 몇 번이 아니라 한 번만 이 함정에 빠진다.
일관성:Python은 항상 이름이 아닌 개체를 전달합니다.기본 파라미터는 (함수 본문이 아닌) 함수 헤더의 일부입니다.따라서 이 값은 함수 호출 시간이 아닌 모듈 로드 시간(및 모듈 로드 시간에만)에 평가해야 합니다.
유용성:Frederik Lundh가 "Python의 기본 파라미터 값"에 대해 설명하면서 지적했듯이 현재 동작은 고급 프로그래밍에 매우 유용할 수 있습니다(적게 사용).
충분한 문서:가장 기본적인 Python 문서인 튜토리얼에서 이 문제는 "함수의 정의에 관한 자세한 사항" 섹션의 첫 번째 하위 섹션에서 "중요 경고"로 크게 발표됩니다.경고문에는 굵은 글씨까지 사용되는데, 이는 머리글자 이외에는 거의 적용되지 않습니다.RTFM: 상세한 매뉴얼을 읽어주세요.
메타학습: 실제로 함정에 빠지는 것은 매우 도움이 되는 순간입니다(적어도 성찰적인 학습자일 경우). 그 후 위의 "일관성"을 더 잘 이해할 수 있고 Python에 대해 많은 것을 배울 수 있기 때문입니다.
이 동작은 다음과 같이 간단하게 설명할 수 있습니다.
- 함수(클래스 등) 선언은 한 번만 실행되며 모든 기본값개체가 생성됩니다.
- 모든 것이 참조로 통과된다.
그래서:
def x(a=0, b=[], c=[], d=0):
a = a + 1
b = b + [1]
c.append(1)
print a, b, c
a
않음 - 객체를 함 - 됨 - int int - "b
않음 -새됩니다.c
변경 - 동일한 개체에 대해 작업이 수행되고 인쇄됩니다.
1) ' 인수의 문제는 과 같은것을 입니다. 1) '변동 디폴트 인수'는 다음과 같습니다.
"이 문제가 있는 모든 기능은 실제 파라미터에서도 유사한 부작용 문제를 겪습니다."
이는 기능 프로그래밍의 규칙에 위배되는 것으로, 일반적으로 바람직하지 않으므로 양쪽을 함께 수정해야 합니다.
예:
def foo(a=[]): # the same problematic function
a.append(5)
return a
>>> somevar = [1, 2] # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5] # usually expected [1, 2]
솔루션: 복사
가장 안전한 해결책은 또는 먼저 입력 객체를 선택한 다음 복사본을 사용하여 모든 작업을 수행하는 것입니다.
def foo(a=[]):
a = a[:] # a copy
a.append(5)
return a # or everything safe by one line: "return a + [5]"
은 카피 있습니다.some_dict.copy()
★★★★★★★★★★★★★★★★★」some_set.copy()
할 도 있어요.somelist[:]
★★★★★★★★★★★★★★★★★」list(some_list)
할 도 있습니다.copy.copy(any_object)
보다 copy.deepcopy()
(후자는 가변 객체가 가변 객체로 구성되어 있는 경우에 편리합니다).일부 개체는 기본적으로 "파일" 개체와 같은 부작용을 기반으로 하므로 복사를 통해 의미 있게 복제할 수 없습니다.카피
class Test(object): # the original problematic class
def __init__(self, var1=[]):
self._var1 = var1
somevar = [1, 2] # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar # [1, 2, [1]] but usually expected [1, 2]
print t2._var1 # [1, 2, [1]] but usually expected [1, 2]
이 함수에 의해 반환되는 인스턴스의 퍼블릭 속성에도 저장해서는 안 됩니다.(인스턴스의 개인 속성을 이 클래스 또는 하위 클래스 외부에서 규칙에 따라 수정하지 않는 것으로 가정합니다._var1
어트리뷰트입니다).
★★★★
입력 매개 변수 개체는 올바른 위치에서 수정(변환)하거나 함수에 의해 반환되는 개체로 바인딩되지 않아야 합니다.(부작용이 없는 프로그래밍을 선호할 경우 강력히 권장됩니다.「부작용」에 대해서는, Wiki 를 참조해 주세요(처음 두 단락은 이 문맥에서 참조).
2)
실제 매개변수에 대한 부작용이 필요하지만 기본 매개변수에 대해 원하지 않는 경우에만 유용한 해결책은 다음과 같습니다.def ...(var1=None):
if var1 is None:
var1 = []
기타...
3) 경우에 따라서는 디폴트 파라미터의 가변동작이 도움이 됩니다.
당신이 묻고 싶은 것은 왜 다음과 같은 질문입니다.
def func(a=[], b = 2):
pass
내부적으로는 이와 동등하지 않습니다.
def func(a=None, b = None):
a_default = lambda: []
b_default = lambda: 2
def actual_func(a=None, b=None):
if a is None: a = a_default()
if b is None: b = b_default()
return actual_func
func = func()
명시적으로 func(없음, 없음)를 호출하는 경우를 제외하고 무시합니다.
즉, 기본 모수를 평가하는 대신 각 모수를 저장하고 함수를 호출할 때 모수를 평가하면 어떨까요?
한 가지 답은 아마도 바로 여기에 있을 것입니다. 그러면 기본 매개변수가 있는 모든 함수가 사실상 닫힘으로 바뀝니다.완전히 닫히지 않고 통역기에 숨겨져 있더라도 데이터는 어딘가에 저장되어야 합니다.더 느리고 메모리도 더 많이 쓸 거예요.
이것은 디폴트값과는 전혀 관계가 없습니다.단, 디폴트값이 변경 가능한 함수를 쓸 때 예기치 않은 동작으로 표시되는 경우가 많습니다.
>>> def foo(a):
a.append(5)
print a
>>> a = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]
이 코드에는 디폴트값이 표시되지 않지만 동일한 문제가 발생합니다.
문제는 말이다foo
발신자가 예상하지 못한 경우,는 발신자로부터 전달된 가변 변수를 변경합니다.이러한 코드는 함수가 다음과 같이 호출되면 됩니다.append_5
; 발신자는 전달된 값을 변경하기 위해 함수를 호출하고 동작을 예상합니다.그러나 이러한 함수는 기본 인수를 사용할 가능성이 매우 낮으며, (발신자가 이미 해당 목록에 대한 참조를 가지고 있기 때문에) 목록을 반환하지 않을 수 있습니다.
오리지널foo
디폴트 인수가 있는 경우,는 변경할 수 없습니다.a
명시적으로 전달되었는지, 디폴트값을 취득했는지의 여부를 나타냅니다.문맥/이름/문서에서 인수가 수정되어야 한다는 것이 명확하지 않은 한 코드는 변경 가능한 인수를 그대로 두어야 합니다.Python에 있든 없든 기본 인수가 있든 없든 인수로 전달되는 가변 값을 로컬 임시로 사용하는 것은 매우 좋지 않습니다.
무언가를 계산하는 과정에서 로컬 임시를 파괴적으로 조작해야 하고 인수 값에서 조작을 시작해야 하는 경우 복사본을 만들어야 합니다.
Python: 변경 가능한 기본 인수
기본 인수는 함수가 함수 객체로 컴파일될 때 평가됩니다.함수에 의해 사용되는 경우, 해당 함수에 의해 여러 번 동일한 객체가 유지되고 있습니다.
변환 가능한 경우(예를 들어 요소를 추가하는 등) 변환된 경우 연속된 콜에서 변환된 상태로 유지됩니다.
그들은 매번 같은 물체이기 때문에 변이된 상태를 유지한다.
동등한 코드:
함수 오브젝트가 컴파일 및 인스턴스화되면 목록이 함수에 바인딩되므로 다음과 같습니다.
def foo(mutable_default_argument=[]): # make a list the default argument
"""function that uses a list"""
는 다음과 거의 동일합니다.
_a_list = [] # create a list in the globals
def foo(mutable_default_argument=_a_list): # make it the default argument
"""function that uses a list"""
del _a_list # remove globals name binding
데모
다음은 데모를 보여드리겠습니다.이러한 오브젝트가 참조될 때마다 동일한 오브젝트임을 확인할 수 있습니다.
- 함수가 함수 객체에 컴파일을 완료하기 전에 목록이 작성되는 것을 확인합니다.
- 목록이 참조될 때마다 ID가 동일한지 관찰합니다.
- 목록을 사용하는 함수가 두 번째로 호출될 때 목록이 변경된 상태로 유지되는 것을 관찰합니다.
- 출력의 출력 순서를 관찰합니다(편리하게 번호를 붙였습니다).
example.py
print('1. Global scope being evaluated')
def create_list():
'''noisily create a list for usage as a kwarg'''
l = []
print('3. list being created and returned, id: ' + str(id(l)))
return l
print('2. example_function about to be compiled to an object')
def example_function(default_kwarg1=create_list()):
print('appending "a" in default default_kwarg1')
default_kwarg1.append("a")
print('list with id: ' + str(id(default_kwarg1)) +
' - is now: ' + repr(default_kwarg1))
print('4. example_function compiled: ' + repr(example_function))
if __name__ == '__main__':
print('5. calling example_function twice!:')
example_function()
example_function()
를 사용하여 실행 중python example.py
:
1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']
이것은 "최소한의 놀라움"의 원칙에 위배되는가?
이 실행 순서는 Python의 신규 사용자에게 자주 혼란을 줍니다.Python 실행 모델을 이해하면 충분히 예상할 수 있습니다.
새로운 Python 사용자를 위한 일반적인 명령:
단, 새로운 사용자에 대한 일반적인 지시는 다음과 같은 기본 인수를 작성하는 것입니다.
def example_function_2(default_kwarg=None):
if default_kwarg is None:
default_kwarg = []
그러면 None singleton이 Sentinel 객체로 사용되며 기본 이외의 인수가 있는지 함수에 알립니다.만약 우리가 논쟁을 하지 않는다면, 우리는 실제로 새로운 빈 목록을 사용하길 원합니다.[]
(디폴트로) 입니다.
제어 흐름에 대한 튜토리얼 섹션은 다음과 같습니다.
디폴트를 후속 콜간에 공유하지 않는 경우는, 대신에 다음과 같이 함수를 기술할 수 있습니다.
def f(a, L=None): if L is None: L = [] L.append(a) return L
가장 짧은 답변은 아마도 "정의는 실행"이 될 것입니다. 따라서 전체 주장은 엄밀한 의미가 없습니다.보다 구체적인 예로서 다음과 같은 것을 들 수 있습니다.
def a(): return []
def b(x=a()):
print x
디폴트 인수식을 실행 시 실행하지 않는 것으로 충분합니다.def
진술이 쉽지 않거나 말이 되지 않거나 둘 다입니다.
하지만 기본 컨스트럭터를 사용하려고 하면 감쪽같이 느껴집니다.
이미 바쁜 토픽이지만, 여기서 읽은 내용을 보면, 다음과 같이 사내에서 동작하고 있는 것을 알 수 있었습니다.
def bar(a=[]):
print id(a)
a = a + [1]
print id(a)
return a
>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object
[1]
>>> id(bar.func_defaults[0])
4484370232
퍼포먼스 최적화입니다.이 기능을 통해 다음 두 가지 기능 호출 중 어느 것이 더 빠를 것으로 생각하십니까?
def print_tuple(some_tuple=(1,2,3)):
print some_tuple
print_tuple() #1
print_tuple((1,2,3)) #2
힌트를 드릴게요.분해 예를 다음에 나타냅니다(http://docs.python.org/library/dis.html) 참조).
#
1
0 LOAD_GLOBAL 0 (print_tuple)
3 CALL_FUNCTION 0
6 POP_TOP
7 LOAD_CONST 0 (None)
10 RETURN_VALUE
#
2
0 LOAD_GLOBAL 0 (print_tuple)
3 LOAD_CONST 4 ((1, 2, 3))
6 CALL_FUNCTION 1
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
경험된 동작이 실용적일지는 의문입니다(버그를 키우지 않고 C에서 정적 변수를 실제로 사용한 사람은 누구입니까?).
보시는 바와 같이 불변의 디폴트 인수를 사용하면 퍼포먼스상의 이점이 있습니다.이것은 자주 호출되는 함수이거나 기본 인수를 구축하는 데 시간이 오래 걸리는 경우 차이를 만들 수 있습니다.또한 Python은 C가 아님을 명심하십시오.C에는 거의 자유로운 상수가 있습니다.Python에서는 이러한 이점이 없습니다.
다음 사항을 고려한다면 이 동작은 놀라운 것이 아닙니다.
- 할당 시행 시 읽기 전용 클래스 속성의 동작.
- 함수는 객체입니다(승인된 답변에 잘 설명되어 있습니다).
(2)의 역할은 이 분야에서 광범위하게 다루어지고 있습니다. (1) 다른 언어에서 온 경우 이러한 행동은 "직관적"이 아니기 때문에 놀라운 원인이 될 수 있습니다.
(1)은 Python 클래스 튜토리얼에 설명되어 있습니다.읽기 전용 클래스 속성에 값을 할당하려는 경우:
...가장 안쪽 스코프 외부에 있는 모든 변수는 읽기 전용입니다(이러한 변수에 쓰기를 시도하면 가장 안쪽 스코프에 새 로컬 변수가 생성되어 동일한 이름의 외부 변수는 변경되지 않습니다).
원래의 예를 참조해, 상기의 점에 주의해 주세요.
def foo(a=[]):
a.append(5)
return a
여기서foo
오브젝트 및a
의 속성입니다.foo
(다음에서 입수 가능)foo.func_defs[0]
)이후a
리스트입니다.a
가변성이 있기 때문에, 읽기/쓰기 어트리뷰트입니다.foo
. 함수 인스턴스화 시 시그니처에 지정된 빈 목록으로 초기화되며 함수 객체가 존재하는 한 읽고 쓸 수 있습니다.
부르기foo
기본값을 재정의하지 않고 기본값이 사용됩니다.foo.func_defs
이 경우,foo.func_defs[0]
에 사용됩니다.a
함수 객체의 코드 범위 내에 있습니다.변경 사항a
바꾸다foo.func_defs[0]
의 일부입니다.foo
오브젝트 및 코드 실행 사이에 지속됩니다.foo
.
다음으로 함수를 실행할 때마다 함수 시그니처의 디폴트가 사용되도록 다른 언어의 디폴트인수 동작을 에뮬레이트하는 문서의 예와 비교합니다.
def foo(a, L=None):
if L is None:
L = []
L.append(a)
return L
(1)과 (2)를 고려하면 이것이 원하는 동작을 하는 이유를 알 수 있습니다.
- 언제?
foo
함수 오브젝트가 인스턴스화 됩니다.foo.func_defs[0]
로 설정되어 있다.None
, 불변의 객체. - 기능이 기본값(지정된 파라미터 없음)으로 실행되는 경우
L
함수의 호출)을 실행합니다.foo.func_defs[0]
(None
)는 로컬 스코프에서 다음과 같이 사용할 수 있습니다.L
. - 에
L = []
, 의 할당은 성공할 수 없습니다.foo.func_defs[0]
이 Atribut은 읽기 전용이기 때문입니다. - (1)에 따라 로컬스코프에도 이름이 붙은 새로운 로컬 변수가 생성되어 함수 호출의 나머지 부분에서 사용됩니다.
foo.func_defs[0]
따라서 장래의 호출에 대해서는 변하지 않는다.foo
.
None을 사용한 간단한 회피책
>>> def bar(b, data=None):
... data = data or []
... data.append(b)
... return data
...
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
다음과 같은 경우가 있습니다.
- 누군가가 모든 언어/라이브러리 기능을 사용하고 있습니다.
- 여기서 행동을 바꾸는 건 현명하지 못한 행동이지만
위의 두 가지 특징을 모두 유지하면서 다른 점을 지적하는 것이 완전히 일치합니다.
- 이것은 혼란스러운 기능이고 Python에서는 유감입니다.
다른 답변 또는 적어도 일부는 3이 아닌 포인트 1과 2를 만들거나 포인트 3과 다운플레이 포인트 1과 2를 만듭니다.하지만 이 세 가지는 모두 사실이다.
여기서 중간에 말을 바꾸면 상당한 파손이 요구되고 Stefano의 오프닝 부분을 직관적으로 처리하도록 Python을 변경함으로써 더 많은 문제가 발생할 수 있습니다.그리고 Python의 내부를 잘 아는 사람이라면 결과의 지뢰밭을 설명할 수 있을 것이다.근데...
기존의 행동은 피토닉이 아니며 파이톤이 성공한 이유는 피토닉에 대한 아주 적은 부분이 이 언어에서 가장 덜 놀랍다는 원칙을 위반하기 때문입니다.그것을 뿌리뽑는 것이 현명한지 아닌지는 정말 문제다.설계상의 결함입니다.동작을 추적함으로써 언어를 더 잘 이해할 수 있다면, C++가 이 모든 것을 수행한다고 말할 수 있습니다. 예를 들어, 미묘한 포인터 오류를 탐색함으로써 많은 것을 배울 수 있습니다.하지만 이것은 피토닉이 아닙니다. Python에 대한 관심이 많은 사람들은 Python이 다른 언어보다 훨씬 덜 놀랍기 때문에 Python에 끌리는 사람들입니다.Dabblers와 호기심 많은 사람들은 Pythonista가 된다.Python에 끌리는 프로그래머의 직관에 어긋나는 디자인 때문에가 아니라 숨겨진 논리 퍼즐 때문에 어떤 일을 하는데 시간이 얼마나 적게 걸리는지에 놀라고 있다.
기본 목록 값을 함수에 전달하는 대체 구조를 시연합니다(사전과 동일하게 작동).
다른 사람들이 광범위하게 코멘트를 하고 있듯이 list 파라미터는 실행시기가 아닌 정의된 함수에 바인드됩니다.목록 및 딕셔너리는 변경 가능하므로 이 파라미터를 변경하면 이 함수에 대한 다른 호출에 영향을 줍니다.그 결과 함수에 대한 후속 호출은 함수에 대한 다른 호출에 의해 변경되었을 수 있는 이 공유 목록을 수신합니다.더 나쁜 것은 두 개의 파라미터가 동시에 다른 파라미터의 변경을 의식하지 않고 이 함수의 공유 파라미터를 사용하고 있다는 점입니다.
잘못된 방법(아마도...)):
def foo(list_arg=[5]):
return list_arg
a = foo()
a.append(6)
>>> a
[5, 6]
b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]
# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()
7
를 사용하면, 1개의 오브젝트로 같은 오브젝트인 것을 확인할 수 있습니다.id
:
>>> id(a)
5347866528
>>> id(b)
5347866528
Brett Slatkin의 "Effective Python: 59 Specific Ways to Writing Better Python", 항목 20: 동적 기본 인수를 지정하기 위한 문서 문자열 사용(48페이지)
Python에서 원하는 결과를 얻기 위한 규약은 다음과 같은 기본값을 제공하는 것입니다.
None
문서 문자열에 실제 동작을 기록합니다.
이 실장에서는 함수에 대한 각 콜이 기본목록을 수신하거나 함수에 전달된 목록을 수신할 수 있습니다.
권장 방법:
def foo(list_arg=None):
"""
:param list_arg: A list of input values.
If none provided, used a list with a default value of 5.
"""
if not list_arg:
list_arg = [5]
return list_arg
a = foo()
a.append(6)
>>> a
[5, 6]
b = foo()
b.append(7)
>>> b
[5, 7]
c = foo([10])
c.append(11)
>>> c
[10, 11]
프로그래머가 기본 목록 매개 변수를 공유하도록 의도한 '잘못된 방법'에 대한 정당한 사용 사례가 있을 수 있지만, 이는 규칙보다 예외일 가능성이 높습니다.
솔루션은 다음과 같습니다.
- 사용하다
None
디폴트값(또는 난스값)으로서object
실행 시 값을 작성하기 위해 이 기능을 켜거나 - 를 사용하다
lambda
기본 매개 변수로 사용하고 try block 내에서 호출하여 기본값을 가져옵니다(이것은 람다 추상화의 목적입니다.
두 번째 옵션은 이 함수의 사용자가 이미 존재하는 콜 가능(예:type
)
오브젝트(따라서 스코프가 있는 넥타이)를 치환하면 이 문제를 해결할 수 있습니다.
def foo(a=[]):
a = list(a)
a.append(5)
return a
못생겼지만, 효과가 있어요.
이 경우:
def foo(a=[]):
...
...인수를 할당합니다.a
발신자가 a 값을 전달하지 않은 경우 이름 없는 목록으로 이동합니다.
이 논의를 쉽게 하기 위해 일시적으로 이름 없는 목록에 이름을 붙입니다.어때.pavlo
?
def foo(a=pavlo):
...
언제든지, 발신자가 우리에게 무엇을 말해주지 않을 경우a
즉, 재사용할 수 있습니다.pavlo
.
한다면pavlo
변경 가능(변경 가능)foo
다음에 우리가 알아차릴 수 있는 효과인, 결국 그것을 수정하게 된다.foo
를 지정하지 않고 호출됩니다.a
.
이것이 여러분이 보는 것입니다(기억하십시오).pavlo
[ ] 로 초기화됩니다.
>>> foo()
[5]
지금이다,pavlo
[5] 입니다.
부르기foo()
다시 한 번 변경하다pavlo
다시:
>>> foo()
[5, 5]
지정a
전화할 때foo()
보증하다pavlo
만지지도 않았어요.
>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
그렇게,pavlo
정지해 있다[5, 5]
.
>>> foo()
[5, 5, 5]
다음 패턴의 대체 수단으로 이 동작을 이용할 수 있습니다.
singleton = None
def use_singleton():
global singleton
if singleton is None:
singleton = _make_singleton()
return singleton.use_me()
한다면singleton
에 의해서만 사용됩니다.use_singleton
대체품으로 다음 패턴을 원합니다.
# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
return singleton.use_me()
외부 리소스에 액세스하는 클라이언트 클래스를 인스턴스화하고 메모화를 위한 딕트 또는 목록을 만드는 데 사용되었습니다.
이 패턴은 잘 알려져 있지 않다고 생각하기 때문에, 향후의 오해를 피하기 위해서 짧은 코멘트를 넣습니다.
다른 모든 답변은 왜 이것이 실제로 좋은 행동이고 바람직한 행동인지, 또는 어쨌든 이것이 필요하지 않은 이유를 설명해 줍니다.내 것은 언어를 자신의 의지에 따르게 할 권리를 행사하려는 고집 센 사람들을 위한 것이지, 그 반대는 아니다.
디폴트값으로 남겨진 각 위치 인수에 대해 동일한 인스턴스를 재사용하는 대신 기본값을 복사하는 데코레이터를 사용하여 이 동작을 "수정"합니다.
import inspect
from copy import deepcopy # copy would fail on deep arguments like nested dicts
def sanify(function):
def wrapper(*a, **kw):
# store the default values
defaults = inspect.getargspec(function).defaults # for python2
# construct a new argument list
new_args = []
for i, arg in enumerate(defaults):
# allow passing positional arguments
if i in range(len(a)):
new_args.append(a[i])
else:
# copy the value
new_args.append(deepcopy(arg))
return function(*new_args, **kw)
return wrapper
이제 이 데코레이터를 사용하여 기능을 재정의해 보겠습니다.
@sanify
def foo(a=[]):
a.append(5)
return a
foo() # '[5]'
foo() # '[5]' -- as desired
이는 여러 인수를 사용하는 함수에 특히 적합합니다.비교:
# the 'correct' approach
def bar(a=None, b=None, c=None):
if a is None:
a = []
if b is None:
b = []
if c is None:
c = []
# finally do the actual work
와 함께
# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
# wow, works right out of the box!
다음과 같이 키워드 arg를 사용하려고 하면 위의 솔루션이 중단된다는 점에 유의하십시오.
foo(a=[4])
이를 위해 데코레이터를 조정할 수 있지만 독자를 위한 연습으로 남겨두고 있습니다.
이 "벌레" 때문에 야근 시간이 많아졌어요!다만, 사용의 가능성이 보이기 시작하고 있습니다(다만, 아직 실행시에 있으면 좋겠다고 생각하고 있습니다).
유용한 예를 하나 들겠습니다.
def example(errors=[]):
# statements
# Something went wrong
mistake = True
if mistake:
tryToFixIt(errors)
# Didn't work.. let's try again
tryToFixItAnotherway(errors)
# This time it worked
return errors
def tryToFixIt(err):
err.append('Attempt to fix it')
def tryToFixItAnotherway(err):
err.append('Attempt to fix it by another way')
def main():
for item in range(2):
errors = example()
print '\n'.join(errors)
main()
다음을 인쇄합니다.
Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
이것은 설계상의 결함이 아닙니다.이 일로 넘어지는 사람은 잘못된 일을 하는 거야.
이 문제에 직면할 가능성이 있는 경우는 3가지입니다.
- 함수의 부작용으로 인수를 수정하려고 합니다.이 경우 기본 인수를 갖는 것은 의미가 없습니다.유일한 예외는 인수 목록을 남용하여 함수 속성을 갖는 경우입니다.
cache={}
실제 인수를 사용하여 함수를 호출할 필요가 전혀 없습니다. - 인수를 변경하지 않고 그대로 두려고 하는데 실수로 수정한 것입니다.그건 버그야, 고쳐봐.
- 함수 내부에서 사용하기 위해 인수를 수정하려고 하지만 함수 외부에서 수정 내용을 볼 수 있을 것으로 예상하지 않았습니다.이 경우 기본값 여부에 관계없이 인수를 복사해야 합니다.Python은 Call-by-value 언어가 아니기 때문에 복사를 할 수 없습니다.명시해야 합니다.
질문의 예는 카테고리 1 또는 카테고리 3에 속할 수 있습니다.통과된 목록을 수정하고 반환하는 것은 이상합니다. 둘 중 하나를 선택해야 합니다.
기능을 다음과 같이 변경합니다.
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
TLDR: define-time defaults는 일관성이 있고 표현력이 엄격합니다.
함수를 정의하면 함수를 포함하는 정의 범위와 함수에 포함된 실행 범위라는 두 가지 범위에 영향을 미칩니다.블록이 스코프에 어떻게 매핑되는지는 분명하지만 문제는 어디에 있는가 하는 것입니다.def <name>(<args=defaults>):
대상:
... # defining scope
def name(parameter=default): # ???
... # execution scope
그def name
부품은 정의 범위 내에서 평가해야 합니다.name
결국 거기서 이용할 수 있게 된 거죠.내부에서만 함수를 평가하면 액세스할 수 없게 됩니다.
부터parameter
항상 사용하는 이름이기 때문에, 동시에 「확장」할 수 있습니다.def name
또한 이 기능은 다음과 같은 시그니처를 가진 함수를 생성할 수 있습니다.name(parameter=...):
베어 대신name(...):
.
자, 그럼 언제 평가해야 할까요?default
?
일관성은 이미 "정의 중"이라고 말하고 있습니다.그 외의 모든 것은def <name>(<args=defaults>):
정의에서도 가장 잘 평가됩니다.그것의 일부를 연기하는 것은 놀라운 선택이 될 것이다.
이 두 가지 선택지도 동일하지 않습니다.default
정의 시 평가되지만 실행 시간에 영향을 줄 수 있습니다.한다면default
실행 시 평가되므로 정의 시간에 영향을 줄 수 없습니다."at definition"을 선택하면 두 가지 경우를 모두 표현할 수 있지만 "at execution"을 선택하면 다음 중 하나만 표현할 수 있습니다.
def name(parameter=defined): # set default at definition time
...
def name(parameter=default): # delay default until execution time
parameter = default if parameter is None else parameter
...
이 질문에 대한 답은 python이 어떻게 데이터를 파라미터(값 또는 참조에 따라 전달)에 전달하는지, 또는 python이 "def" 스테이트먼트를 어떻게 처리하는지 등에 있다고 생각합니다.
간단한 소개.첫째, python에는 숫자와 같은 단순한 기본 데이터 유형과 개체 데이터 유형이 있습니다.둘째, python은 파라미터에 데이터를 전달할 때 기본 데이터 유형을 값으로 전달합니다. 즉, 값의 로컬 복사본을 로컬 변수로 만들지만 참조(즉, 포인터)를 통해 객체를 전달합니다.
위의 두 가지 점을 인정하면서 python code에 무슨 일이 일어났는지 설명하겠습니다.이는 객체에 대한 참조가 전달되기 때문이지 가변/불변성 또는 "def" 문장이 정의될 때 한 번만 실행된다는 사실과는 무관합니다.
[]는 객체이므로 python은 []의 참조를 에 전달합니다.a
,예.,a
는 오브젝트로서 메모리에 있는[ ]에 대한 포인터일 뿐입니다.그러나 []에는 참조가 많은 사본이 하나밖에 없습니다.첫 번째 foo()의 경우 리스트 []는 append 메서드에 의해 1로 변경됩니다.단, 목록 객체의 복사본은 1개뿐이며 이 객체는 1이 됩니다.두 번째 foo()를 실행할 때 effbot 웹 페이지에 표시되는 내용(항목은 더 이상 평가되지 않음)이 잘못되었습니다. a
는 목록 객체로 평가되지만, 현재 객체의 내용은 1입니다.이게 바로 기준 통과의 효과입니다.foo(3)의 결과는 같은 방법으로 쉽게 도출할 수 있다.
제 답변을 더 검증하기 위해 두 가지 추가 코드를 살펴보겠습니다.
====== 2번 ========
def foo(x, items=None):
if items is None:
items = []
items.append(x)
return items
foo(1) #return [1]
foo(2) #return [2]
foo(3) #return [3]
[]
오브젝트입니다.None
(전자는 불변이고 후자는 불변이다.그러나 불변성은 질문과는 무관하다).공간 어디에도 없습니다만, 거기에 있는 것은 알고 있습니다.또, 「없음」의 카피는 1개뿐입니다.따라서 foo가 호출될 때마다 항목은 None(없음)으로 평가됩니다(단 한 번만 평가된다는 일부 답변과는 달리). 명확하게는 None의 참조(또는 주소)입니다.그런 다음 foo에서 항목은 []로 변경됩니다. 즉, 주소가 다른 다른 개체를 가리킵니다.
====== 3번 =======
def foo(x, items=[]):
items.append(x)
return items
foo(1) # returns [1]
foo(2,[]) # returns [2]
foo(3) # returns [1,3]
foo(1)의 호출에 의해 아이템은 주소(예를 들어 111111)를 가진 리스트 오브젝트[]를 가리키게 됩니다.후속편 foo 함수에서는 리스트의 내용이 1로 변경되지만 주소는 변경되지 않고 여전히 1111111입니다.그럼 foo(2,[])가 오네요.foo(1)를 호출할 때 foo(2, [])의 []의 내용은 기본 파라미터 []와 동일하지만 주소가 다릅니다.파라미터를 명시적으로 제공하므로items
이 새로운 주소의[]
2222222 라고 말하고 변경 후 반송해 주세요.이것으로 foo(3)가 실행됩니다.그 이후부터x
제공된 경우 항목은 기본값을 다시 사용해야 합니다.기본값은 어떻게 됩니까?foo 함수: 111111에 있는 목록 개체를 정의할 때 설정됩니다.따라서 아이템은 요소 1을 가진 주소 111111로 평가된다.2222222에 있는 목록에도 요소 2가 하나 포함되어 있지만 더 이상 항목별로 표시되지 않습니다.따라서 3을 더하면items
[1,3].
상기 설명에서 본 질문에 대한 답변에서 추천된 effobot 웹페이지가 적절한 답변을 하지 못했음을 알 수 있습니다.게다가 effot 웹페이지에 있는 포인트는 잘못된 것 같습니다.UI에 관한 코드라고 생각합니다.버튼은 정확합니다.
for i in range(10):
def callback():
print "clicked button", i
UI.Button("button %s" % i, callback)
각 버튼은 다른 값을 표시하는 개별 콜백 함수를 유지할 수 있습니다.i
예를 들면 다음과 같습니다.
x=[]
for i in range(10):
def callback():
print(i)
x.append(callback)
실행했을 경우x[7]()
예상대로 7점 받고x[9]()
will은 9를 준다.i
.
언급URL : https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument
'itsource' 카테고리의 다른 글
Cake PHP - Mysql에서 Maria로 업그레이드DB (0) | 2022.09.16 |
---|---|
PHP에서 모바일 장치를 검색하는 가장 간단한 방법 (0) | 2022.09.16 |
PHP setlocale이 작동하지 않음 (0) | 2022.09.16 |
JS의 Chrome CPU 프로파일에서 'self'와 'total'의 차이 (0) | 2022.09.16 |
JavaScript에서 타임스탬프를 얻으려면 어떻게 해야 하나요? (0) | 2022.09.16 |