İsteğe bağlı işlevselliği bir işlevin ana amacından ayırmanın pythonic bir yolu var mı?


11

bağlam

Aşağıdaki Python kodum olduğunu varsayalım:

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        for _ in range(n_iters):
            number = halve(number)
        sum_all += number
    return sum_all


ns = [1, 3, 12]
print(example_function(ns, 3))

example_functionburada nslistedeki öğelerin her birini gözden geçirip, sonuçları biriktirirken 3 kez yarıya indiriyoruz. Bu komut dosyasını çalıştırmanın çıktısı basitçe:

2.0

1 / (2 ^ 3) * (1 + 3 + 12) = 2 olduğundan.

Şimdi, diyelim ki (herhangi bir nedenle, belki hata ayıklama veya günlük kaydı), attığı ara adımlar hakkında bir tür bilgi görüntülemek istiyorum example_function. Belki daha sonra bu işlevi böyle bir şeye yeniden yazarım:

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            print(number)
        sum_all += number
        print('sum_all:', sum_all)
    return sum_all

şimdi, daha önce olduğu gibi aynı argümanlarla çağrıldığında, aşağıdakileri çıkarır:

Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0

Bu tam olarak istediğim şeyi başarıyor. Bununla birlikte, bu, bir işlevin sadece bir şey yapması gerektiği ilkesine aykırıdır ve şimdi kodu biraz example_functiondaha uzun ve daha karmaşıktır. Böyle basit bir işlev için bu bir sorun değildir, ancak benim bağlamımda birbirimizi çağıran oldukça karmaşık işlevlerim var ve yazdırma ifadeleri genellikle burada gösterilenden daha karmaşık adımlar içeriyor ve bu da kodumun karmaşıklığında önemli bir artışa neden oluyor. fonksiyonlarımın gerçek amacı ile ilgili satırlardan daha fazla günlük kaydı ile ilgili kod satırları vardı!).

Ayrıca, daha sonra artık işlevimde herhangi bir yazdırma ifadesi istemediğime karar verirsem, example_functiontüm bu printifadelerle ilgili tüm değişkenler ile birlikte tüm ifadeleri manuel olarak gözden geçirip silmem gerekir, hem sıkıcı hem de hata olan bir işlem -yatkın.

İşlev yürütme sırasında her zaman yazdırma veya yazdırmama olasılığım olursa durum daha da kötüleşir, bu da beni ya çok benzer iki işlevi (biri printifadeleri olan, biri olmadan) bildirmeye yönlendirir , gibi bir şey tanımlamak için:

def example_function(numbers, n_iters, debug_mode=False):
    sum_all = 0
    for number in numbers:
        if debug_mode:
            print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            if debug_mode:
                print(number)
        sum_all += number
        if debug_mode:
            print('sum_all:', sum_all)
    return sum_all

basit bir durumda bile şişkin ve (umarım) gereksiz yere karmaşık bir fonksiyonla sonuçlanır example_function.


Soru

Baskı işlevselliğini orijinal işlevinden "ayırmak" için bir pythonic yolu var mı example_function?

Daha genel olarak, isteğe bağlı işlevselliği bir işlevin ana amacından ayırmanın pythonic bir yolu var mı?


Şimdiye kadar denedim:

Şu anda bulduğum çözüm, ayırma için geri aramalar kullanıyor. Örneğin, şu şekilde yeniden yazılabilir example_function:

def example_function(numbers, n_iters, callback=None):
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            number = number/2

            if callback is not None:
                callback(locals())
        sum_all += number
    return sum_all

ve sonra istediğim yazdırma işlevini gerçekleştiren bir geri arama işlevi tanımlamak için:

def print_callback(locals):
    print(locals['number'])

ve şöyle çağırıyor example_function:

ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)

daha sonra çıktılar:

0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0

Bu, yazdırma işlevini, temel işlevinden başarıyla ayırır example_function. Bununla birlikte, bu yaklaşımla ilgili temel sorun, geri arama işlevinin yalnızca example_function(bu durumda geçerli sayıyı yarıya indirdikten hemen sonra) belirli bir kısmında çalıştırılabilmesi ve tüm baskının tam olarak orada olması gerektiğidir. Bu bazen geri arama işlevinin tasarımını oldukça karmaşık hale getirir (ve bazı davranışların gerçekleştirilmesini imkansız hale getirir).

