Verständnisfrage zu Bitweiser-Operatoren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1737
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

bei mir tauchen immer wieder Fragen auf, wenn es um Bitweiser-Operatoren geht.
Wenn ich zum Beispiel folgendes ausführe, erhalte ich ein Ergebnis, das ich nur bedingt erwarte:

Code: Alles auswählen

>>>set([1, 2, 3, 4]) & set([7, 3, 6, 1])
{1, 3}
An sich erwarte ich schon, dass die 1 und die 3 geliefert werden. Allerdings frage ich mich, wie läuft der Prozess dahinter ab, bis die Bits, bildlich gesprochen, übereinander stehen und kombiniert werden können? Weil wenn ich jetzt jede Zahl in der geschriebenen Reihenfolge in Binär übersetze und die zwei Mengen untereinander schreibe, dann steht die 1 nicht unter der 1, sondern unter der 4. Ich habe dann sowas:

Code: Alles auswählen

0b1, 0b10, 0b11, 0b100
0b111, 0b11, 0b110, 0b1
Werden die Werte vor so einem Vergleich immer erst sortiert? Aufsteigend? Und wie ist das bei Strings?

Code: Alles auswählen

>>>set("a4bc") & set("cgb4")
{'c', 'b', '4'}
Zusammengefasst interessiert mich, was passiert intern, bis es zur Verknüpfung zu den Bits kommt?

Ich bin euch sehr dankbar für jede Antwort. Macht euch aber bitte nicht die Mühe, die Frage in ChatGPT zu tippen und die Antwort zu posten. Ich frage und bitte bewusst um menschliches Wissen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
snafu
User
Beiträge: 6967
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Wie kommst du darauf, dass bei Sets bitweise Operationen zur Anwendung kommen? Wenn du in der Shell zwei Aufrufe mit einer Pipe verknüpfst, dann wird intern ja auch kein bitweises Oder auf die Rückgabewerte angewendet.
Benutzeravatar
pillmuncher
User
Beiträge: 1534
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Deine Verwirrung kommt daher, dass du ein unzutreffendes mentales Bild davon hast, was & und | bedeuten. Genauer gesagt, dass du ihre Anwendung auf Bitketten als fundamental anzusehen scheinst. Es gibt hier keine fundamentale Bedeutung, sondern nur strukturell ähnliche Verwendungsweisen in unterschiedlichen mathematischen Gebieten.

Tatsächlich sind & und | zunächst nur andere symbolische Namen für die logischen Operatoren∧und∨. Die Boolesche Algebra, auf der Bitketten-Berechnungen beruhen, bildet einen vollständingen Verband. ∧und∨ sind dabei die sog. join und meet Operatoren. Mengen bilden ebenfalls einen solchen vollständingen Verband. Die Operatoren aus der Mengenlehre sind der Vereinigungsoperator ∪ und der Schnittmengenoperator ∩. Die visuelle Ähnlichkeit zwischen ∩ und ∧auf der einen, und ∪ und ∨ auf der anderen Seite sind nicht zufällig gewählt. ab in der Logik bedeutet "sowohl a als auch b" und ab in der Mengenlehre bedeutet "die Menge der Elemente die sowohl in a als auch in b vorkommen". Analog dazu bedeutet ab "a oder b oder beides" und ab "die Menge der Elemente die in a oder b oder beiden vorkommen".

Wenn man die Namen ∧und∨ durch & und | ersetzt und auf ein einzelnes Paar von Bits anwendet, dann bekommt man zwei Wahrheitstabellen:

Code: Alles auswählen

A B | A & B
----+------
0 0 |   0
0 1 |   0
1 0 |   0
1 1 |   1

A B | A | B
----+------
0 0 |   0
0 1 |   1
1 0 |   1
1 1 |   1
Wenn man logische Operatoren verwendet, dann sieht das so aus:

Code: Alles auswählen

P Q | P ∧ Q
----+-------
⊥ ⊥ |   ⊥
⊥ ⊤ |   ⊥
⊤ ⊥ |   ⊥
⊤ ⊤ |   ⊤

