Apakah ada cara pythonic untuk memisahkan fungsi opsional dari tujuan utama suatu fungsi?

11

Konteks

Misalkan saya memiliki kode Python berikut:

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_functiondi sini hanya melalui masing-masing elemen dalam nsdaftar dan membagi dua 3 kali, sambil mengumpulkan hasilnya. Output dari menjalankan skrip ini adalah:

2.0

Sejak 1 / (2 ^ 3) * (1 + 3 + 12) = 2.

Sekarang, katakanlah (untuk alasan apa pun, mungkin debugging, atau logging), saya ingin menampilkan beberapa jenis informasi tentang langkah-langkah perantara yang example_functionsedang dilakukan. Mungkin saya akan menulis ulang fungsi ini menjadi sesuatu seperti ini:

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

yang sekarang, ketika dipanggil dengan argumen yang sama seperti sebelumnya, menampilkan yang berikut:

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

Ini mencapai apa yang saya maksudkan. Namun, ini sedikit bertentangan dengan prinsip bahwa suatu fungsi seharusnya hanya melakukan satu hal, dan sekarang kode untuk example_functionsligthly lebih panjang dan lebih kompleks. Untuk fungsi sederhana seperti ini, ini bukan masalah, tetapi dalam konteks saya, saya memiliki fungsi yang cukup rumit saling memanggil, dan pernyataan pencetakan sering melibatkan langkah-langkah yang lebih rumit daripada yang ditunjukkan di sini, menghasilkan peningkatan substansial dalam kompleksitas kode saya (untuk satu fungsi saya ada lebih banyak baris kode yang terkait dengan logging daripada ada baris yang terkait dengan tujuan sebenarnya!).

Selain itu, jika nanti saya memutuskan bahwa saya tidak ingin ada lagi pernyataan pencetakan dalam fungsi saya, saya harus melalui example_functiondan menghapus semua printpernyataan secara manual, bersama dengan variabel apa pun yang terkait dengan fungsi ini, suatu proses yang membosankan dan juga salah. -Rentan.

Situasi semakin memburuk jika saya ingin selalu memiliki kemungkinan untuk mencetak atau tidak mencetak selama pelaksanaan fungsi, membuat saya mendeklarasikan dua fungsi yang sangat mirip (satu dengan printpernyataan, satu tanpa), yang mengerikan untuk dijaga, atau untuk mendefinisikan sesuatu seperti:

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

yang menghasilkan fungsi rumit dan mudah-mudahan tidak perlu, bahkan dalam kasus sederhana kami example_function.


Pertanyaan

Apakah ada cara pythonic untuk "memisahkan" fungsi pencetakan dari fungsi asli example_function?

Secara umum, adakah cara pythonic untuk memisahkan fungsi opsional dari tujuan utama suatu fungsi?


Apa yang saya coba sejauh ini:

Solusi yang saya temukan saat ini adalah menggunakan callback untuk decoupling. Misalnya, seseorang dapat menulis ulang example_functionseperti ini:

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

dan kemudian mendefinisikan fungsi panggilan balik yang melakukan fungsi pencetakan mana pun yang saya inginkan:

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

dan memanggil example_functionseperti ini:

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

yang kemudian menghasilkan:

0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0

Ini berhasil memisahkan fungsi pencetakan dari fungsionalitas dasar example_function. Namun, masalah utama dengan pendekatan ini adalah bahwa fungsi callback hanya dapat dijalankan pada bagian tertentu dari example_function(dalam hal ini tepat setelah mengurangi separuh nomor saat ini), dan semua pencetakan harus terjadi tepat di sana. Ini kadang-kadang memaksa desain fungsi callback menjadi cukup rumit (dan membuat beberapa perilaku tidak mungkin untuk dicapai).

Sebagai contoh, jika seseorang ingin mencapai jenis pencetakan yang sama persis seperti yang saya lakukan di bagian sebelumnya dari pertanyaan (menunjukkan nomor mana yang sedang diproses, bersama dengan separuh yang sesuai) panggilan balik yang dihasilkan adalah:

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)

yang menghasilkan output yang sama persis seperti sebelumnya:

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

