لماذا يتم تقييم الوسائط الافتراضية في وقت التعريف في بايثون؟

StackOverflow https://stackoverflow.com/questions/1651154

  •  22-07-2019
  •  | 
  •  

سؤال

لقد واجهت صعوبة بالغة في فهم السبب الجذري للمشكلة في الخوارزمية.بعد ذلك، من خلال تبسيط الوظائف خطوة بخطوة، اكتشفت أن تقييم الوسائط الافتراضية في بايثون لا يتصرف كما توقعت.

رمز على النحو التالي:

class Node(object):
    def __init__(self, children = []):
        self.children = children

المشكلة هي أن كل مثيل لفئة Node يشترك في نفس الشيء children السمة، إذا لم يتم إعطاء السمة بشكل صريح، مثل:

>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

لا أفهم منطق قرار التصميم هذا؟لماذا قرر مصممو بايثون أن يتم تقييم الوسائط الافتراضية في وقت التعريف؟هذا يبدو غير بديهي للغاية بالنسبة لي.

هل كانت مفيدة؟

المحلول

سيكون البديل ثقيل الوزن للغاية - تخزين "قيم الوسيطة الافتراضية" في كائن الوظيفة على هيئة "مجموعات" من التعليمات البرمجية ليتم تنفيذها مرارًا وتكرارًا في كل مرة يتم فيها استدعاء الوظيفة بدون قيمة محددة لتلك الوسيطة - ومن شأنه أن يجعلها من الصعب جدًا الحصول على الربط المبكر (الربط في وقت محدد)، وهو ما تريده غالبًا.على سبيل المثال، في بايثون كما هو موجود:

def ack(m, n, _memo={}):
  key = m, n
  if key not in _memo:
    if m==0: v = n + 1
    elif n==0: v = ack(m-1, 1)
    else: v = ack(m-1, ack(m, n-1))
    _memo[key] = v
  return _memo[key]

...إن كتابة دالة محفوظة مثل ما سبق هي مهمة أولية تمامًا.بصورة مماثلة:

for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

...البسيط i=i, ، بالاعتماد على الربط المبكر (وقت التعريف) لقيم الوسيطة الافتراضية، يعد طريقة بسيطة للغاية للحصول على الربط المبكر.لذا، فإن القاعدة الحالية بسيطة ومباشرة، وتتيح لك القيام بكل ما تريد بطريقة يسهل شرحها وفهمها للغاية:إذا كنت تريد الربط المتأخر لقيمة التعبير، فقم بتقييم هذا التعبير في نص الدالة؛إذا كنت تريد الربط المبكر، فقم بتقييمه كقيمة افتراضية للوسيطة.

البديل، وهو فرض الربط المتأخر في كلتا الحالتين، لن يوفر هذه المرونة، وسيجبرك على المرور عبر الأطواق (مثل تغليف وظيفتك في مصنع إغلاق) في كل مرة تحتاج فيها إلى الربط المبكر، كما في الأمثلة المذكورة أعلاه - حتى الآن المزيد من القواعد النموذجية ذات الوزن الثقيل المفروضة على المبرمج من خلال قرار التصميم الافتراضي هذا (بعيدًا عن القرارات "غير المرئية" المتمثلة في إنشاء وتقييم متكرر للمجموعات الكبيرة في كل مكان).

وبعبارة أخرى، "يجب أن تكون هناك طريقة واحدة واضحة للقيام بذلك، ويفضل أن تكون واحدة فقط [1]":عندما تريد الربط المتأخر، هناك بالفعل طريقة واضحة تمامًا لتحقيق ذلك (نظرًا لأن جميع أكواد الوظيفة يتم تنفيذها فقط في وقت الاتصال، فمن الواضح أنه يتم تقييم كل شيء هناك متأخرا ملزمة)؛يمنحك إجراء تقييم الوسيطة الافتراضية ربطًا مبكرًا طريقة واضحة لتحقيق الربط المبكر أيضًا (علامة زائد!-) بدلاً من إعطاء طريقتين واضحتين للحصول على الربط المتأخر ولا توجد طريقة واضحة للحصول على الربط المبكر (ناقص!-).

[1]:"على الرغم من أن هذه الطريقة قد لا تكون واضحة في البداية إلا إذا كنت هولنديًا."

نصائح أخرى

القضية هي هذه.

يعد تقييم الوظيفة كمُهيئ أمرًا مكلفًا للغاية في كل مرة يتم استدعاء الوظيفة.

  • 0 هو حرفي بسيط.قم بتقييمه مرة واحدة، واستخدمه إلى الأبد.

  • int هي وظيفة (مثل القائمة) يجب تقييمها في كل مرة تكون مطلوبة كمُهيئ.

البناء [] هو الحرفي، مثل 0, ، وهذا يعني "هذا الكائن بالضبط".