P Q | P ∨ Q
----+-------
⊥ ⊥ |   ⊥
⊥ ⊤ |   ⊤
⊤ ⊥ |   ⊤
⊤ ⊤ |   ⊤
Die typographischen Namen für ⊤ und ⊥ sind Down Tack und Up Tack, in der Logik True und False oder Verum und Falsum, in der Verbandstheorie Top und Bottom, und in der Ordnungstheorie (Verbände sind Ordnungen) Supremum und Infimum.

Wie kommt man nun zu den Operationen auf Bitketten? Das ist einfach die paarweise Anwendung derselben Operation auf zwei "zipped" Bitketten:

Code: Alles auswählen

  0b101 
& 0b110
-------
  0b100 

  0b101 
| 0b110
-------
  0b111
Wenn die Bitketten Python-Listen wären, könnte man schreiben:

Code: Alles auswählen

>>> [a & b for a, b in zip([1, 0, 1], [1, 1, 0])]
[1, 0, 0]
>>> [a | b for a, b in zip([1, 0, 1], [1, 1, 0])]
[1, 1, 1]
Das lässt sich nicht eins zu eins auf die Mengenlehre übertragen, weil Elemente in einer Menge nicht ordinal geordnet sind. Man kann von einer Menge Zahlen sagen, dass es eine kleinste oder größte gibt, oder dass von zwei Zahlen eine größer oder kleiner als die andere ist, aber es gibt keine "erste" oder "letzte" oder "mittlere" Zahl, weswegen man zip hier nicht einfach so anwenden kann. Statt dessen bildet Python eine neue Menge, die alle Elemente beider Mengen enthält, oder testet welche Elemente in beiden Mengen vorkommen und bildet daraus dann eine neue Menge.


Disclaimer: Ja, die Tabellen habe ich von ChatGPT zeichnen lassen, weil ich zu faul war, das selbst zu tun. Alles andere ist von mir.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
snafu
User
Beiträge: 6967
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Dennis89: Hier findet man übrigens die passende Stelle in der Doku zu den verschiedenen Set-Operationen:

https://docs.python.org/3/library/stdtypes.html#set

Es werden immer zuerst die ausgeschriebenen Methodennamen genannt und direkt darunter der Operator, der jeweils als Alias dafür verwendet wird. Anschließend erfolgt die Beschreibung der Methode. Niemand spricht dort von bitweisen Operationen.

Ich würde die Zeichen auch anders als du in erster Linie als logische Operatoren ansehen. Die bitweisen Operatoren sind einfach ein Spezialfall davon, auch wenn dir vielleicht bisher keine andere Verwendung begegnet ist. Es sind in dem Sinne ja ebenso logische Operationen, nur dass die auf jedes einzelne Bit angewendet werden, wie ja bereits anschaulich erklärt wurde.
Pedroski55
User
Beiträge: 54
Registriert: Freitag 25. Juli 2025, 00:20

bei mir tauchen immer wieder Fragen auf, wenn es um Bitweiser-Operatoren geht.
Vielleicht hilft das hier:

Code: Alles auswählen

"""
AND  & True if both are True
OR  | True if one or both are True
NOT  ~ Bitwise inversion 1 becomes 0, 0 becomes 1
XOR  ^ True if one is True and other is False
LEFT-SHIFT  << x<< y moves all values in x y places to the left
RIGHT-SHIFT  >> x >> y moves all values in x y places to the left
"""

a = [1, 2, 3, 4]
b = [7, 3, 6, 1]
abin = [format(num, '08b') for num in a] # ['00000001', '00000010', '00000011', '00000100']
bbin = [format(num, '08b') for num in b] # ['00000111', '00000011', '00000110', '00000001']

for i in range(len(abin)):
    print(f'abin[{i}] = {abin[i]}')
    print(f'bbin[{i}] = {bbin[i]}')
    num1 = a[i] & b[i]    
    print(f'AND = {format(num1, "08b")}')  
    num2 = a[i] | b[i]
    print(f'OR = {format(num2, "08b")}')
    print('********')