tetapi sulit untuk menulis, membaca, dan men-debug.

JLagana
sumber
6
lihat loggingmodul python
Chris_Rands
@Chris_Rands benar .. gunakan modul logging .. dengan cara itu Anda dapat menghidupkan dan mematikan logging .. gunakan tautan berikut. stackoverflow.com/questions/2266646/…
Yatish Kadam
2
Saya tidak melihat bagaimana loggingmodul akan membantu di sini. Meskipun pertanyaan saya menggunakan printpernyataan ketika mengatur konteks, saya sebenarnya mencari solusi bagaimana memisahkan segala jenis fungsi opsional dari tujuan utama suatu fungsi. Misalnya, mungkin saya ingin fungsi untuk merencanakan berbagai hal saat berjalan. Dalam hal ini saya percaya bahwa loggingmodul itu bahkan tidak akan berlaku.
JLagana
3
@Pythonic adalah kata sifat yang menjelaskan sintaksis python / style / struktur / penggunaan untuk menjunjung tinggi filosofi Python. Ini bukan aturan sintaksis atau desain, melainkan pendekatan yang perlu ditegakkan secara bertanggung jawab untuk menghasilkan basis kode python yang bersih dan dapat dipelihara. Dalam kasus Anda, memiliki beberapa baris jejak atau pernyataan cetak menambah nilai untuk pemeliharaan kemudian memilikinya; jangan keras pada dirimu sendiri. Pertimbangkan pendekatan apa pun yang menurut Anda ideal.
Nair
1
Pertanyaan ini terlalu luas. Kami mungkin dapat menjawab pertanyaan-pertanyaan spesifik (seperti saran untuk digunakan loggingmenunjukkan), tetapi tidak bagaimana memisahkan kode arbitrer.
chepner

Jawaban:

4

Jika Anda memerlukan fungsionalitas di luar fungsi untuk menggunakan data dari dalam fungsi, maka perlu ada beberapa sistem pesan di dalam fungsi untuk mendukung ini. Tidak ada jalan lain untuk ini. Variabel lokal dalam fungsi sepenuhnya terisolasi dari luar.

Modul logging cukup bagus dalam mengatur sistem pesan. Tidak hanya terbatas untuk mencetak pesan log - menggunakan penangan khusus, Anda dapat melakukan apa saja.

Menambahkan sistem pesan mirip dengan contoh panggilan balik Anda, kecuali bahwa tempat-tempat di mana 'panggilan balik' (penangan logging) ditangani dapat ditentukan di mana saja di dalam example_function (dengan mengirim pesan ke pencatat). Variabel apa saja yang diperlukan oleh penangan logging dapat ditentukan saat Anda mengirim pesan (Anda masih bisa menggunakan locals(), tetapi yang terbaik adalah secara eksplisit mendeklarasikan variabel yang Anda butuhkan).

Yang baru example_functionmungkin terlihat seperti:

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

Ini menentukan tiga lokasi di mana pesan dapat ditangani. Dengan sendirinya, ini example_functiontidak akan melakukan apa pun selain fungsi example_functionitu sendiri. Itu tidak akan mencetak apa pun, atau melakukan fungsi lainnya.

Untuk menambahkan fungsionalitas tambahan ke example_function, maka Anda perlu menambahkan penangan ke logger.

Misalnya, jika Anda ingin melakukan beberapa pencetakan dari variabel yang dikirim (mirip dengan debuggingcontoh Anda ), maka Anda mendefinisikan penangan kustom, dan menambahkannya ke example_functionlogger:

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())

Jika Anda ingin memplot hasil pada grafik, maka cukup tentukan handler lain:

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())
)

Anda dapat mendefinisikan dan menambahkan penangan apa pun yang Anda inginkan. Mereka akan benar-benar terpisah dari fungsionalitas example_function, dan hanya dapat menggunakan variabel yang diberikan example_functionkepada mereka.

