[python] “최소한의 놀라움”과 변하기 쉬운 기본 주장

파이썬으로 오랫동안 땜질을하는 사람은 다음과 같은 문제로 물렸거나 조각났습니다.

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

파이썬 초보자는이 함수가 항상 하나의 요소로만 목록을 반환 할 것으로 기대합니다 [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은 Python의 기본 매개 변수 값 에서이 동작의 이유에 대해 아주 잘 설명합니다 .
나는 그것이 매우 분명하다는 것을 알았고 함수 객체가 어떻게 작동하는지 더 잘 알기 위해 그것을 읽는 것이 좋습니다.


답변

다음 코드가 있다고 가정하십시오.

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

def eat(food=fruits):
    ...

내가 먹는 선언을 볼 때 가장 놀라운 것은 첫 번째 매개 변수가 주어지지 않으면 튜플과 같다고 생각하는 것입니다. ("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의 현재 접근 방식을 좋아합니다. 기본 함수 인수는 함수가 정의되고 해당 객체가 항상 기본값 일 때 평가됩니다. 나는 그들이 빈 목록을 사용하여 특별한 경우를 할 수 있다고 생각하지만, 그런 종류의 특별한 케이싱은 더 이상 놀랍게 만들지 않을 것입니다.


답변

문서 의 관련 부분 :

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

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

답변

나는 파이썬 인터프리터 내부 작업에 대해 아무것도 알지 못하며 (컴파일러 및 인터프리터도 전문가가 아닙니다) 무의미하거나 불가능한 것을 제안하더라도 나를 비난하지 마십시오.

파이썬 객체 가 변경 가능하다면 기본 인수를 디자인 할 때 이것을 고려해야한다고 생각합니다. 목록을 인스턴스화 할 때 :

a = []

에서 참조 하는 목록 을 얻을 것으로 예상 됩니다 a.

왜해야 a=[]

def x(a=[]):

함수 정의와 호출이 아닌 새로운 목록을 인스턴스화합니까? “사용자가 인수를 제공하지 않으면 새 목록 을 인스턴스화 하여 호출자가 생성 한 것처럼 사용하십시오 “라고 묻는 것과 같습니다 . 나는 이것이 대신 모호하다고 생각합니다.

def x(a=datetime.datetime.now()):

사용자 a는 정의하거나 실행할 때의 날짜 시간을 기본값으로 설정 x하시겠습니까? 이 경우, 이전의 경우와 같이 기본 인수 “assignment”가 함수의 첫 번째 명령어 ( datetime.now()함수 호출시 호출) 인 것과 동일한 동작을 유지합니다 . 반면에 사용자가 정의 시간 매핑을 원하면 다음과 같이 작성할 수 있습니다.

b = datetime.datetime.now()
def x(a=b):

나는 알고있다 : 그것은 폐쇄이다. 또는 파이썬은 정의 시간 바인딩을 강제하는 키워드를 제공 할 수 있습니다.

def x(static a=b):


답변

글쎄, 그 이유는 코드가 실행될 때 바인딩이 수행되고 함수가 정의 될 때 함수 정의가 실행되기 때문입니다.

이것을 비교하십시오 :

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

이 코드는 정확히 예상치 못한 상황이 발생합니다. bananas는 클래스 속성이므로 항목을 추가하면 해당 클래스의 모든 인스턴스에 추가됩니다. 그 이유는 정확히 같습니다.

그것은 단지 “어떻게 작동 하는가”일 뿐이며, 함수 케이스에서 다르게 작동하게 만드는 것은 아마도 복잡 할 것이고 클래스 케이스에서는 불가능할 것입니다. 객체가 생성되면 실행합니다.

그렇습니다. 그러나 페니가 떨어지면 파이썬의 일반적인 작동 방식과 완벽하게 맞습니다. 사실, 그것은 훌륭한 교육 보조 자료이며, 왜 이런 일이 발생했는지 이해하면 파이썬을 훨씬 더 잘 이해할 것입니다.

그것은 훌륭한 파이썬 튜토리얼에서 두드러지게 등장해야한다고 말했습니다. 언급했듯이 모든 사람이 조만간이 문제에 부딪칩니다.


답변

왜 당신은 내성하지?

정말 파이썬에서 제공하는 통찰력 성찰을 수행 (한 사람을 놀라게하지 23callables에 적용을).

func다음과 같이 정의 된 간단한 작은 함수가 있습니다.

>>> def func(a = []):
...    a.append(5)

파이썬이 그것을 만나면, 가장 먼저 할 일은 code이 함수를위한 객체 를 만들기 위해 그것을 컴파일하는 것입니다 . 이 컴파일 단계가 완료되는 동안, 파이썬은 평가 * 다음 저장 (빈리스트 기본 인수를 []함수 객체 자체에 여기) . 최고 답변이 언급했듯이 목록 a은 이제 함수 의 멤버 로 간주 될 수 있습니다 func.

이제 함수 객체 에서 목록이 어떻게 확장되는지 조사하기 전과 후에 약간의 내부 검사를 해 봅시다 . 나는 Python 3.x이것을 위해 파이썬 2를 사용하고 있습니다 (사용 __defaults__하거나 func_defaults파이썬 2에서; 그렇습니다, 같은 것에 대한 두 개의 이름).

실행 전 기능 :

>>> def func(a = []):
...     a.append(5)
...     

Python이이 정의를 실행 한 후에는 지정된 기본 매개 변수 ( a = []여기)를 가져 와서 함수 객체 __defaults__속성 (관련 섹션 : Callables)에 넣습니다 .

>>> func.__defaults__
([],)

좋아, __defaults__예상대로 의 단일 항목으로 빈 목록이 있습니다.

실행 후 기능 :

이제이 함수를 실행 해 봅시다 :

>>> func()

자, 그것들을 __defaults__다시 봅시다 :

>>> func.__defaults__
([5],)

놀랐습니까? 객체 내부의 값이 변경됩니다! 함수에 대한 연속적인 호출은 이제 해당 임베디드 list오브젝트에 추가됩니다 .

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

따라서이 ‘결함’이 발생 하는 이유 는 기본 인수가 함수 객체의 일부이기 때문입니다. 여기에는 이상한 일이 없습니다. 모두 조금 놀랍습니다.

이것을 방지하는 일반적인 해결책 None은 기본값 으로 사용 하고 함수 본문에서 초기화하는 것입니다.

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

함수 본문은 매번 새로 실행되므로에 인수가 전달되지 않으면 항상 새로운 빈 목록이 나타납니다 a.


리스트 __defaults__가 함수에서 사용 된 것과 동일한 지 확인하기 위해 함수 본문 내에서 사용 된리스트 func를 반환하도록 함수를 변경할 수 있습니다 . 그런 다음에있는 목록과 비교 (위치 에서 ) 이러한 실제로 동일한 목록 인스턴스로 다스 려하는 방법은 다음과 같이 표시됩니다ida__defaults__[0]__defaults__

>>> def func(a = []):
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

내성의 힘을 가진 모든 것!


* 함수 컴파일 중 파이썬이 기본 인수를 평가하는지 확인하려면 다음을 실행하십시오.

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받는 통화 시간 이 모두 반환 된 기능의 목록을 얻을 것이다, 그래서 난의 값 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모듈을 사용하여 인수와 기본값에 대한 정보를 얻을 수 있습니다.

>>> 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=[]

그러나 우리는 내성을 검사하고 기본 인수 무엇인지 확인하는 기능을 잃었습니다 . 객체가 생성되지 않았으므로 실제로 함수를 호출하지 않으면 객체를 잡을 수 없습니다. 우리가 할 수있는 최선의 방법은 소스 코드를 저장하고 문자열로 반환하는 것입니다.