Örneğin, sorunun önceki bir bölümünde yaptığımla aynı türden bir baskı elde etmek isterseniz (karşılık gelen yarılanmalarla birlikte hangi sayının işlendiğini gösterir) ortaya çıkan geri arama:

def complicated_callback(locals):
    i_iter = locals['i_iter']
    number = locals['number']
    if i_iter == 0:
        print('Processing number', number*2)
    print(number)
    if i_iter == locals['n_iters']-1:
        print('sum_all:', locals['sum_all']+number)

bu da öncekiyle tam olarak aynı çıktıyı verir:

Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0

ama yazmak, okumak ve hata ayıklamak için bir acıdır.


6
python loggingmodülünü inceleyin
Chris_Rands

@Chris_Rands doğru .. günlük modülünü kullanın .. bu şekilde günlük kaydını açıp kapatabilirsiniz .. aşağıdaki bağlantıyı kullanın. stackoverflow.com/questions/2266646/…
Yatish Kadam

2
loggingModülün burada nasıl yardımcı olacağını görmüyorum . Her ne kadar sorum printbağlamı ayarlarken ifadeler kullanıyor olsa da , aslında herhangi bir isteğe bağlı işlevsellik işlevinin ana amacından nasıl çözüleceğine dair bir çözüm arıyorum. Örneğin, belki bir işlevi çalışır gibi şeyler çizmek istiyorum. Bu durumda loggingmodülün uygulanamayacağına inanıyorum .
JLagana

3
@Pythonic, Python felsefesini desteklemek için python sözdizimini / stilini / yapısını / kullanımını açıklayan bir sıfattır. Bu sözdizimsel veya tasarım kuralı değil, temiz ve bakımı kolay bir python kod tabanı üretmek için sorumlu bir şekilde sürdürülmesi gereken bir yaklaşımdır. Sizin durumunuzda, az sayıda iz veya baskı ifadesine sahip olmak, sürdürülebilirliğe değerler ekler; kendini zorlama. İdeal olduğunu düşündüğünüz bu yaklaşımlardan herhangi birini düşünün.
Nair

1
Bu soru çok geniş. Belirli soruları (kullanma önerilerinin logginggösterdiği gibi) ele alabiliriz, ancak rastgele kodların nasıl ayrılacağını değil.
chepner

Yanıtlar:


4

Fonksiyonun içinden veri kullanmak için fonksiyonun dışında fonksiyonaliteye ihtiyacınız varsa, bunu desteklemek için fonksiyonun içinde bazı mesajlaşma sistemi olması gerekir. Etrafta yol bulunmuyor. Fonksiyonlardaki yerel değişkenler tamamen dışarıdan izole edilmiştir.

Kayıt modülü bir mesaj sistemi kurma konusunda oldukça iyidir. Yalnızca günlük iletilerini yazdırmakla sınırlı değildir - özel işleyicileri kullanarak her şeyi yapabilirsiniz.

Bir mesaj sistemi eklemek, geri arama örneğinize benzer, ancak 'geri aramaların' (günlük tutucular) işlendiği yerlerin içinde herhangi bir yerde example_function (mesajların kaydediciye gönderilmesiyle) belirtilebilmesi dışında. Günlük işleyicileri için gereken değişkenler, iletiyi gönderdiğinizde belirtilebilir (yine de kullanabilirsiniz locals(), ancak ihtiyacınız olan değişkenleri açıkça belirtmek en iyisidir).

Yeni bir example_functionşeye benzeyebilir:

import logging

# Helper function
def send_message(logger, level=logging.DEBUG, **kwargs):
  logger.log(level, "", extra=kwargs)