# LEFT-SHIFT
x = 0b00000101 # 5
y = x << 2  # 20
z = bin(y) # '0b10100'
Benutzeravatar
__blackjack__
User
Beiträge: 14384
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Pedroski55: Du erklärst hier die bitweisen Operatoren — gefragt war aber wie die bei den gezeigten Mengenoperationen verwendet werden. Und die Antwort wurde ja auch schon gegeben: gar nicht.

``for i in range(len(sequence))`` ist ein „anti pattern“ in Python. Man kann direkt über die Elemente von Sequenzen iterieren, ohne den unnötigen Zwischenschritt über einen Laufindex. Sollte man _zusätzlich_ zu den Elementen eine laufende Zahl benötigen gibt es `enumerate()`. Um über die Elemente von mehr als einer Sequenz ”parallel” zu iterieren gibt es `zip()`.

`format()` _innerhalb_ einer f-Zeichenkette zu verwenden ist unsinnig. Die Funktionalität hat man doch bereits durch die f-Zeichenkette. Das ”vorberechnen” von `abin` und `bbin` macht das hier auch nur unnötig komplizierter als es sein müsste.

Code: Alles auswählen

#!/usr/bin/env python3
for a, b in zip([1, 2, 3, 4], [7, 3, 6, 1]):
    print(f"  a = {a:04b}")
    print(f"  b = {b:04b}")
    print(f"AND = {a & b:04b}")
    print(f" OR = {a | b:04b}")
    print(f"XOR = {a ^ b:04b}")
    print("**********")
Who is General Failure and why is he reading my hard disk?
Benutzeravatar
snafu
User
Beiträge: 6967
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Pedroski55: Und es ist nicht nur der unnötige Zwischenschritt für den Indexzugriff, sondern auch fehleranfällig, falls man Zahlen ändert und versehentlich die Liste a länger als die Liste b macht. Dann wirft das nämlich einen IndexError und den auch erst nach Ausgabe des letzten möglichen Zahlenpaars. Ist hier bei dem Beispielcode nicht ganz so wild, aber innerhalb eines größeren komplexeren Programms durchaus nervig. zip() hingegen würde einfach alle Zahlen, die zu viel sind, ignorieren und dann keine weiteren Paare liefern.

Anbei der Code in Rust (einfach weil ich Lust drauf hatte). Liegt für diese recht einfache Aufgabe irgendwo zwischen Python und C. Wobei Rust auch zip() kennt, wenn auch nur als Methode der Iterator API und leider auch immer nur für ein Argument statt beliebig vielen wie in Python.

Code: Alles auswählen

fn main() {
    let vec_a = [1, 2, 3, 4];
    let vec_b = [7, 3, 6, 1];

    for (a, b) in vec_a.into_iter().zip(vec_b) {
        println!("a   = {:04b}", a);
        println!("b   = {:04b}", b);
        println!("AND = {:04b}", a & b);
        println!("OR  = {:04b}", a | b);
        println!("XOR = {:04b}", a ^ b);
        println!("**********");
    }
}
nezzcarth
User
Beiträge: 1804
Registriert: Samstag 16. April 2011, 12:47

Dennis89 hat geschrieben: Mittwoch 22. April 2026, 10:36 Zusammengefasst interessiert mich, was passiert intern, bis es zur Verknüpfung zu den Bits kommt?
Zusammengefasst kann man sagen, dass & und | hier effektiv keine bitweisen Operatoren sind, sondern Mengenoperatoren. Ducktyping und so:

Code: Alles auswählen

In [3]: {3,5}.__or__({5,6})
Out[3]: {3, 5, 6}

In [4]: {3,5}.__xor__({5,6})
Out[4]: {3, 6}

In [5]: {3,5}.__and__({5,6})
Out[5]: {5}

In [6]: {3,5}.__sub__({5,6})
Out[6]: {3}

In [7]: {3,5} | {5,6}
Out[7]: {3, 5, 6}

In [8]: {3,5} ^ {5,6}
Out[8]: {3, 6}

In [9]: {3,5} & {5,6}
Out[9]: {5}

In [10]: {3,5} - {5,6}
Out[10]: {3}

Benutzeravatar
Dennis89
User
Beiträge: 1737
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

