파이썬

[python] "최소한의 놀라움"과 변경 가능한 기본 인수

zooheon 2022. 8. 21. 23:09
반응형

Python을 오랫동안 만지작거리는 사람은 다음 문제에 물린(또는 조각난) 사람입니다.

def foo(a=[]):
    a.append(5)
    return a

Python 초보자는 매개변수 없이 호출된 이 함수가 항상 하나의 요소만 있는 목록을 반환할 것으로 예상합니다: [5]. 결과는 매우 다르며 매우 놀랍습니다(초보자의 경우).

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

내 매니저는 이 기능을 처음 접했고 그것을 언어의 "극적인 디자인 결함"이라고 불렀습니다. 나는 그 행동에 근본적인 설명이 있다고 대답했고, 내부를 이해하지 못하면 실제로 매우 당혹스럽고 예상치 못한 일입니다. 그러나 나는 다음과 같은 질문에 (자신에게) 대답할 수 없었습니다. 함수 실행이 아닌 함수 정의에서 기본 인수를 바인딩하는 이유는 무엇입니까? 경험이 풍부한 행동이 실제로 사용되는지 의심스럽습니다(누가 실제로 버그를 번식시키지 않고 C에서 정적 변수를 사용했습니까?)

편집 :

Baczek은 흥미로운 예를 들었습니다 . 귀하의 대부분의 의견과 특히 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나타낼 수 있습니다. 함수 개체)는 정의 시 발생하고 일부(기본 매개변수 할당)는 함수 호출 시 발생합니다.

실제 동작은 더 일관적입니다. 즉, 해당 행이 실행될 때 해당 행의 모든 ​​것이 평가됩니다. 즉, 함수 정의에서를 의미합니다.

 

사실 이것은 설계상의 하자도 아니고, 내부나 성능 때문도 아니다. 그것은 단순히 파이썬의 함수가 단지 코드 조각이 아니라 일급 객체라는 사실에서 비롯됩니다.

이런 식으로 생각하면 완전히 이해가 됩니다. 함수는 정의에 따라 평가되는 객체입니다. 기본 매개변수는 일종의 "멤버 데이터"이므로 해당 상태는 다른 개체에서와 마찬가지로 한 호출에서 다른 호출로 변경될 수 있습니다.

어쨌든 effbot(Fredrik Lundh)은 Python의 기본 매개변수 값에서 이 동작에 대한 이유를 아주 잘 설명했습니다 . 나는 그것이 매우 명확하다는 것을 알았고, 함수 객체가 어떻게 작동하는지에 대한 더 나은 지식을 위해 그것을 읽는 것이 좋습니다.

 

다음 코드가 있다고 가정합니다.

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

내가 eat 선언을 볼 때 가장 놀라운 것은 첫 번째 매개변수가 주어지지 않으면 튜플과 같을 것이라고 생각하는 것입니다.("apples", "bananas", "loganberries")

그러나 나중에 코드에서 다음과 같은 작업을 수행한다고 가정합니다.

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

그런 다음 기본 매개변수가 함수 선언이 아니라 함수 실행 시 바인딩된 경우 과일이 변경되었음을 발견하고 (매우 나쁜 방식으로) 놀랐을 것입니다. foo이것은 위의 함수가 목록을 변경하고 있다는 것을 발견하는 것보다 더 놀라운 IMO가 될 것입니다 .

진짜 문제는 가변 변수에 있으며 모든 언어가 어느 정도 이 문제를 가지고 있습니다. 다음은 질문입니다. 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의 현재 접근 방식을 좋아합니다. 기본 함수 인수는 함수가 정의될 ​​때 평가되고 해당 객체는 항상 기본값입니다. 나는 그들이 빈 목록을 사용하여 특수 케이스를 사용할 수 있다고 생각하지만, 그런 종류의 특수 케이스는 이전 버전과 호환되지 않는 것은 말할 것도 없고 훨씬 더 놀라운 일을 야기할 것입니다.

 

문서 의 관련 부분 :

기본 매개변수 값은 함수 정의가 실행될 때 왼쪽에서 오른쪽으로 평가됩니다. 이는 함수가 정의될 ​​때 표현식이 한 번 평가되고 각 호출에 대해 동일한 "미리 계산된" 값이 사용됨을 의미합니다. 이것은 기본 매개변수가 목록이나 사전과 같은 변경 가능한 개체인 경우를 이해하는 데 특히 중요합니다. 함수가 개체를 수정하면(예: 목록에 항목을 추가하여) 기본값이 사실상 수정됩니다. 이것은 일반적으로 의도한 것이 아닙니다. 이 문제를 해결하는 방법 None은 기본값으로 사용하고 함수 본문에서 명시적으로 테스트하는 것입니다. 예:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

 

반응형