Meskipun pencatatan dapat digunakan sebagai sistem pengiriman pesan, mungkin lebih baik untuk pindah ke sistem perpesanan yang lengkap, seperti PyPubSub , sehingga tidak mengganggu pencatatan yang sebenarnya yang mungkin Anda lakukan:

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
)
RPalmer
sumber
Terima kasih atas jawabannya, RPalmer. Kode yang Anda berikan menggunakan loggingmodul memang lebih terorganisir dan dapat dikelola daripada apa yang saya usulkan menggunakan printdan ifpernyataan. Namun, itu tidak memisahkan fungsi pencetakan dari fungsi utama example_functionfungsi. Artinya, masalah utama setelah example_functionmelakukan dua hal sekaligus masih tetap, membuat kodenya lebih rumit daripada yang saya inginkan.
JLagana
Bandingkan ini dengan misalnya saran panggilan balik saya. Menggunakan panggilan balik, example_functionsekarang hanya memiliki satu fungsi, dan hal-hal pencetakan (atau fungsi apa pun yang ingin kita miliki) terjadi di luarnya.
JLagana
Hai @JLagana. My example_functiondipisahkan dari fungsi pencetakan - satu-satunya fungsi yang ditambahkan ke fungsi adalah mengirim pesan. Ini mirip dengan contoh panggilan balik Anda, kecuali hanya mengirim variabel spesifik yang Anda inginkan, bukan semua locals(). Terserah penangan log (yang Anda lampirkan ke logger di tempat lain) untuk melakukan fungsionalitas tambahan (pencetakan, grafik, dll). Anda tidak perlu melampirkan penangan sama sekali, dalam hal ini tidak ada yang terjadi ketika pesan dikirim. Saya telah memperbarui posting saya untuk membuatnya lebih jelas.
RPalmer
Saya berdiri dikoreksi, contoh Anda memang memisahkan fungsi pencetakan dari fungsi utama example_function. Terima kasih telah membuatnya lebih jelas sekarang! Saya sangat suka jawaban ini, satu-satunya harga yang dibayar adalah kompleksitas tambahan dari pesan yang lewat, yang, seperti yang Anda sebutkan, tampaknya tidak dapat dihindari. Terima kasih juga untuk referensi ke PyPubSub, yang membuat saya membaca tentang pola pengamat .
JLagana
1

Jika Anda ingin tetap dengan hanya mencetak pernyataan, Anda dapat menggunakan dekorator yang menambahkan argumen yang menghidupkan / mematikan pencetakan ke konsol.

Berikut adalah dekorator yang menambahkan argumen hanya kata kunci dan nilai default verbose=Falseuntuk fungsi apa pun, memperbarui dokumen dan tanda tangan. Memanggil fungsi apa adanya mengembalikan output yang diharapkan. Memanggil fungsi dengan verbose=Trueakan mengaktifkan pernyataan cetak dan mengembalikan hasil yang diharapkan. Ini memiliki manfaat tambahan karena tidak harus membuka setiap cetakan dengan if debug:blok.

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

Membungkus fungsi Anda sekarang memungkinkan Anda untuk menghidupkan / mematikan fungsi cetak menggunakan 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

Contoh:

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

Saat Anda memeriksa example_function, Anda akan melihat dokumentasi yang diperbarui juga. Karena fungsi Anda tidak memiliki dokumen, itu hanya apa yang ada di dekorator.

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.

Dalam hal filosofi pengkodean. Memiliki fungsi yang tidak menimbulkan efek samping adalah paradigma pemrograman fungsional. Python bisa menjadi bahasa fungsional, tetapi tidak dirancang untuk secara eksklusif seperti itu. Saya selalu mendesain kode saya dengan memikirkan pengguna.

Jika menambahkan opsi untuk mencetak langkah-langkah perhitungan adalah keuntungan bagi pengguna, maka TIDAK ADA yang salah dengan melakukan itu. Dari sudut pandang desain, Anda akan terjebak dengan menambahkan perintah print / logging di suatu tempat.