# Your example function with logging information
def example_function(numbers, n_iters):
    logger = logging.getLogger("example_function")
    # If you have a logging system set up, then we don't want the messages sent here to propagate to the root logger
    logger.propagate = False
    sum_all = 0
    for number in numbers:
        send_message(logger, action="processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            send_message(logger, action="division", i_iter=i_iter, number=number)
        sum_all += number
        send_message(logger, action="sum", sum=sum_all)
    return sum_all

Bu, mesajların işlenebileceği üç konumu belirtir. Kendi başına, bu, kendi example_functionişlevselliğinden başka bir şey yapmayacaktır example_function. Hiçbir şey yazdırmaz veya başka bir işlevsellik yapmaz.

'A ekstra işlevsellik example_functioneklemek için, günlükçüye işleyiciler eklemeniz gerekir.

Örneğin, gönderilen değişkenlerden ( debuggingörneğin örneğinize benzer ) bir miktar baskı yapmak istiyorsanız, özel işleyiciyi tanımlar ve example_functiongünlükçüye eklersiniz :

class ExampleFunctionPrinter(logging.Handler):
    def emit(self, record):
        if record.action == "processing":
          print("Processing number {}".format(record.number))
        elif record.action == "division":
          print(record.number)
        elif record.action == "sum":
          print("sum_all: {}".format(record.sum))

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(ExampleFunctionPrinter())

Sonuçları bir grafiğe çizmek istiyorsanız, sadece başka bir işleyici tanımlayın:

class ExampleFunctionDivisionGrapher(logging.Handler):
    def __init__(self, grapher):
      self.grapher = grapher

    def emit(self, record):
      if record.action == "division":
        self.grapher.plot_point(x=record.i_iter, y=record.number)

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(
    ExampleFunctionDivisionGrapher(MyFancyGrapherClass())
)

İstediğiniz işleyicileri tanımlayabilir ve ekleyebilirsiniz. Bunlar, işlevselliğinden tamamen ayrı olacak example_functionve yalnızca example_functiononlara veren değişkenleri kullanabilecektir .

Günlüğe kaydetme bir mesajlaşma sistemi olarak kullanılabilse de, PyPubSub gibi tam teşekküllü bir mesajlaşma sistemine geçmek daha iyi olabilir , böylece yaptığınız gerçek günlüğe kaydetmeyi engellemez:

from pubsub import pub

# Your example function
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        pub.sendMessage("example_function.processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            pub.sendMessage("example_function.division", i_iter=i_iter, number=number)
        sum_all += number
        pub.sendMessage("example_function.sum", sum=sum_all)
    return sum_all

# If you need extra functionality added in, then subscribe to the messages.
# Otherwise nothing will happen, other than the normal example_function functionality.
def handle_example_function_processing(number):
    print("Processing number {}".format(number))

def handle_example_function_division(i_iter, number):
    print(number)

def handle_example_function_sum(sum):
    print("sum_all: {}".format(sum))

pub.subscribe(
    "example_function.processing",
    handle_example_function_processing
)
pub.subscribe(
    "example_function.division",
    handle_example_function_division
)
pub.subscribe(
    "example_function.sum",
    handle_example_function_sum
)

Cevabınız için teşekkürler RPalmer. loggingModülü kullanarak sağladığınız kod , gerçekten önerdiğim printve ififadelerden daha düzenli ve sürdürülebilir . Ancak, yazdırma işlevini işlevin ana işlevinden ayırmaz example_function. Yani, example_functionaynı anda iki şey yapmanın ana sorunu hala kodunu olmasını istediğimden daha karmaşık hale getiriyor.
JLagana

Bunu, örneğin geri arama önerim ile karşılaştırın. Geri aramalar kullanıldığında, example_functionartık yalnızca bir işlevsellik vardır ve yazdırma işleri (veya sahip olmak istediğimiz diğer işlevler) bunun dışında gerçekleşir.
JLagana

Merhaba @JLagana. My example_function, yazdırma işlevinden ayrılmıştır - işleve eklenen tek işlev, iletileri göndermektir. Tümü yerine yalnızca istediğiniz belirli değişkenleri göndermesi dışında geri arama örneğinize benzer locals(). Ekstra işlevsellik (yazdırma, grafik oluşturma vb.) Yapmak günlük işleyicilerine bağlıdır (kaydediciye başka bir yere bağlarsınız). Hiç işleyici iliştirmenize gerek yoktur, bu durumda mesajlar gönderildiğinde hiçbir şey olmaz. Bunu daha açık hale getirmek için yayımı güncelledim.
RPalmer

Düzeltilmiş duruyorum, örneğin baskı işlevselliğini ana işlevlerinden ayırdı example_function. Şimdi daha fazla netleştirdiğiniz için teşekkürler! Bu cevabı gerçekten çok sevdim, ödenen tek fiyat, sizin de bahsettiğiniz gibi kaçınılmaz gibi görünen mesajların iletilmesinin ek karmaşıklığı. Gözlemci kalıbını okumamı sağlayan PyPubSub'a yapılan referans için de teşekkürler .
JLagana

1

Yalnızca print ifadelerine sadık kalmak istiyorsanız, konsolu yazdırmayı açan / kapatan bir argüman ekleyen bir dekoratör kullanabilirsiniz.

İşte verbose=Falseherhangi bir işleve salt anahtar kelime bağımsız değişkenini ve varsayılan değerini ekleyen , öğretiyi ve imzayı güncelleyen bir dekoratör . İşlevi olduğu gibi çağırmak beklenen çıktıyı döndürür. Fonksiyonu ile çağırmak, verbose=Truebaskı ifadelerini açar ve beklenen çıktıyı döndürür. Bu, her baskıyı bir if debug:blokla önceden yazmak zorunda kalmamanın ek bir avantajına sahiptir .

from functools import wraps
from inspect import cleandoc, signature, Parameter
import sys
import os

def verbosify(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        def toggle(*args, verbose=False, **kwargs):
            if verbose:
                _stdout = sys.stdout
            else:
                _stdout = open(os.devnull, 'w')
            with redirect_stdout(_stdout):
                return func(*args, **kwargs)
        return toggle(*args, **kwargs)
    # update the docstring
    doc = '\n\nOption:\n-------\nverbose : bool\n    '
    doc += 'Turns on/off print lines in the function.\n '
    wrapper.__doc__ = cleandoc(wrapper.__doc__ or '\n') + doc
    # update the function signature to include the verbose keyword
    sig = signature(func)
    param_verbose = Parameter('verbose', Parameter.KEYWORD_ONLY, default=False)
    sig_params = tuple(sig.parameters.values()) + (param_verbose,)
    sig = sig.replace(parameters=sig_params)
    wrapper.__signature__ = sig
    return wrapper

İşlevinizi sarmak artık yazdırma işlevlerini kullanarak açıp / kapatmanıza olanak tanır verbose.

@verbosify
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            print(number)
        sum_all += number
        print('sum_all:', sum_all)
    return sum_all

Örnekler:

example_function([1,3,12], 3)
# returns:
2.0

example_function([1,3,12], 3, verbose=True)
# returns/prints:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
2.0

İncelediğinizde example_function, güncellenmiş belgeleri de görürsünüz. Fonksiyonunuzun bir öğretisi olmadığından, sadece dekoratörde olan şeydir.

help(example_function)
# prints:
Help on function example_function in module __main__:

example_function(numbers, n_iters, *, verbose=False)
    Option:
    -------
    verbose : bool
        Turns on/off print lines in the function.

Kodlama felsefesi açısından. Yan etkileri olmayan bir işleve sahip olmak işlevsel bir programlama paradigmasıdır. Python olabilir işlevsel bir dil olması, ancak münhasıran bu şekilde olmak tasarlanmamıştır. Kodumu her zaman kullanıcı düşünülerek tasarlarım.

Hesaplama adımlarını yazdırma seçeneğini eklemek kullanıcıya bir avantaj sağlıyorsa, bununla ilgili HİÇBİR yanlış var. Tasarım açısından bakıldığında, bir yere yazdırma / kaydetme komutlarını eklemeniz gerekir.


Cevabınız için teşekkürler, James. Sağlanan kod gerçekten önerdiğim, kullanan printve ififadelerden daha organize ve sürdürülebilir . Dahası, baskı işlevinin bir kısmını aslında example_functionçok güzel olan ana işlevselliğinden ayırmayı başarıyor (Dekoratörün doktora, hoş bir dokunuşa otomatik olarak eklenmesini de sevdim). Bununla birlikte, yazdırma işlevselliğini ana işlevinden tamamen ayırmaz example_function: yine de printişlevin gövdesine ifadeleri ve eşlik eden mantığı eklemeniz gerekir .
JLagana

Bunu, örneğin geri arama önerim ile karşılaştırın. Geri çağrıları kullanarak, example_function artık yalnızca bir işlevselliğe sahiptir ve yazdırma işleri (veya sahip olmak istediğimiz diğer işlevler) bunun dışında gerçekleşir.
JLagana

Son olarak, hesaplama adımlarını yazdırmanın kullanıcıya faydası varsa, o zaman yazdırma komutlarını bir yere eklemeye sıkışacağım. Bununla birlikte, onların example_functionbedeninin dışında olmalarını istiyorum , böylece karmaşıklığı sadece ana işlevselliğinin karmaşıklığıyla ilişkili kalır. Tüm bunların gerçek hayattaki uygulamamda, zaten önemli ölçüde karmaşık olan bir ana fonksiyonum var. Vücuduna baskı / çizim / loglama ifadeleri eklemek, onu korumak ve hata ayıklamak için oldukça zor olan bir canavar haline gelir.
JLagana

1

debug_modeKoşulu kapsayan bir işlev tanımlayabilir ve istenen isteğe bağlı işlevi ve bağımsız değişkenlerini bu işleve iletebilirsiniz ( burada önerildiği gibi ):

def DEBUG(function, *args):
    if debug_mode:
        function(*args)

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        DEBUG(print, 'Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            DEBUG(print, number)
        sum_all += number
        DEBUG(print, 'sum_all:', sum_all)
    return sum_all

ns = [1, 3, 12]
debug_mode = True
print(example_function(ns, 3))

Aramadan debug_modeönce açıkça bir değer atanmış olması gerektiğini unutmayınDEBUG .

Şüphesiz, print .

Ayrıca, sayısal bir değer kullanarak bu kavramı birkaç hata ayıklama düzeyine genişletebilirsiniz debug_mode.


Cevabınız için teşekkürler Gerd. Aslında çözümünüz ifher yerde ifade ihtiyacından kurtulur ve ayrıca yazdırmayı açıp kapatmayı kolaylaştırır. Ancak, yazdırma işlevselliğini ana işlevlerinden ayırmaz example_function. Bunu, örneğin geri arama önerim ile karşılaştırın. Geri çağrıları kullanarak, example_function artık yalnızca bir işlevselliğe sahiptir ve yazdırma işleri (veya sahip olmak istediğimiz diğer işlevler) bunun dışında gerçekleşir.
JLagana

1

Cevabımı bir sadeleştirme ile güncelledim: işlev example_function, example_functionartık geçirilip geçirilmediğini görmek için sınamaya gerek kalmayacak şekilde varsayılan bir değerle tek bir geri arama veya kanca geçirilir:

hook=lambda *args, **kwargs: None

Yukarıdaki dönen Noneve example_functionbu varsayılan değeri çağırabilecek bir lambda ifadesidir.hook işlev içindeki çeşitli yerlerde herhangi bir konum ve anahtar kelime parametresi birleşimi .

Aşağıdaki örnekte yalnızca "end_iteration"ve "result"etkinlikleriyle ilgileniyorum .

def example_function(numbers, n_iters, hook=lambda *args, **kwargs: None):
    hook("init")
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            hook("start_iteration", number)
            number = number/2
            hook("end_iteration", number)
        sum_all += number
    hook("result", sum_all)
    return sum_all

if __name__ == '__main__':
    def my_hook(event_type, *args):
        if event_type in ["end_iteration", "result"]:
            print(args[0])

    print('sum = ', example_function([1, 3, 12], 3))
    print('sum = ', example_function([1, 3, 12], 3, my_hook))

Baskılar:

sum =  2.0
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
sum =  2.0

Kanca işlevi istediğiniz kadar basit veya ayrıntılı olabilir. Burada olay türünü kontrol ediyor ve basit bir baskı yapıyor. Ancak bir loggerörnek alıp mesajı kaydedebilir. İhtiyacınız varsa kayıt işleminin tüm zenginliğine sahip olabilirsiniz, ancak ihtiyacınız yoksa basitliğe sahip olabilirsiniz.


Cevabınız için teşekkürler Ronald. Geri çağırma fikrini işlevin farklı bölümlerinde yürütmek (ve bunlara bir bağlam değişkeni geçirmek) için genişletme fikri, gerçekten de en iyi yol gibi görünüyor. Geri aramaların yazılmasını çok daha karmaşık hale getirir example_function.
JLagana

Varsayılan değer hoş bir dokunuş; bir sürü ififadeyi kaldırmak için basit bir yol :)
JLagana
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.