super, vielen Dank für eure (sehr detaillierten) Erklärungen! Das war mir so nicht bewusst, wie ihr richtig erkannt habt. Zu den einzelnen Beiträgen, habe ich auch keine weiteren Fragen mehr, wurde sehr gut erklärt.


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14384
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Auch wenn keine Fragen mehr sind, hätte ich trotzdem noch eine Erklärung. :-) Nämlich wo sich die beiden Themen Mengenoperationen und bitweise Operationen tatsächlich treffen können: Wenn man eine feste Grundmenge an Elementen hat, kann man jedem Element eine Bitposition in einer Zahl zuordnen, die dann bedeutet das Element ist in der Menge enthalten (1) oder nicht enthalten (0). Wenn man das so kodiert, dann entsprechen die Operatoren ``&`` und ``|`` bitweise und was die Menge(n) angeht einander.

In Python findet man das beispielsweise in der Standardbibliothek im `enum`-Modul mit dem `IntFlag`-Typ. Mal als typisches Leer-/Handbuchbeispiel die Wochentage als Menge:

Code: Alles auswählen

#!/usr/bin/env python3
from enum import auto, IntFlag


class Days(IntFlag):
    NONE = 0
    MON = auto()
    TUE = auto()
    WED = auto()
    THU = auto()
    FRI = auto()
    SAT = auto()
    SUN = auto()


def print_days(label, value):
    print(f"{label:>5} = {value:08b} = {value!r}")


def main():
    for day in Days:
        print_days(day.name, day)

    all_days = ~Days.NONE
    print_days("all", all_days)

    days_a = Days.MON | Days.SAT | Days.SUN
    days_b = Days.WED | Days.SAT
    print_days("a", days_a)
    print_days("b", days_b)
    print_days("a | b", days_a | days_b)
    print_days("a & b", days_a & days_b)
    print_days("a ^ b", days_a ^ days_b)


if __name__ == "__main__":
    main()
Ausgabe:

Code: Alles auswählen

  MON = 00000001 = <Days.MON: 1>
  TUE = 00000010 = <Days.TUE: 2>
  WED = 00000100 = <Days.WED: 4>
  THU = 00001000 = <Days.THU: 8>
  FRI = 00010000 = <Days.FRI: 16>
  SAT = 00100000 = <Days.SAT: 32>
  SUN = 01000000 = <Days.SUN: 64>
  all = 01111111 = <Days.MON|TUE|WED|THU|FRI|SAT|SUN: 127>
    a = 01100001 = <Days.MON|SAT|SUN: 97>
    b = 00100100 = <Days.WED|SAT: 36>
a | b = 01100101 = <Days.MON|WED|SAT|SUN: 101>
a & b = 00100000 = <Days.SAT: 32>
a ^ b = 01000101 = <Days.MON|WED|SUN: 69>
Bei Programmiersprachen die keine spezielle Unterstützung für so etwas haben, macht man das oft einfach manuell mit Zahlen, Konstanten, und bitweisen Operationen. In C beispielsweise.

Es gibt aber auch Sprachen bei denen so etwas direkt als Datentyp eingebaut ist. In Pascal kann man Mengentypen von jedem aufzählbaren Typen oder Bereiche davon erstellen, mit bis zu 256 Elementen in der Grundmenge, die dann mit bis zu 32 Bytes gespeichert werden.
Who is General Failure and why is he reading my hard disk?
Sirius3
User
Beiträge: 18403
Registriert: Sonntag 21. Oktober 2012, 17:20

Man kann auch umgekehrt bit-Operationen als Mengen ausdrücken:

Code: Alles auswählen

from enum import auto, IntFlag

class Bits(IntFlag):
    NONE = 0
    BIT0 = auto()
    BIT1 = auto()
    BIT2 = auto()
    BIT3 = auto()
    BIT4 = auto()
    BIT5 = auto()
    BIT6 = auto()
    BIT7 = auto()

num84 = set(Bits(84))
num107 = set(Bits(107))
print(num84)
print(num107)
print(num107 & num84)
print(num107 | num84)
print(sum(num107 | num84))
Antworten