المشكلة هي أن بعض الناس يأملون أن يكون ذلك يعني list كما في "قم بتقييم هذه الوظيفة لي، من فضلك، للحصول على الكائن الذي يمثل المُهيئ".

سيكون عبئًا ساحقًا إضافة ما هو ضروري if بيان للقيام بهذا التقييم في كل وقت.من الأفضل أن تأخذ جميع الوسائط كأحرف حرفية ولا تقوم بأي تقييم إضافي للوظيفة كجزء من محاولة إجراء تقييم الوظيفة.

والأهم من ذلك، أنه من الناحية الفنية مستحيل لتنفيذ افتراضيات الوسيطة كتقييمات للوظيفة.

تأمل للحظة في الرعب المتكرر لهذا النوع من الدائرية.لنفترض أنه بدلاً من أن تكون القيم الافتراضية قيمًا حرفية، فإننا نسمح لها بأن تكون وظائف يتم تقييمها في كل مرة تكون فيها القيم الافتراضية للمعلمة مطلوبة.

[وهذا من شأنه أن يوازي الطريق collections.defaultdict يعمل.]

def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

ما هي قيمة another_func()؟للحصول على الافتراضي ل b, ، يجب تقييمه aFunc, ، الأمر الذي يتطلب تقييم another_func.أُووبس.

وبالطبع في موقفك من الصعب أن نفهم. ولكن يجب أن نرى، أن تقييم وسائط الافتراضية في كل مرة أن تضع عبئا ثقيلا على وقت تشغيل النظام.

وأيضا يجب أن نعرف أنه في حالة أنواع الحاويات قد تحدث هذه المشكلة - ولكن هل يمكن الالتفاف عليه من خلال جعل شيء واضح:

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

والحل لهذه، مناقشتها هنا (وجدا الصلبة)، هو:

class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

وأما لماذا نبحث عن إجابة من فون Löwis، ولكن من المرجح لأن تعريف الدالة يجعل الكائن كود بسبب بنية بيثون، وربما لا يكون هناك تسهيلات للعمل مع أنواع مرجع مثل هذا في الوسائط الافتراضية.

وأعتقد أن هذا الحدس جدا، حتى تعلمت كيف تطبق بيثون الوسائط الافتراضية.

وثمة وظيفة لكائن. في وقت الحمل، بيثون بإنشاء الكائن وظيفة، ويقيم الإعدادات الافتراضية في بيان def، ويضع لهم في الصفوف (tuple)، ويضيف أن الصفوف (tuple) كسمة وظيفة اسمه func_defaults. ثم، عندما يتم استدعاء وظيفة، إذا لم دعوة توفير قيمة، بيثون الاستيلاء على القيمة الافتراضية من func_defaults.

وعلى سبيل المثال:

>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

وهكذا جميع المكالمات إلى f التي لا توفر حجة سوف تستخدم نفس مثيل C، لأن هذا هو القيمة الافتراضية.

وبقدر ما السبب بيثون يفعل ذلك بهذه الطريقة: حسنا، هذا الصفوف (tuple) <م> قد يحتوي على وظائف من شأنها أن الحصول على استدعاء كل مرة كانت هناك حاجة إلى قيمة الوسيطة الافتراضية. وبصرف النظر عن مشكلة واضحة فورا من الأداء، عليك أن تبدأ الدخول في الكون الحالات الخاصة، مثل تخزين قيم حرفية بدلا من وظائف لأنواع غير قابلة للتغيير لتجنب المكالمات وظيفة لا لزوم لها. وبالطبع هناك ضمانات الأداء وافر.

والسلوك الفعلي هو حقا بسيطة. وهناك مشكلة تافهة، في حالة ما إذا كنت <م> تريد قيمة افتراضية ليتم إنتاجها بواسطة استدعاء وظيفة في وقت التشغيل:

def f(x = None):
   if x == None:
      x = g()

يأتي هذا من تركيز بايثون على بساطة بناء الجملة والتنفيذ.تحدث عبارة def عند نقطة معينة أثناء التنفيذ.عندما يصل مترجم بايثون إلى تلك النقطة، فإنه يقوم بتقييم الكود الموجود في هذا السطر، ثم يقوم بإنشاء كائن كود من نص الدالة، والذي سيتم تشغيله لاحقًا، عند استدعاء الدالة.

إنه تقسيم بسيط بين إعلان الوظيفة ونص الوظيفة.يتم تنفيذ الإعلان عند الوصول إليه في الكود.يتم تنفيذ الجسم في وقت المكالمة.لاحظ أنه يتم تنفيذ الإعلان في كل مرة يتم الوصول إليه، لذا يمكنك إنشاء وظائف متعددة عن طريق التكرار.

funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print "1: ", func()
    print "2: ", func()