James
sumber
Terima kasih atas jawabannya, James. Kode yang disediakan memang lebih terorganisir dan dapat dikelola daripada yang saya usulkan, yang menggunakan printdan ifpernyataan. Selain itu, ia benar-benar berhasil memisahkan bagian dari fungsi pencetakan dari example_functionfungsi utama, yang sangat bagus (saya juga suka bahwa dekorator secara otomatis menambahkan ke docstring, sentuhan yang bagus). Namun, itu tidak sepenuhnya memisahkan fungsi pencetakan dari fungsi utama example_function: Anda masih harus menambahkan printpernyataan dan logika yang menyertainya ke tubuh fungsi `s.
JLagana
Bandingkan ini dengan misalnya saran panggilan balik saya. Menggunakan panggilan balik, example_function sekarang hanya memiliki satu fungsi, dan hal-hal pencetakan (atau fungsi apa pun yang ingin kita miliki) terjadi di luarnya.
JLagana
Terakhir, kami setuju bahwa jika mencetak langkah-langkah perhitungan adalah keuntungan bagi pengguna, maka saya akan terjebak dengan menambahkan perintah pencetakan di suatu tempat. Namun saya ingin mereka berada di luar example_functiontubuh, sehingga kompleksitasnya tetap hanya terkait dengan kompleksitas fungsi utamanya. Dalam aplikasi kehidupan nyata saya dari semua ini, saya memiliki fungsi utama yang sudah sangat kompleks. Menambahkan pernyataan mencetak / merencanakan / mencatat ke dalam tubuhnya membuatnya menjadi binatang buas yang cukup sulit untuk dirawat dan didebug.
JLagana
1

Anda dapat mendefinisikan fungsi yang merangkum debug_modekondisi dan meneruskan fungsi opsional yang diinginkan dan argumennya ke fungsi tersebut (seperti yang disarankan di sini ):

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))

Catatan yang debug_modejelas harus telah diberi nilai sebelum menelepon DEBUG.

Tentu saja dimungkinkan untuk menjalankan fungsi selain print.

Anda juga dapat memperluas konsep ini ke beberapa level debug dengan menggunakan nilai numerik untuk debug_mode.

Gerd
sumber
Terima kasih atas jawabannya, Gerd. Memang solusi Anda menghilangkan kebutuhan akan ifpernyataan di semua tempat, dan juga membuatnya lebih mudah untuk menghidupkan dan mematikan pencetakan. Namun, itu tidak memisahkan fungsi pencetakan dari fungsi utama example_function. Bandingkan ini dengan misalnya saran panggilan balik saya. Menggunakan panggilan balik, example_function sekarang hanya memiliki satu fungsi, dan hal-hal pencetakan (atau fungsi apa pun yang ingin kita miliki) terjadi di luarnya.
JLagana
1

Saya telah memperbarui jawaban saya dengan penyederhanaan: fungsi example_functiondilewatkan satu panggilan balik atau pengait dengan nilai default sehingga example_functiontidak perlu lagi menguji untuk melihat apakah sudah lulus atau tidak:

hook=lambda *args, **kwargs: None

Di atas adalah ekspresi lambda yang mengembalikan Nonedan example_functiondapat memanggil nilai default ini hookdengan kombinasi parameter posisi dan kata kunci di berbagai tempat dalam fungsi.

Dalam contoh di bawah ini, saya hanya tertarik pada acara "end_iteration"dan "result".

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))

Cetakan:

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

Fungsi kait bisa sesederhana atau serumit yang Anda inginkan. Ini dia melakukan pengecekan jenis acara dan melakukan cetak sederhana. Tapi itu bisa mendapatkan loggercontoh dan mencatat pesan. Anda dapat memiliki semua kekayaan penebangan jika Anda membutuhkannya tetapi kesederhanaan jika tidak.

Booboo
sumber
Terima kasih atas jawabannya, Ronald. Gagasan memperluas ide panggilan balik untuk mengeksekusi panggilan balik di bagian fungsi yang berbeda (dan mengirimkan variabel konteks kepada mereka) tampaknya merupakan cara terbaik untuk melakukannya. Itu membuatnya jauh lebih mudah untuk menulis panggilan balik dan dengan harga yang wajar dalam kompleksitas tambahan example_function.
JLagana
Sentuhan bagus dengan nilai default; ini cara mudah untuk menghapus banyak ifpernyataan :)
JLagana