تم إنشاء خمس وظائف منفصلة، ​​مع إنشاء قائمة منفصلة في كل مرة يتم فيها تنفيذ إعلان الوظيفة.على كل حلقة من خلال funcs, ، يتم تنفيذ نفس الوظيفة مرتين في كل تمريرة، باستخدام نفس القائمة في كل مرة.هذا يعطي النتائج:

1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

لقد أعطاك الآخرون الحل البديل، وهو استخدام param=None، وتعيين قائمة في النص إذا كانت القيمة بلا، وهي لغة بايثون اصطلاحية بالكامل.إنه أمر قبيح بعض الشيء، لكن البساطة قوية، والحل البديل ليس مؤلمًا للغاية.

تم التعديل للإضافة:لمزيد من المناقشة حول هذا الأمر، راجع مقالة effbot هنا: http://effbot.org/zone/default-values.htm, ، ومرجع اللغة هنا: http://docs.python.org/reference/compound_stmts.html#function

والتعاريف وظيفة بيثون هي رمز فقط، مثل كافة التعليمات البرمجية الأخرى. انهم ليسوا "السحري" في الطريقة التي هي بعض اللغات. على سبيل المثال، في جاوة التي يمكن أن تشير "الآن" لشيء محددة "في وقت لاحق":

public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

ولكن في بيثون

def foo(): bar()
foo()   # boom! "bar" has no binding yet
def bar(): pass
foo()   # ok

وهكذا، يتم تقييم الوسيطة الافتراضية في لحظة أن هذا سطر من التعليمات البرمجية يتم تقييم!

ولأن إذا كان لديهم، ثم شخص ما من شأنه نشر سؤال يسأل لماذا لم يكن العكس :-P

لنفترض الآن أن لديهم. كيف يمكنك تنفيذ السلوك الحالي إذا لزم الأمر؟ فإنه من السهل لإنشاء كائنات جديدة داخل وظيفة، ولكن لا يمكنك "اهلك" هم (يمكنك حذفها، ولكنها ليست هي نفسها).

وسوف تقدم رأيا مخالفا، من خلال addessing الحجج الرئيسية في وظائف أخرى.

<اقتباس فقرة>   

وتقييم الوسائط الافتراضية عندما وظيفة يتم تنفيذه سيكون سيئا للأداء.

وأجد من الصعب تصديق ذلك. إذا مهام الوسيطة الافتراضية مثل foo='some_string' تضيف قيمة حقيقية مبلغ غير مقبول من النفقات العامة، وأنا متأكد من أنه سيكون من الممكن تحديد تخصيصات حرفية ثابتة وprecompute لهم.

<اقتباس فقرة>   

إذا كنت ترغب في التعيين الافتراضي مع كائن قابلة للتغيير مثل foo = []، ومجرد استخدام foo = None، تليها foo = foo or [] في الجسم وظيفة.

ورغم أن هذا قد يكون إشكالية في حالات فردية، ونمط تصميم انها ليست أنيقة جدا. وتضيف كود النمطي ويحجب القيم الوسيطة الافتراضية. أنماط مثل foo = foo or ... لا تعمل إذا foo يمكن أن يكون كائن مثل مجموعة نمباي مع القيمة الحقيقة غير محددة. وفي الحالات التي يكون فيها None هو قيمة وسيطة ذات المغزى التي قد تنتقل عن قصد، فإنه لا يمكن أن تستخدم الحارس وهذا الحل يصبح قبيح حقا.

<اقتباس فقرة>   

والسلوك الحالي هو مفيد للكائنات افتراضية قابلة للتغيير أن <م> يجب تقاسمها تزوجنا المكالمات وظيفة.

وسأكون سعيدا لرؤية دليل على عكس ذلك، ولكن في تجربتي هذه الحالة استخدام أقل شيوعا بكثير من الأشياء قابلة للتغيير التي يجب أن تنشأ من جديد في كل مرة يتم استدعاء الدالة. ويبدو لي أيضا مثل حالة استخدام أكثر تقدما، في حين أن المهام الافتراضية عرضي مع الحاويات الفارغة هي مسكتك المشترك للمبرمجين بيثون الجديد. ولذلك، فإن مبدأ أقل دهشة يوحي ينبغي تقييم القيم الوسيطة الافتراضية عندما يتم تنفيذ الدالة.

وبالإضافة إلى ذلك، يبدو لي أن هناك يعد حل سهل للكائنات قابلة للتغيير التي يجب أن تكون مشتركة عبر المكالمات الدالة: التهيئة لهم خارج الدالة

ولذا فإنني أزعم أن هذا كان قرارا تصميم سيئة. تخميني هو الذي تم اختياره لأن تنفيذها هو أبسط الواقع ولأنه يحتوي على صالح (وإن كانت محدودة) استخدام القضية. للأسف، وأنا لا أعتقد أن هذا سيغير من أي وقت مضى، لأن المطورين بيثون الأساسية يريدون تجنب تكرار كمية من عدم التوافق الوراء أن بيثون 3 قدم.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top